Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/widget-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ jobs:
wheel = wheels[0]
required = {
"quantem/widget/static/chooselattice.js",
"quantem/widget/static/showbragg.js",
"quantem/widget/static/show1d.js",
"quantem/widget/static/show2d.js",
"quantem/widget/static/show3d.js",
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ new `rcN` heading when that rc is published to TestPyPI.

## Unreleased

- Add `ShowBragg`, an interactive front end for the `quantem.diffraction`
Bragg-disk workflow: template building, per-position correlation probing,
disk detection tuned on a sampled subset then run over the full scan, a
Bragg vector map with numbered candidates, basis selection by candidate
index or direct placement, and a per-position lattice fit handed off to
`StrainMap`.
- Installation, Colab tutorial cells, and the verify snippet now install the
TestPyPI wheel and import `load` from `quantem.gpu.io`. `quantem
showdiffraction --demo` is documented as needing the public Fe3O4 hub folder,
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ for backend setup, Colab instructions, and verification.
| `ShowPtycho` | Interactive SSB phase and aberration review | [API](https://electronmicroscopy.github.io/quantem.widget/api/showptycho.html) |
| `ShowDiffraction` | Measure diffraction spots, rings, spacing, and angles | [tutorial](https://electronmicroscopy.github.io/quantem.widget/tutorials/showdiffraction.html) · [API](https://electronmicroscopy.github.io/quantem.widget/api/showdiffraction.html) |
| `ChooseLattice` | Select an origin and lattice vectors | [API](https://electronmicroscopy.github.io/quantem.widget/api/choose-lattice.html) |
| `ShowBragg` | Detect Bragg disks, pick a basis, and fit the lattice for strain | [API](https://electronmicroscopy.github.io/quantem.widget/api/showbragg.html) |
| `ShowEDS` | Explore linked EDS/EELS maps and spectra | — |
| `ShowFolder` | Browse, group, and select microscopy session files | [tutorial](https://electronmicroscopy.github.io/quantem.widget/tutorials/showfolder.html) · [API](https://electronmicroscopy.github.io/quantem.widget/api/showfolder.html) |

Expand Down
1 change: 1 addition & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ parts:
- file: api/showeds
- file: api/showdiffraction
- file: api/choose-lattice
- file: api/showbragg
- file: api/showfolder
- file: api/html-export
- file: api/io
Expand Down
1 change: 1 addition & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ readers and test agents.
| [ShowPtycho](showptycho) | `quantem.widget.ShowPtycho` | ptychography state JSON, WebGPU folder export with BF-indexed `G(k)` payload |
| [ShowDiffraction](showdiffraction) | `quantem.widget.showdiffraction.ShowDiffraction` | state JSON, PNG, interactive HTML |
| [ChooseLattice](choose-lattice) | `quantem.widget.choose_lattice.ChooseLattice` | state JSON (`save_state=True`) |
| [ShowBragg](showbragg) | `quantem.widget.showbragg.ShowBragg` | state JSON (`save_state=True`), `BraggVectors` and `StrainMap` in Python |
| [ShowFolder](showfolder) | `quantem.widget.showfolder.ShowFolder` | selection JSON for selected microscopy files and folders |

All widget-level HTML exports follow the [HTML export](html-export) protocol.
70 changes: 70 additions & 0 deletions docs/api/showbragg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# ShowBragg

Drive `quantem.diffraction.BraggVectors` Bragg-disk workflow. Probe the correlation map at a scan
position, tune disk detection on sampled subset, and from there accumulate a Bragg vector
map with fitting the lattice at every scan position.

```python
from quantem.widget import ShowBragg

widget = ShowBragg(dataset) # Dataset4dstem, or an existing BraggVectors
```

Template generation and correlation probing are updated interactively as their controls change. Operations that require processing the full scan, like Bragg-disk detection and lattice fitting, are run explicitly using buttons or Python methods.

```python
widget.detect() # full detect_disks
widget.fit() # fit_lattice
widget.basis # (origin, g1, g2)
strain = widget.strain_map() # Construct StrainMap
```

Scientific results remain owned by the underlying BraggVectors workflow. The complete wrapped BraggVectors object is available through widget.bragg for functionality that is not exposed directly in the interface.

For template_radius and max_peak_shift, a value of 0.0 means that the corresponding value should be determined automatically by quantem.

Strain visualization is handled outside ShowBragg, the widget returns a StrainMap, which can then be visualized using the standard StrainMap.plot_strain interface.

## Reference

```{eval-rst}
.. autoclass:: quantem.widget.showbragg.ShowBragg
:members:
:show-inheritance:
```

## Display configuration

`ShowBragg` follows the shared viewer presets described in the
[UI Guide](../developer/ui-guide):

Each UI preset configures show_title, show_controls, and controls_collapsed.

The controls can also be managed programmatically:

* widget.collapse_controls()
* widget.expand_controls()
* widget.toggle_controls()

When the controls are collapsed, parameter columns and execution buttons are hidden while the primary visualization remains visible.

## Interactive controls

| Control | Trait | Behavior |
|---|---|---|
| Template source selector | `template_source` | Rebuilds the template from a synthetic disk, the data mean, or a supplied probe |
| Template radius / edge | `template_radius`, `template_edge` | Re-renders `template_png` and the correlation map |
| Subtract mean toggle | `template_subtract_mean` | Rebuilds the template as a zero-sum band-pass kernel |
| Scan row / col fields | `probe_position` | Re-renders the diffraction pattern and its correlation map |
| Six detection fields | `min_abs_intensity`, `min_spacing`, `edge_boundary`, `subpixel`, `upsample_factor`, `max_num_peaks` | Stored for the next preview or full run |
| Preview on grid button | `preview_peaks`, `detection_state` | Detects on `preview_grid` x `preview_grid` sampled positions and marks them on the probe panel |
| Run full detection button | `detection_state`, `bvm_png`, `candidates` | Detects at every scan position, then accumulates the Bragg vector map |
| Recompute map button | `bvm_sampling`, `bvm_png` | Re-accumulates the Bragg vector map at the given sampling |
| Candidate fields | `num_candidates`, `candidate_min_spacing`, `candidate_min_abs_intensity` | Re-derives the numbered candidate set live |
| Click a numbered candidate | `origin_index`, `g1_index`, `g2_index` | Assigns that candidate to the active basis role |
| Drag a basis marker | `origin_rc`, `g1_rc`, `g2_rc` | Places the vector freely and clears that role's candidate index |
| Reset to automatic button | all six basis traits | Lets quantem pick the basis again |
| Run fit button | `fit_state`, `mask_weight_png`, `fit_error_png` | Fits the lattice at every position and shows the two diagnostics |
| Controls / Hide button | `controls_collapsed` | Hides the parameter columns and run buttons, keeping the images |
| Pan (drag) / zoom (wheel) | view transform | Any image panel translates / zooms about the cursor |
| Double-click | view transform | Resets that panel's zoom and pan |
12 changes: 6 additions & 6 deletions docs/developer/ui-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ Show3D(

| Name | Widgets | Meaning |
|---|---|---|
| `show_title` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction | Show or hide the top title row. |
| `show_controls` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction | Expose controls at all. Set `False` for a permanently clean display with no GUI recovery button. |
| `controls_collapsed` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction | Start with controls hidden. Show4DSTEM exposes this as programmatic/state control only; other widgets may also show a top `Controls` button. |
| `collapse_controls()` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction | Programmatically collapse controls. |
| `expand_controls()` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction | Programmatically expand controls. |
| `toggle_controls()` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction | Programmatically toggle collapsed controls. |
| `show_title` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction, ShowBragg | Show or hide the top title row. |
| `show_controls` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction, ShowBragg | Expose controls at all. Set `False` for a permanently clean display with no GUI recovery button. |
| `controls_collapsed` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction, ShowBragg | Start with controls hidden. Show4DSTEM exposes this as programmatic/state control only; other widgets may also show a top `Controls` button. |
| `collapse_controls()` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction, ShowBragg | Programmatically collapse controls. |
| `expand_controls()` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction, ShowBragg | Programmatically expand controls. |
| `toggle_controls()` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowEDS, ShowDiffraction, ShowBragg | Programmatically toggle collapsed controls. |
| `show_stats` | Show1D, Show2D, Show3D, Show3DSlices, Show4DSTEM, ShowDiffraction | Show or hide mean/min/max/std readouts where the widget renders or computes them. |
| `show_panel_titles` | Show2D, Show3D | Show labels on individual image/volume panels. |
| `panel_title_font_size` | Show2D, Show3D | Font size for per-panel labels. |
Expand Down
105 changes: 34 additions & 71 deletions js/chooselattice/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@ import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import { useTheme } from "../theme";
import { extractBytes, preserveRestoredWidgetModelsOnSave } from "../format";
import { preserveRestoredWidgetModelsOnSave } from "../format";
import { useHideStaticFallback } from "../staticFallback";
import {
canvasPoint,
clamp,
drawImage,
imageToScreen,
screenToImage,
usePngBitmap,
zoomAt,
type ImageViewport,
} from "../imageView";

const MIN_ZOOM = 0.5;
const MAX_ZOOM = 20;
const CANVAS_SIZE = 512;
const CANVAS_BORDER_PX = 1;
const HIT_PX = 10;
Expand All @@ -37,10 +45,6 @@ const compactButton = {
type Point = [number, number]; // [row, col] in original image pixels
type DragMode = "none" | "pan" | "point";

function clamp(value: number, lo: number, hi: number): number {
return Math.min(hi, Math.max(lo, value));
}

function ChooseLattice() {
const model = useModel();
const rootRef = React.useRef<HTMLDivElement>(null);
Expand All @@ -56,38 +60,19 @@ function ChooseLattice() {
const [pointLabels] = useModelState<string[]>("point_labels");
const [points, setPoints] = useModelState<Point[]>("points");

// Decode the PNG payload once per change into a drawable bitmap.
const [image, setImage] = React.useState<ImageBitmap | HTMLImageElement | null>(null);
React.useEffect(() => {
const bytes = extractBytes(frameBytes);
if (bytes.length === 0) {
setImage(null);
return;
}
let cancelled = false;
const blob = new Blob([bytes as unknown as BlobPart], { type: "image/png" });
if (typeof createImageBitmap === "function") {
createImageBitmap(blob).then((bmp) => { if (!cancelled) setImage(bmp); });
} else {
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => { if (!cancelled) setImage(img); URL.revokeObjectURL(url); };
img.src = url;
}
return () => { cancelled = true; };
}, [frameBytes]);
const image = usePngBitmap(frameBytes);

// View state: zoom + pan (CSS px, canvas-centered).
const [zoom, setZoom] = React.useState(1);
const [panX, setPanX] = React.useState(0);
const [panY, setPanY] = React.useState(0);

// displayScale maps original image pixels -> CSS px at zoom=1.
const displayScale = height > 0 && width > 0
? CANVAS_SIZE / Math.max(height, width)
: 1;
const canvasW = CANVAS_SIZE;
const canvasH = CANVAS_SIZE;
const viewport: ImageViewport = React.useMemo(
() => ({ height, width, canvas: CANVAS_SIZE, zoom, panX, panY }),
[height, width, zoom, panX, panY],
);

const canvasRef = React.useRef<HTMLCanvasElement>(null);
const uiRef = React.useRef<HTMLCanvasElement>(null);
Expand Down Expand Up @@ -116,13 +101,7 @@ function ChooseLattice() {
ctx.fillStyle = themeColors.bg;
ctx.fillRect(0, 0, canvasW, canvasH);
if (!image || !width || !height) return;
const cx = canvasW / 2;
const cy = canvasH / 2;
const drawW = width * displayScale * zoom;
const drawH = height * displayScale * zoom;
const x = cx - drawW / 2 + panX;
const y = cy - drawH / 2 + panY;
ctx.drawImage(image, x, y, drawW, drawH);
drawImage(ctx, image, viewport);
// Confirm the decoded bitmap on the next compositing frame. Static docs
// can mount while Chrome is still promoting the canvas layer; without a
// second paint that one-shot draw can remain a white presentation frame.
Expand All @@ -132,61 +111,45 @@ function ChooseLattice() {
if (!current || !currentCtx) return;
currentCtx.fillStyle = themeColors.bg;
currentCtx.fillRect(0, 0, canvasW, canvasH);
currentCtx.drawImage(image, x, y, drawW, drawH);
drawImage(currentCtx, image, viewport);
});
return () => window.cancelAnimationFrame(confirmFrame);
}, [image, width, height, displayScale, zoom, panX, panY, canvasW, canvasH, themeColors.bg]);
}, [image, width, height, viewport, canvasW, canvasH, themeColors.bg]);

// Convert a mouse event to original-image (row, col) coordinates.
const screenToImg = React.useCallback((e: { clientX: number; clientY: number }): Point => {
const canvas = canvasRef.current;
if (!canvas) return [0, 0];
const rect = canvas.getBoundingClientRect();
const mouseCanvasX = (e.clientX - rect.left) * (canvas.width / rect.width);
const mouseCanvasY = (e.clientY - rect.top) * (canvas.height / rect.height);
const cx = canvasW / 2;
const cy = canvasH / 2;
const col = (mouseCanvasX - cx - panX) / (displayScale * zoom) + width / 2;
const row = (mouseCanvasY - cy - panY) / (displayScale * zoom) + height / 2;
return [row, col];
}, [canvasW, canvasH, panX, panY, displayScale, zoom, width, height]);

const imgToScreen = React.useCallback((row: number, col: number): [number, number] => {
const cx = canvasW / 2;
const cy = canvasH / 2;
const x = cx + (col - width / 2) * displayScale * zoom + panX;
const y = cy + (row - height / 2) * displayScale * zoom + panY;
return [x, y];
}, [canvasW, canvasH, panX, panY, displayScale, zoom, width, height]);
const [x, y] = canvasPoint(canvas, e);
return screenToImage(viewport, x, y);
}, [viewport]);

const imgToScreen = React.useCallback(
(row: number, col: number): [number, number] => imageToScreen(viewport, row, col),
[viewport],
);

const hitTestPoint = React.useCallback((row: number, col: number): number => {
const hitArea = HIT_PX / (displayScale * zoom);
const hitArea = HIT_PX / ((canvasW / Math.max(height, width)) * zoom);
const list = points || [];
for (let i = list.length - 1; i >= 0; i--) {
const [pr, pc] = list[i];
if (Math.hypot(row - pr, col - pc) <= hitArea) return i;
}
return -1;
}, [points, displayScale, zoom]);
}, [points, canvasW, height, width, zoom]);

// Wheel: cursor-anchored zoom. Page-scroll prevention is handled by a
// native non-passive listener below (React's synthetic onWheel is passive,
// so calling preventDefault directly here would only log a console warning).
const handleWheel = (e: React.WheelEvent) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mouseCanvasX = (e.clientX - rect.left) * (canvas.width / rect.width);
const mouseCanvasY = (e.clientY - rect.top) * (canvas.height / rect.height);
const cx = canvasW / 2;
const cy = canvasH / 2;
const mouseImageX = (mouseCanvasX - cx - panX) / zoom + cx;
const mouseImageY = (mouseCanvasY - cy - panY) / zoom + cy;
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = clamp(zoom * zoomFactor, MIN_ZOOM, MAX_ZOOM);
setPanX(mouseCanvasX - (mouseImageX - cx) * newZoom - cx);
setPanY(mouseCanvasY - (mouseImageY - cy) * newZoom - cy);
setZoom(newZoom);
const [x, y] = canvasPoint(canvas, e);
const next = zoomAt(viewport, x, y, e.deltaY);
setPanX(next.panX);
setPanY(next.panY);
setZoom(next.zoom);
};

const resetView = React.useCallback(() => {
Expand Down
22 changes: 22 additions & 0 deletions js/imageView.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { imageToScreen, screenToImage, zoomAt, type ImageViewport } from "./imageView";

const view: ImageViewport = { height: 48, width: 64, canvas: 512, zoom: 1, panX: 0, panY: 0 };

describe("imageView", () => {
it("round-trips image and screen coordinates under pan and zoom", () => {
const panned: ImageViewport = { ...view, zoom: 3.2, panX: -40, panY: 17 };
const [x, y] = imageToScreen(panned, 12.5, 30.25);
const [row, col] = screenToImage(panned, x, y);
expect(row).toBeCloseTo(12.5);
expect(col).toBeCloseTo(30.25);
});

it("keeps the point under the cursor fixed while zooming", () => {
const before = screenToImage(view, 300, 200);
const zoomed: ImageViewport = { ...view, ...zoomAt(view, 300, 200, -1) };
const after = screenToImage(zoomed, 300, 200);
expect(after[0]).toBeCloseTo(before[0]);
expect(after[1]).toBeCloseTo(before[1]);
});
});
Loading