Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ new `rcN` heading when that rc is published to TestPyPI.

## Unreleased

- Show4DSTEM compares native diffraction patterns side by side in live
Multiple view, with shared scan-region reductions and live detector dragging.
Playback controls are reserved for Single view.

- Add `Plot2D` for scalar maps with calibrated Cartesian axes, colormap
selection, viewport controls, and editable Matplotlib figure export.

Expand Down
18 changes: 18 additions & 0 deletions docs/api/show4dstem.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Show4DSTEM

## Compare diffraction patterns side by side

For a live notebook with several datasets, use
`Show4DSTEM(stack, view_mode="multiple", compare_dp_mode="all")`.
Each visible dataset shows its native diffraction pattern at the same scan
position. Circle, Square or Rect scan selection with Mean compares each
dataset over identical scan positions, never an average across datasets.
Dragging the detector in any diffraction tile updates the shared virtual
detector and all virtual images before release.

The diffraction grid shares the virtual-image grid's zoom, pan, reset,
scale bars and column layout. Playback is available in Single view only.
The `all` mode requires a live kernel; standalone export is not qualified for
this layout. Existing `selected` and `average` modes remain available.
Packed sources require their own reduction support and are not established
by this dense-array feature. See the
[interaction contract](../maintainer/all-diffraction-comparison.md).

Public import:

```python
Expand Down
43 changes: 43 additions & 0 deletions docs/maintainer/all-diffraction-comparison.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# All diffraction comparison (live)

Given a live Show4DSTEM with multiple datasets, when `compare_dp_mode="all"`
is selected, show each visible dataset's native diffraction pattern at the
same scan position. Keep signed values intact and use one absolute display
range across the grid. Scan selection updates every pattern. Clicking a grid
pattern moves the common detector center; the primary diffraction panel retains
the aperture geometry, radius, zoom and other tools.

The diffraction row uses the same comparison component as the virtual-image
row: wheel zoom, Shift-drag pan, Reset/double-click, scale bars, responsive
columns, panel selection, hide/star/reorder and resize. Diffraction scale bars
use detector pixels unless reciprocal-space calibration is provided. Color and
linear/signed-log settings follow the main diffraction controls. FFT/profile
tools remain in the primary diffraction panel, not each comparison tile.

Select Circle, Square or Rect in the scan ROI controls below the virtual
images, with Mean as the reduction. Each method is reduced independently over
identical scan positions. No averaging across methods occurs. Dragging updates
the shared outline on animation frames and requests live native reductions.
One kernel request is in flight; intermediate positions coalesce to the latest
pointer position. A `vi_roi_receipt` acknowledges each completed reduction,
including unchanged patterns, before the next pending position is sent. The
final pointer position is retained. Off restores the point patterns. This does
not establish 120 FPS derived-data updates: measure actual five-panel paints
separately from animation-frame cadence and backend execution. The receipt
contains row, col, backend wall milliseconds and revision; it is not a GPU-only
timer or proof that the browser painted the result. Native arrays remain signed and
unchanged. An empty selection must not be presented as a valid mean.

This additive layout reuses QuantEM.GPU's WebGPU range/colormap/compositing
engine. It does not apply denoising or change the original data. The existing
average/selected modes are unchanged. Python transfers only the requested
native patterns, not a second 4D cube. The all-pattern grid is live-kernel-only
at this stage; kernel-free exports, lazy-page stress and physical-phone behavior
require separate qualification. Do not claim these from a live dense-cube test.

Example:

```python
Show4DSTEM(stack, view_mode="multiple", compare_dp_mode="all",
frame_labels=["Noisy", "SHINE", "Fourier", "Sadri", "Fusion"])
```
16 changes: 16 additions & 0 deletions docs/maintainer/storyboard-show4dstem.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Show4DSTEM Storyboard

### S4D-21: Compare Native Patterns With Shared Live Controls

- Open a live multiple-dataset viewer with `compare_dp_mode="all"`.
- Select a scan point and confirm that every diffraction tile follows it.
- Select Circle/Mean in scan space; drag while holding the pointer and confirm
that all patterns update over the same region before release.
- Drag in each diffraction tile and confirm that the common virtual detector
and every virtual image update before release. Repeat with Point and Annular.
- Change columns, zoom, Shift-drag pan and reset; retain calibrated or pixel
scale bars and shared contrast. FFT/profile remain primary-panel tools.
- Switch Single to Multiple during playback. Playback must stop and its controls
disappear; returning to Single must remain paused.
- Check signed-array preservation independently of the display transform.
Do not count these live dense-array checks as packed, portable HTML, physical
phone or 120-FPS qualification.

Use with [Storyboard](storyboard).

MacBook support is a first-class Show4DSTEM target, not an afterthought. For
Expand Down
58 changes: 58 additions & 0 deletions js/show4dstem/AllDiffractionGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from "react";
import { GPUColormapEngine } from "../colormaps";
import { extractBytes } from "../format";
import { getGPUDevice, isSoftwareGPUAdapter } from "../.generated/engine/device/webgpu";

type DiffractionSlots = {
engine: GPUColormapEngine;
slots: Map<number, number>;
ranges: Map<number, {min: number; max: number}>;
};

/** Upload native patterns once; the common grid owns every interaction. */
export function AllDiffractionGrid({bytes, indices, rows, cols, selectionLabel, renderGrid}: {
bytes: DataView | undefined; indices: number[]; rows: number; cols: number;
selectionLabel: string; renderGrid: (data: DiffractionSlots) => React.ReactNode;
}) {
const [engine, setEngine] = React.useState<GPUColormapEngine | null>(null);
const [data, setData] = React.useState<DiffractionSlots | null>(null);
const [error, setError] = React.useState("");
React.useEffect(() => {
let disposed = false;
let renderer: GPUColormapEngine | null = null;
void (async () => {
try {
const device = await getGPUDevice();
if (!device || isSoftwareGPUAdapter()) throw new Error("Hardware WebGPU is required for the all-pattern display.");
renderer = new GPUColormapEngine(device);
if (disposed) renderer.destroy(); else setEngine(renderer);
} catch (cause) { if (!disposed) setError(String(cause)); }
})();
return () => { disposed = true; renderer?.destroy(); };
}, []);
React.useEffect(() => {
if (!engine || !bytes || !indices.length) return;
const payload = extractBytes(bytes);
const length = rows * cols;
if (payload.byteLength !== indices.length * length * 4) return;
let cancelled = false;
const values = new Float32Array(payload.buffer, payload.byteOffset, payload.byteLength / 4);
const slots = new Map(indices.map((frame, index) => [frame, index]));
indices.forEach((_, index) => engine.uploadData(index, values.subarray(index * length, (index + 1) * length), cols, rows));
void (async () => {
try {
const ranges = await engine.computeRangeBatch([...slots.values()]);
if (cancelled) return;
const range = {min: Math.min(...ranges.map(r => r.min)), max: Math.max(...ranges.map(r => r.max))};
setData({engine, slots, ranges: new Map(indices.map(frame => [frame, range]))});
} catch (cause) { if (!cancelled) setError(String(cause)); }
})();
return () => { cancelled = true; };
}, [engine, bytes, indices, rows, cols]);
return <section aria-label="All diffraction patterns" style={{width:"100%"}}>
<div style={{fontSize:11, marginBottom:4}}>Diffraction patterns · {selectionLabel} · shared contrast</div>
{error && <div role="alert">{error}</div>}
{!indices.length && <div>No scan positions selected.</div>}
{indices.length > 0 && data && renderGrid(data)}
</section>;
}
Loading
Loading