diff --git a/docs/_static/gallery/peeling-layers-thumbnail.png b/docs/_static/gallery/peeling-layers-thumbnail.png new file mode 100644 index 00000000..e718f749 Binary files /dev/null and b/docs/_static/gallery/peeling-layers-thumbnail.png differ diff --git a/docs/conf.py b/docs/conf.py index 92de1ff7..60750bc3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -220,20 +220,18 @@ # -- Options for nbsphinx gallery------------------------------------------ notebook_root = 'gallery' thumbnail_root = os.path.join('_static', 'gallery') +gallery_notebooks = [ + 'nyc-taxi', + 'masking-a-range', + 'rectangle-selection', + 'scatter3d-with-threshold', + 'scatter3d-with-slider', + 'interactive-masking', + 'peeling-layers', +] nbsphinx_thumbnails = { - os.path.join(notebook_root, 'nyc-taxi'): os.path.join( - thumbnail_root, 'nyc-taxi-thumbnail.png' - ), - os.path.join(notebook_root, 'masking-a-range'): os.path.join( - thumbnail_root, 'masking-a-range-thumbnail.png' - ), - os.path.join(notebook_root, 'rectangle-selection'): os.path.join( - thumbnail_root, 'rectangle-selection-thumbnail.png' - ), - os.path.join(notebook_root, 'scatter3d-with-threshold'): os.path.join( - thumbnail_root, 'scatter3d-with-threshold-thumbnail.png' - ), - os.path.join(notebook_root, 'scatter3d-with-slider'): os.path.join( - thumbnail_root, 'scatter3d-with-slider-thumbnail.png' - ), + os.path.join(notebook_root, notebook): os.path.join( + thumbnail_root, f'{notebook}-thumbnail.png' + ) + for notebook in gallery_notebooks } diff --git a/docs/gallery/index.ipynb b/docs/gallery/index.ipynb index 7baddadb..1760ff8d 100644 --- a/docs/gallery/index.ipynb +++ b/docs/gallery/index.ipynb @@ -3,7 +3,13 @@ { "cell_type": "markdown", "id": "ad3c84db-66fa-4438-9475-5099eacf34ba", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "# Gallery" ] @@ -12,6 +18,10 @@ "cell_type": "markdown", "id": "6a6ec63f-51df-4bca-ab29-762f6d13420c", "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, "tags": [ "nbsphinx-gallery" ] @@ -21,7 +31,9 @@ "* [Masking a range](masking-a-range.ipynb)\n", "* [Rectangle selection](rectangle-selection.ipynb)\n", "* [3-D scatter plot with threshold](scatter3d-with-threshold.ipynb)\n", - "* [3-D scatter plot with slider](scatter3d-with-slider.ipynb)" + "* [3-D scatter plot with slider](scatter3d-with-slider.ipynb)\n", + "* [Interactive masking](interactive-masking.ipynb)\n", + "* [Peeling off the layers](peeling-layers.ipynb)" ] } ], @@ -40,8 +52,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.15" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/docs/gallery/interactive-masking.ipynb b/docs/gallery/interactive-masking.ipynb new file mode 100644 index 00000000..65ff558b --- /dev/null +++ b/docs/gallery/interactive-masking.ipynb @@ -0,0 +1,328 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "da024b16-3a9d-4761-983b-ecb94ef192ad", + "metadata": {}, + "source": [ + "# Interactive masking\n", + "\n", + "In this example, we will use a custom drawing tool to draw rectangles on a 2D figure.\n", + "The data inside the rectangles will be masked." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58d40a92-d9a9-47e9-9c2e-151d734c48df", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "%matplotlib widget\n", + "import plopp as pp\n", + "import scipp as sc\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "d252eb07-78fc-416b-addc-7b9f273ba7a8", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We first generate some data that contains three bands of peaks that all have different spreads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c05fe5b-483a-438e-a7ba-a64b24a33b61", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from plopp.data.examples import three_bands\n", + "\n", + "da = three_bands()" + ] + }, + { + "cell_type": "markdown", + "id": "402753eb-bb08-4e6c-9299-a021231010eb", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We then construct our custom tool,\n", + "using Mpltoolbox's [Rectangles tool](https://mpltoolbox.readthedocs.io/en/latest/rectangles.html),\n", + "and inheriting from Plopp's\n", + "[DrawingTool](https://scipp.github.io/plopp/about/generated/plopp.widgets.drawing.DrawingTool.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b3d6a70-8497-4a55-b3f4-0ad29d2b1f8c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from plopp.widgets.drawing import DrawingTool\n", + "from functools import partial\n", + "from mpltoolbox import Rectangles\n", + "\n", + "\n", + "def define_mask(da, rect_info):\n", + " \"\"\"\n", + " Function that creates a mask inside the area\n", + " covered by the rectangle.\n", + " \"\"\"\n", + " x = rect_info['x']\n", + " y = rect_info['y']\n", + " b = min(y['bottom'], y['top'])\n", + " t = max(y['bottom'], y['top'])\n", + " l = min(x['left'], x['right'])\n", + " r = max(x['left'], x['right'])\n", + "\n", + " xcoord = sc.midpoints(da.coords[x['dim']])\n", + " ycoord = sc.midpoints(da.coords[y['dim']])\n", + " return (xcoord >= l) & (xcoord <= r) & (ycoord >= b) & (ycoord <= t)\n", + "\n", + "\n", + "def _get_rect_info(artist, figure):\n", + " \"\"\"\n", + " Convert the raw rectangle info to a dict containing the dimensions of\n", + " each axis, and values with units.\n", + " \"\"\"\n", + " return lambda: {\n", + " 'x': {\n", + " 'dim': figure.canvas.dims['x'],\n", + " 'left': sc.scalar(artist.xy[0], unit=figure.canvas.units['x']),\n", + " 'right': sc.scalar(\n", + " artist.xy[0] + artist.width, unit=figure.canvas.units['x']\n", + " ),\n", + " },\n", + " 'y': {\n", + " 'dim': figure.canvas.dims['y'],\n", + " 'bottom': sc.scalar(artist.xy[1], unit=figure.canvas.units['y']),\n", + " 'top': sc.scalar(\n", + " artist.xy[1] + artist.height, unit=figure.canvas.units['y']\n", + " ),\n", + " },\n", + " }\n", + "\n", + "\n", + "RectangleTool = partial(\n", + " DrawingTool, tool=Rectangles, get_artist_info=_get_rect_info, icon='vector-square'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "e4671098-831c-4272-8174-725d006266e5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Finally, we create our visualization interface with the figure,\n", + "adding our new tool to the toolbar." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b58820e-5108-4c44-8746-240bde11b09f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from plopp.widgets import Box\n", + "\n", + "data_node = pp.Node(da)\n", + "\n", + "\n", + "def apply_masks(da, *masks):\n", + " out = da.copy(deep=False)\n", + " for i, mask in enumerate(masks):\n", + " out.masks[str(i)] = mask\n", + " return out\n", + "\n", + "\n", + "masking_node = pp.Node(apply_masks, data_node)\n", + "\n", + "fig = pp.figure2d(masking_node, norm='log')\n", + "\n", + "r = RectangleTool(\n", + " figure=fig, input_node=data_node, func=define_mask, destination=masking_node\n", + ")\n", + "fig.toolbar['roi'] = r" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcf38064-eafe-407e-818c-e3516bd33195", + "metadata": { + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "r.value = True\n", + "\n", + "r._tool.click(50, 200)\n", + "r._tool.click(200, 250)\n", + "r._tool.click(30, 50)\n", + "r._tool.click(250, 170)\n", + "\n", + "from ipywidgets import HBox\n", + "\n", + "fig.children = [\n", + " fig.top_bar,\n", + " HBox([fig.left_bar, fig.canvas.to_image(), fig.right_bar]),\n", + " fig.bottom_bar,\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84335607-9ff0-4291-84ec-d062965958e4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "fig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dd7b431-2c33-4d66-8360-45b7a137f05f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "pp.show_graph(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "651d7f28-1147-49de-861e-feb825c87d08", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "To retrieve the masked data array,\n", + "simply call the node that is applying the masks:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac24d509-142d-45d9-89eb-f3481ab08dc2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "masking_node()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad0bf947-2429-403f-b255-8b838842ab83", + "metadata": { + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# This cell is used to generate the thumbnail for the docs gallery.\n", + "# It is hidden from the online documentation.\n", + "fig.save('../_static/gallery/interactive-masking-thumbnail.png')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/gallery/masking-a-range.ipynb b/docs/gallery/masking-a-range.ipynb index a3b1166b..8726a04d 100644 --- a/docs/gallery/masking-a-range.ipynb +++ b/docs/gallery/masking-a-range.ipynb @@ -3,7 +3,13 @@ { "cell_type": "markdown", "id": "61e0babe-849c-4fac-a0ea-2fd82d7db326", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "# Masking a range\n", "\n", @@ -16,7 +22,13 @@ "cell_type": "code", "execution_count": null, "id": "fe12dbfc-8ca7-4537-b185-3fe57db59d09", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "%matplotlib widget\n", @@ -29,7 +41,13 @@ { "cell_type": "markdown", "id": "f99ea04f-3b3b-49df-b4a2-99aa9bd8090a", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "We first generate some data that contains three bands of peaks that all have different spreads." ] @@ -38,40 +56,30 @@ "cell_type": "code", "execution_count": null, "id": "9d367a8e-9587-4b46-bb74-9e4c8eaa60ef", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ - "npeaks = 200\n", - "per_peak = 500\n", - "spread = 30.0\n", - "ny = 300\n", - "nx = 300\n", - "a = np.ones((ny, nx))\n", + "from plopp.data.examples import three_bands\n", "\n", - "xbins = np.arange(nx + 1)\n", - "ybins = np.arange(ny + 1)\n", - "for n in range(npeaks):\n", - " xc = np.random.random() * nx\n", - " yc = np.random.choice([ny / 4, ny / 2, 3 * ny / 4])\n", - " xy = np.random.normal(\n", - " loc=(xc, yc), scale=spread * np.random.random(), size=[per_peak, 2]\n", - " )\n", - " h, ye, xe = np.histogram2d(xy[:, 1], xy[:, 0], bins=(ybins, xbins))\n", - " a += h\n", - "\n", - "da = sc.DataArray(\n", - " data=sc.array(dims=['y', 'x'], values=a, unit='counts'),\n", - " coords={\n", - " 'x': sc.array(dims=['x'], values=xbins, unit='cm'),\n", - " 'y': sc.array(dims=['y'], values=ybins, unit='cm'),\n", - " },\n", - ")" + "da = three_bands()" ] }, { "cell_type": "markdown", "id": "0e5010d1-1710-476e-91c0-a01c281bfcf6", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "source": [ "We then construct our interface with a slider, a node that adds a mask, and a node that sums the unmasked data along the `y` dimension." ] @@ -80,7 +88,13 @@ "cell_type": "code", "execution_count": null, "id": "346e8cc8-3874-464e-ad49-dcfb5ad92770", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "ydim = 'y'\n", @@ -119,7 +133,13 @@ "cell_type": "code", "execution_count": null, "id": "7201be42-c726-43cd-b6ad-65cf8f2941b3", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "pp.show_graph(fig1d)" @@ -130,7 +150,12 @@ "execution_count": null, "id": "37373128-b948-4100-a761-87229730c62a", "metadata": { - "nbsphinx": "hidden" + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] }, "outputs": [], "source": [ diff --git a/docs/gallery/peeling-layers.ipynb b/docs/gallery/peeling-layers.ipynb new file mode 100644 index 00000000..23974dab --- /dev/null +++ b/docs/gallery/peeling-layers.ipynb @@ -0,0 +1,137 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e769d76c-baa5-4168-aa8c-2a08c8a21442", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "# Peeling off the layers\n", + "\n", + "This example uses thresholds to select two different ranges of values in the data,\n", + "and display them on the same scatter plot using a lower opacity for the outer layer,\n", + "revealing the inside of the clusters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1dab369-6b1d-43c5-86ab-3805136378fc", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import plopp as pp\n", + "import scipp as sc\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "78466f8a-ad5a-49d0-84f2-4c997ed7b4ff", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We first generate some fake data, meant to represent clusters of points in a three-dimensional space.\n", + "\n", + "The data values scale with $1/r^{2}$ where $r$ is the distance to the center of each cluster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "150b3e02-592c-4f13-95cc-354458dd6deb", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from plopp.data.examples import clusters3d\n", + "\n", + "da = clusters3d()" + ] + }, + { + "cell_type": "markdown", + "id": "d054e1a6-e503-43e8-9a9b-2ec73c5ef6e5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We select two data ranges (high values and low values),\n", + "and display both at the same time,\n", + "lowering the opacity of the low-value data range." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7103a291-caeb-4a6c-9676-7c61e8516e3d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# Select ranges\n", + "a = da[da.data > sc.scalar(0.05)]\n", + "b = da[(da.data > sc.scalar(0.002)) & (da.data < sc.scalar(0.005))]\n", + "\n", + "# Display both on the same scatter plot\n", + "p = pp.scatter3d({'a': a, 'b': b}, pos='position', norm='log')\n", + "\n", + "# Extract the point clouds from the final plot and set a lower opacity on the second point cloud\n", + "clouds = list(p[0].artists.values())\n", + "clouds[1].opacity = 0.1\n", + "\n", + "p" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/gallery/rectangle-selection.ipynb b/docs/gallery/rectangle-selection.ipynb index 1fb8b9b3..2d3fc646 100644 --- a/docs/gallery/rectangle-selection.ipynb +++ b/docs/gallery/rectangle-selection.ipynb @@ -7,7 +7,7 @@ "source": [ "# Rectangle selection\n", "\n", - "In this example, we will use a custom drawing tool to draw rectangle on a 2D figure.\n", + "In this example, we will use a custom drawing tool to draw rectangles on a 2D figure.\n", "The data inside the rectangles will be summed along the vertical dimension,\n", "and displayed on a one-dimensional plot below the image." ] @@ -16,14 +16,19 @@ "cell_type": "code", "execution_count": null, "id": "58d40a92-d9a9-47e9-9c2e-151d734c48df", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "%matplotlib widget\n", "import plopp as pp\n", "import scipp as sc\n", - "import numpy as np\n", - "import mpltoolbox as tbx" + "import numpy as np" ] }, { @@ -38,34 +43,18 @@ "cell_type": "code", "execution_count": null, "id": "3c05fe5b-483a-438e-a7ba-a64b24a33b61", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ - "npeaks = 200\n", - "per_peak = 500\n", - "spread = 30.0\n", - "ny = 300\n", - "nx = 300\n", - "a = np.ones((ny, nx))\n", + "from plopp.data.examples import three_bands\n", "\n", - "xbins = np.arange(nx + 1)\n", - "ybins = np.arange(ny + 1)\n", - "for n in range(npeaks):\n", - " xc = np.random.random() * nx\n", - " yc = np.random.choice([ny / 4, ny / 2, 3 * ny / 4])\n", - " xy = np.random.normal(\n", - " loc=(xc, yc), scale=spread * np.random.random(), size=[per_peak, 2]\n", - " )\n", - " h, ye, xe = np.histogram2d(xy[:, 1], xy[:, 0], bins=(ybins, xbins))\n", - " a += h\n", - "\n", - "da = sc.DataArray(\n", - " data=sc.array(dims=['y', 'x'], values=a, unit='counts'),\n", - " coords={\n", - " 'x': sc.array(dims=['x'], values=xbins, unit='cm'),\n", - " 'y': sc.array(dims=['y'], values=ybins, unit='cm'),\n", - " },\n", - ")" + "da = three_bands()" ] }, { @@ -147,7 +136,13 @@ "cell_type": "code", "execution_count": null, "id": "6acd8916-8ba8-4065-9cbe-5d76a74ce281", - "metadata": {}, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, "outputs": [], "source": [ "from plopp.widgets import Box\n", @@ -220,7 +215,12 @@ "execution_count": null, "id": "01b502c8-d10c-45ff-8447-a9fd7121781f", "metadata": { - "nbsphinx": "hidden" + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] }, "outputs": [], "source": [ diff --git a/docs/gallery/scatter3d-with-threshold.ipynb b/docs/gallery/scatter3d-with-threshold.ipynb index 4e4eabbd..0f8a2363 100644 --- a/docs/gallery/scatter3d-with-threshold.ipynb +++ b/docs/gallery/scatter3d-with-threshold.ipynb @@ -105,8 +105,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.13" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/src/plopp/data/examples.py b/src/plopp/data/examples.py index 83a99ef5..47f8bd35 100644 --- a/src/plopp/data/examples.py +++ b/src/plopp/data/examples.py @@ -3,6 +3,9 @@ from functools import lru_cache +import numpy as np +import scipp as sc + _version = '1' @@ -44,3 +47,71 @@ def nyc_taxi() -> str: This data has been manipulated! """ return get_path('nyc_taxi_data.h5') + + +def three_bands(npeaks=200, per_peak=500, spread=30.0): + """ + Generate a 2D dataset with three bands of peaks. + + Parameters + ---------- + npeaks: + Number of peaks. + per_peak: + Number of points per peak. + spread: + Standard deviation (spread or 'width') of the peaks. + """ + ny = 300 + nx = 300 + rng = np.random.default_rng() + shape = (npeaks, per_peak) + x = np.empty(shape) + y = np.empty(shape) + xcenters = rng.uniform(0, nx, size=npeaks) + ycenters = rng.choice([ny / 4, ny / 2, 3 * ny / 4], size=npeaks) + spreads = rng.uniform(0, spread, size=npeaks) + for i, (xc, yc, sp) in enumerate(zip(xcenters, ycenters, spreads)): + xy = np.random.normal(loc=(xc, yc), scale=sp, size=[per_peak, 2]) + x[i, :] = xy[:, 0] + y[i, :] = xy[:, 1] + + xcoord = sc.array(dims=['row'], values=x.ravel(), unit='cm') + ycoord = sc.array(dims=['row'], values=y.ravel(), unit='cm') + table = sc.DataArray( + data=sc.ones(sizes=xcoord.sizes, unit='counts'), + coords={'x': xcoord, 'y': ycoord}, + ) + return table.hist(y=300, x=300) + sc.scalar(1.0, unit='counts') + + +def clusters3d(nclusters=100, npercluster=2000): + """ + Generate a 3D dataset with clusters of points. + + Parameters + ---------- + nclusters: + Number of clusters. + npercluster: + Number of points per cluster. + """ + position = np.zeros((nclusters, npercluster, 3)) + values = np.zeros((nclusters, npercluster)) + + for n in range(nclusters): + center = 200.0 * (np.random.random(3) - 0.5) + r = 10.0 * np.random.normal(size=[npercluster, 3]) + position[n, :] = r + center + values[n, :] = 1 / np.linalg.norm(r, axis=1) ** 2 + + return sc.DataArray( + data=sc.array(dims=['row'], values=values.flatten()), + coords={ + 'position': sc.vectors( + dims=['row'], + unit='m', + values=position.reshape(nclusters * npercluster, 3), + ) + }, + )