diff --git a/docs/_toc.yml b/docs/_toc.yml index a2e53e8d..526b509d 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -16,6 +16,11 @@ parts: chapters: - file: tutorials/download_data - file: tutorials/show4dstem + sections: + - file: tutorials/show4dstem_single + title: Open one 4D-STEM dataset + - file: tutorials/show4dstem_multiple + title: Compare datasets or tilts - file: tutorials/showptycho - file: tutorials/show2d - file: tutorials/show3d diff --git a/docs/tutorials/show4dstem.ipynb b/docs/tutorials/show4dstem.ipynb index 7d2d2f04..3e17b2db 100644 --- a/docs/tutorials/show4dstem.ipynb +++ b/docs/tutorials/show4dstem.ipynb @@ -9,7 +9,7 @@ "\n", "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/electronmicroscopy/quantem.widget/blob/main/docs/tutorials/show4dstem.ipynb)\n", "\n", - "`Show4DSTEM` opens a 4D-STEM dataset with live virtual detectors: a bright-field / annular-dark-field aperture over the diffraction stack on one side, the resulting virtual image on the other. It accepts a NumPy array, a PyTorch tensor, a quantem `Dataset4dstem`, or the output of `load(...)`.\n", + "`Show4DSTEM` opens 4D-STEM data with live virtual detectors: a bright-field / annular-dark-field aperture over the diffraction stack on one side, the resulting virtual image on the other. Start with [one master file](show4dstem_single) or [several compatible datasets or tilts](show4dstem_multiple), then return here for the interactive public-data examples.\n", "\n", "This tutorial uses the public binned gold 4D-STEM dataset from [`bobleesj/quantem-data`](https://huggingface.co/datasets/bobleesj/quantem-data). `show4dstem_gold(size=\"medium\")` returns a calibrated real-data preview by striding the `gold_128_npy_bin8` scan to 64 by 64 positions with 24 by 24 detector frames, so the rendered documentation opens quickly while preserving uint16 detector counts. Use `load_tutorial_show4dstem(scan_stride=1)` for the full 128 by 128 scan.\n", "\n", @@ -35,7 +35,7 @@ "id": "e56b079d", "metadata": {}, "outputs": [], - "source": "import numpy as np\n\nfrom quantem.widget import Show4DSTEM\nfrom quantem.widget.datasets import show4dstem_gold\n\nstem_dataset = show4dstem_gold(size=\"medium\")\n" + "source": "from quantem.widget import Show4DSTEM\nfrom quantem.widget.datasets import show4dstem_gold\n\nstem_dataset = show4dstem_gold(size=\"medium\")\n" }, { "cell_type": "markdown", @@ -60,28 +60,10 @@ "id": "2f0efcff", "metadata": {}, "source": [ - "## Multi-panel screening\n", + "## Your data\n", "\n", - "Use `view_mode=\"multiple\"` when the extra frame axis represents datasets, time points, scan regions, or acquisition repeats that should share one detector ROI. The diffraction panel stays the familiar Show4DSTEM control surface; the virtual-image side becomes a grid. Click a tile to make it the selected dataset, keep `compare_dp_mode=\"selected\"` for tilt-series or dataset review, switch to `\"average\"` only when the mean diffraction pattern across the visible grid is the quantity you want, and use the tile star/hide/reorder controls to curate the set for a later cell or export.\n", - "\n", - "`compare_max_panels` is the visible page size. If there are more datasets than that, the multiple grid gets a Group control like Show2D/Show3D galleries. Keep `compare_group_mode=\"paged\"` for page-by-page lazy loading, or switch to `compare_group_mode=\"all\"` when you want one dense overview of all reduced virtual-image panels. All mode still computes lazy folder data in page-sized batches instead of keeping every raw 4D master resident.\n", - "\n", - "The small example below splits the same real gold scan into eight calibrated regions so the tutorial remains light, but the API is the same for a folder with many real master files.\n" + "For microscope data, continue with [one master file](show4dstem_single) or [several compatible datasets or tilts](show4dstem_multiple). Both guides use the same short `load(...)` then `Show4DSTEM(data)` workflow. For sharing, see [Show4DSTEM export](show4dstem_export).\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16fbc7b5", - "metadata": {}, - "outputs": [], - "source": "gold = stem_dataset.array\nrow_edges = np.linspace(0, gold.shape[0], 3, dtype=int)\ncol_edges = np.linspace(0, gold.shape[1], 5, dtype=int)\n\nregion_stack = np.stack(\n [\n gold[row_edges[row]:row_edges[row + 1], col_edges[col]:col_edges[col + 1]]\n for row in range(2)\n for col in range(4)\n ],\n axis=0,\n).astype(gold.dtype, copy=False)\n\nregion_labels = [f\"region {row + 1}-{col + 1}\" for row in range(2) for col in range(4)]\n\nmulti = Show4DSTEM(\n region_stack,\n sampling=tuple(stem_dataset.sampling),\n units=tuple(stem_dataset.units),\n view_mode=\"multiple\",\n compare_cols=4,\n compare_max_panels=4,\n compare_panel_gap_px=0,\n compare_dp_mode=\"selected\",\n frame_dim_label=\"Region\",\n frame_labels=region_labels,\n dp_scale_mode=\"linear\",\n offline=True,\n offline_dtype=\"uint16\",\n save_state=True,\n precompute_virtual_images=False,\n show_fft=False,\n)\nmulti\n" - }, - { - "cell_type": "markdown", - "id": "0e9b8542", - "metadata": {}, - "source": "## Real master-file folders on a workstation\n\nFor large no-bin or multi-acquisition sessions, open completed `*_master.h5` files directly from a folder and keep the comparison grid dense. `Show4DSTEM.from_folder(...)` paints the initial page first, then preloads every unhidden dataset across the selected GPUs when the exact shape/dtype footprint fits. Larger series stay full-resolution and lazy; the automatic policy never silently bins or narrows data. `page_size` controls how many datasets are shown and computed together, while `columns` controls the grid width.\n\nPass `sampling` and `units` when the file metadata does not contain physical scan or reciprocal-space calibration. Use pixel units when calibration is unknown rather than inventing mrad or angstrom values.\n\n```python\nfrom quantem.widget import Show4DSTEM\n\nviewer = Show4DSTEM.from_folder(\n \"/data/session\",\n gpus=[0, 1],\n det_bin=1,\n columns=5,\n page_size=5,\n preload_all_if_fits=True,\n compare_dp_mode=\"selected\",\n warm_cache=True,\n watch=True,\n)\nviewer\n\n# Optional programmatic paging. Page numbers are zero-based.\nviewer.set_compare_page(1)\nviewer.show_compare_all_groups() # dense overview of every visible panel\nviewer.show_compare_paged_groups() # return to page-by-page browsing\nviewer.preload_all_datasets() # re-check after GPU memory changes\nviewer.wait_for_dataset_preload(timeout=120) # optional deterministic wait\nviewer.compare_page_idx, viewer.compare_page_count\n```\n\nUse `compare_dp_mode=\"selected\"` when clicking a panel should show that dataset's diffraction pattern. Switch to `compare_dp_mode=\"average\"` only when the shared panel should show the average diffraction pattern for the visible grid. The panel order, hidden panels, starred panels, and current page are stored on the widget and can be reused with `state_dict()` / `load_state_dict()`.\n\nWhen sharing the result, choose the export type based on what the recipient needs. `export_kind=\"report\"` writes a compact static HTML report with PNG virtual-image pages and no raw 4D payload, which is the safe option for lazy folders and many datasets. `export_kind=\"interactive\"` embeds the binned raw 4D data so the exported page can still be driven like a widget, but the file can be much larger. The GUI HTML menu shows report options first, then a size-sorted ladder of interactive raw-4D presets.\n\n```python\n# Compact page-aware report for colleagues.\nviewer.export_html(\n \"show4dstem_report.html\",\n export_kind=\"report\",\n dataset_scope=\"unhidden\", # or \"current_page\", \"starred\", \"all\"\n scan_bin=2, # real-space mean bin for smaller report PNGs\n det_bin=8, # detector mean bin for the representative DP\n dtype=\"uint8\",\n)\n\n# Raw offline widget when the recipient needs interactive 4D data.\nviewer.export_html(\n \"show4dstem_interactive.html\",\n export_kind=\"interactive\",\n scan_bin=2,\n det_bin=4,\n dtype=\"uint8\",\n)\n```\n\nThe exported folder opens without Python on the receiving machine: double-click the bundled `Show4DSTEM.command` launcher (macOS — it starts a local server and opens Chrome), or open `index.html` and grant it the data folder when Chrome asks. Full recipes on [Show4DSTEM export](show4dstem_export).\n" } ], "metadata": { diff --git a/docs/tutorials/show4dstem_multiple.md b/docs/tutorials/show4dstem_multiple.md new file mode 100644 index 00000000..8629d24e --- /dev/null +++ b/docs/tutorials/show4dstem_multiple.md @@ -0,0 +1,93 @@ +# Compare several 4D-STEM datasets or tilts + +Use this workflow for a tilt series, repeated acquisition, dose series, or +other set of compatible `*_master.h5` files. One shared detector controls every +virtual-image panel, while the selected dataset supplies the diffraction +pattern. + +The masters must have matching scan shape, detector shape, and frame count. +List them explicitly when order matters, such as a known tilt-angle sequence. + +## Jupyter notebook + +```python +from quantem.gpu.io import load +from quantem.widget import Show4DSTEM + +masters = [ + "/data/tilts/sample_m6deg_master.h5", + "/data/tilts/sample_m4deg_master.h5", + "/data/tilts/sample_m2deg_master.h5", + "/data/tilts/sample_0deg_master.h5", + "/data/tilts/sample_p2deg_master.h5", + "/data/tilts/sample_p4deg_master.h5", + "/data/tilts/sample_p6deg_master.h5", +] + +data = load(masters) +viewer = Show4DSTEM(data) +viewer +``` + +That is the complete beginner call. Native detector sampling and the source +count dtype are preserved. `Show4DSTEM` detects the extra dataset axis, opens +the Multiple view, uses the filenames as labels, and shows the selected +dataset's diffraction pattern. Put the sample name and tilt angle in each +filename so the viewer labels remain meaningful. + +The first usable panel should appear before the complete series finishes +loading. Each later panel fills its reserved position as that master becomes +resident. You can begin dragging the detector on the loaded panels while the +remaining masters continue loading. + +Use **Selected** for ordinary tilt review: clicking a virtual-image tile makes +its dataset the source of the diffraction pattern. Use **Average** only when +the mean diffraction pattern across the loaded visible datasets is the +scientific quantity you intend to inspect. + +## Local WebGPU viewer + +List the masters in the desired order: + +```bash +quantem show4dstem \ + /data/tilts/sample_m6deg_master.h5 \ + /data/tilts/sample_m4deg_master.h5 \ + /data/tilts/sample_m2deg_master.h5 \ + /data/tilts/sample_0deg_master.h5 \ + /data/tilts/sample_p2deg_master.h5 \ + /data/tilts/sample_p4deg_master.h5 \ + /data/tilts/sample_p6deg_master.h5 \ + --backend webgpu --html +``` + +If a folder contains only the compatible masters you want to compare, the +short form is: + +```bash +quantem show4dstem /data/tilts --backend webgpu --html +``` + +Both commands keep native detector sampling and use the compact browser browse +dtype. Add `--dtype uint16` only when the browser view must preserve counts +above 255. + +The generated viewer opens in Multiple mode and loads datasets progressively. +On macOS, double-click `Show4DSTEM.command` and keep its Terminal window open. +The source HDF5 files remain local; WebGPU interaction runs in the browser. + +## What to verify + +1. Panel labels match the intended sample and tilt order. +2. Each virtual image appears as its dataset loads; the grid does not wait for + the final master before becoming useful. +3. Clicking a tile changes the selected diffraction pattern. +4. Dragging the detector updates every loaded virtual-image panel immediately. +5. Average produces a diffraction pattern from the loaded visible datasets and + does not remain in a requested/loading state. + +## Next steps + +- [Open one 4D-STEM dataset](show4dstem_single) +- [Show4DSTEM export recipes](show4dstem_export) +- [Show4DSTEM API reference](../api/show4dstem) diff --git a/docs/tutorials/show4dstem_single.md b/docs/tutorials/show4dstem_single.md new file mode 100644 index 00000000..7c42d058 --- /dev/null +++ b/docs/tutorials/show4dstem_single.md @@ -0,0 +1,58 @@ +# Open one 4D-STEM dataset + +Use this workflow when you want to inspect one completed `*_master.h5` file: +move through scan positions, drag a virtual detector, and compare BF, ABF, and +ADF images without reducing the detector grid. + +## Jupyter notebook + +Load the master with the public GPU loader, then let Jupyter render the viewer +as the last expression: + +```python +from quantem.gpu.io import load +from quantem.widget import Show4DSTEM + +data = load("/data/session/scan_001_master.h5") +viewer = Show4DSTEM(data) +viewer +``` + +The default keeps native detector sampling and the source count dtype. The +loader selects CUDA on an NVIDIA workstation or Metal/MPS on Apple Silicon. + +After the widget appears: + +1. Drag over the real-space image to choose a scan position and inspect its + diffraction pattern. +2. Drag or resize the detector on the diffraction pattern. The virtual image + updates while you drag. +3. Try BF, ABF, and ADF presets, then adjust diffraction and virtual-image + contrast independently. + +## Local WebGPU viewer + +Use the CLI when you want the same dataset in a local browser without keeping +a notebook kernel alive: + +```bash +quantem show4dstem /data/session/scan_001_master.h5 --backend webgpu --html +``` + +This keeps native detector sampling and uses the compact browser browse dtype. +Add `--dtype uint16` only when the browser view must preserve counts above 255. + +The command creates a folder containing `index.html`, `Show4DSTEM.command`, a +nested `.viewer/`, and a nested `data/` directory linked to the source HDF5 +family. On macOS, double-click `Show4DSTEM.command`. Keep its Terminal window +open while using the viewer; closing it stops the local server. + +The browser fetches diffraction frames as needed and computes the virtual image +with WebGPU. A loading message means data are moving from the local HDF5 files +into the browser; it is not an upload to a remote service. + +## Next steps + +- [Compare several datasets or tilts](show4dstem_multiple) +- [Show4DSTEM export recipes](show4dstem_export) +- [Show4DSTEM API reference](../api/show4dstem) diff --git a/src/quantem/widget/show4dstem_factory.py b/src/quantem/widget/show4dstem_factory.py index df470577..3ceff229 100644 --- a/src/quantem/widget/show4dstem_factory.py +++ b/src/quantem/widget/show4dstem_factory.py @@ -59,12 +59,19 @@ def _build_mps_viewer(data: Any, **kwargs: Any) -> Any: return _show4dstem_mps(data, **kwargs) -def _apply_loadresult_labels(data: Any, payload: Any, kwargs: dict[str, Any]) -> None: - """Label a CUDA 5D ``load([...])`` stack as datasets.""" - if not isinstance(data, LoadResult) or getattr(payload, "ndim", 0) != 5: +def _apply_loadresult_defaults(data: Any, payload: Any, kwargs: dict[str, Any]) -> None: + """Give a loaded multi-dataset stack its natural comparison view.""" + shape = getattr(payload, "shape", ()) + try: + is_multi_dataset = int(getattr(payload, "ndim", len(shape))) == 5 + except (TypeError, ValueError): + is_multi_dataset = False + if not isinstance(data, LoadResult) or not is_multi_dataset: return meta = getattr(data, "metadata", {}) or {} kwargs.setdefault("frame_dim_label", "Dataset") + kwargs.setdefault("view_mode", "multiple") + kwargs.setdefault("compare_dp_mode", "selected") names = meta.get("file_names") if names is not None: kwargs.setdefault("frame_labels", list(names)) @@ -79,13 +86,7 @@ def Show4DSTEM(data: Any, **kwargs: Any) -> Any: from quantem.widget import Show4DSTEM Show4DSTEM(load("a.h5")) # auto: CUDA / MPS - Show4DSTEM(load("a.h5", backend="mps")) # explicit Apple Metal load - Show4DSTEM(load(["a.h5", "b.h5"], det_bin=4)) # many datasets, one slider - Show4DSTEM(load("a.h5"), backend="webgpu") # browser WebGPU compute - - w = Show4DSTEM(load("a.h5"), backend="webgpu", offline_codec="bslz4", - data_url="show4dstem-data") - w.export_html("show4dstem.html") + Show4DSTEM(load(["a.h5", "b.h5"])) # automatic comparison Dispatch is automatic from what ``load`` returns: - Apple Silicon MPS single-file loads use the raw-Metal real-time viewer. @@ -96,10 +97,10 @@ def Show4DSTEM(data: Any, **kwargs: Any) -> Any: - Browser WebGPU performs detector reductions in the browser. """ payload = _payload(data) + _apply_loadresult_defaults(data, payload, kwargs) if is_mps_show4dstem_payload(payload): return _build_mps_viewer(payload, **kwargs) - _apply_loadresult_labels(data, payload, kwargs) return _Show4DSTEMBase(payload, **kwargs) diff --git a/tests/show4dstem/test_factory.py b/tests/show4dstem/test_factory.py index 7834126f..0c40f702 100644 --- a/tests/show4dstem/test_factory.py +++ b/tests/show4dstem/test_factory.py @@ -135,6 +135,29 @@ def _fake_mps_builder(data, **kwargs): assert calls == [(payload, {"verbose": False})] +def test_show4dstem_opens_mps_5d_loadresult_as_dataset_comparison(monkeypatch) -> None: + payload = SimpleNamespace( + chunks=[object()], + shape=(2, 4, 4, 8, 8), + metadata={"scan_shape": (4, 4)}, + ) + load_result = LoadResult(payload, {"file_names": ["-2 deg", "+2 deg"]}) + + def _fake_mps_builder(data, **kwargs): + return {"data": data, "kwargs": kwargs} + + monkeypatch.setattr(factory, "_build_mps_viewer", _fake_mps_builder) + + result = factory.Show4DSTEM(load_result) + + assert result["kwargs"] == { + "frame_dim_label": "Dataset", + "frame_labels": ["-2 deg", "+2 deg"], + "view_mode": "multiple", + "compare_dp_mode": "selected", + } + + def test_show4dstem_routes_mps_gpu_frame_proxy_to_mps_builder(monkeypatch) -> None: payload = SimpleNamespace(_is_gpu_frames=True, device="mps:0") @@ -182,7 +205,7 @@ def _fake_base(data, **kwargs): assert factory.show4dstem_backend_kind(payload) == "base" -def test_show4dstem_labels_5d_loadresult_as_dataset_stack(monkeypatch) -> None: +def test_show4dstem_opens_5d_loadresult_as_dataset_comparison(monkeypatch) -> None: payload = SimpleNamespace(ndim=5) load_result = LoadResult(payload, {"file_names": ("first.h5", "second.h5")}) @@ -196,9 +219,61 @@ def _fake_base(data, **kwargs): assert result["data"] is payload assert result["kwargs"]["frame_dim_label"] == "Dataset" assert result["kwargs"]["frame_labels"] == ["first.h5", "second.h5"] + assert result["kwargs"]["view_mode"] == "multiple" + assert result["kwargs"]["compare_dp_mode"] == "selected" assert result["kwargs"]["verbose"] is False +def test_show4dstem_preserves_explicit_5d_view_options(monkeypatch) -> None: + payload = SimpleNamespace(ndim=5) + load_result = LoadResult(payload, {"file_names": ("first.h5", "second.h5")}) + + def _fake_base(data, **kwargs): + return {"data": data, "kwargs": kwargs} + + monkeypatch.setattr(factory, "_Show4DSTEMBase", _fake_base) + + result = factory.Show4DSTEM( + load_result, + view_mode="single", + compare_dp_mode="average", + ) + + assert result["kwargs"]["view_mode"] == "single" + assert result["kwargs"]["compare_dp_mode"] == "average" + + +def test_simple_5d_loadresult_keeps_selected_and_average_dp_working() -> None: + data = np.zeros((2, 2, 2, 6, 6), dtype=np.uint16) + data[0, :, :, 1:3, 1:3] = 8 + data[1, :, :, 3:5, 3:5] = 24 + loaded = LoadResult(data, {"file_names": ("tilt -2 deg", "tilt +2 deg")}) + + widget = factory.Show4DSTEM( + loaded, + precompute_virtual_images=False, + verbose=False, + ) + try: + assert widget.view_mode == "multiple" + assert widget.compare_dp_mode == "selected" + selected_first = widget.frame_bytes + + widget.frame_idx = 1 + selected_second = widget.frame_bytes + assert selected_second != selected_first + + widget.compare_dp_mode = "average" + average = widget.frame_bytes + assert average != selected_first + assert average != selected_second + + widget.compare_dp_mode = "selected" + assert widget.frame_bytes == selected_second + finally: + widget.close() + + def test_public_show4dstem_constructs_small_binned_numpy_viewer() -> None: data = np.arange(2 * 2 * 4 * 4, dtype=np.uint16).reshape(2, 2, 4, 4)