# Show4DSTEM — All Features Comprehensive demo of every Show4DSTEM feature using realistic synthetic 4D-STEM data with a bright-field disk, Bragg reflections, Kikuchi-like background, and scan-position-dependent diffraction contrast. Data generated with PyTorch (GPU-accelerated on MPS/CUDA). For 5D time/tilt series support, see
show4dstem_5d.ipynb.
[1]:
# Install in Google Colab
try:
import google.colab
!pip install -q -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ quantem-widget
except ImportError:
pass # Not in Colab, skip
[2]:
try:
%load_ext autoreload
%autoreload 2
%env ANYWIDGET_HMR=1
except Exception:
pass # autoreload unavailable (Colab Python 3.12+)
env: ANYWIDGET_HMR=1
1. Synthetic 4D-STEM Data Generator (PyTorch)#
Creates a realistic 4D-STEM dataset using vectorized PyTorch operations:
Bright-field disk with position-dependent beam tilt
6 first-order Bragg reflections with orientation-dependent intensity
6 second-order spots (weaker)
Radial amorphous scattering background
Poisson shot noise
[3]:
import torch
import numpy as np
from quantem.widget import Show4DSTEM
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
def make_4dstem(scan_rows=16, scan_cols=16, det_rows=64, det_cols=64):
"""4D-STEM dataset with BF disk, Bragg spots, and amorphous background (PyTorch)."""
# Detector coordinate grids
dr = torch.arange(det_rows, device=device, dtype=torch.float32)
dc = torch.arange(det_cols, device=device, dtype=torch.float32)
rr, cc = torch.meshgrid(dr, dc, indexing="ij") # (det_rows, det_cols)
cr, cc0 = det_rows / 2, det_cols / 2
center_dist = ((rr - cr) ** 2 + (cc - cc0) ** 2).sqrt()
# Amorphous background (radial falloff)
bg = 0.05 * torch.exp(-center_dist / 30)
# BF disk
bf = (center_dist < 8).float() * (1.0 + 0.2 * torch.cos(center_dist * 0.5))
# 6 first-order Bragg spots
spots = torch.zeros(det_rows, det_cols, device=device)
spot_angles = []
for k in range(6):
angle = k * torch.pi / 3
spot_angles.append(angle)
sr = cr + 20 * torch.sin(torch.tensor(angle, device=device))
sc = cc0 + 20 * torch.cos(torch.tensor(angle, device=device))
d2 = (rr - sr) ** 2 + (cc - sc) ** 2
spots += 0.4 * torch.exp(-d2 / (2 * 2.5 ** 2))
# 6 second-order spots (weaker)
for k in range(6):
angle = k * torch.pi / 3 + torch.pi / 6
sr = cr + 35 * torch.sin(torch.tensor(angle, device=device))
sc = cc0 + 35 * torch.cos(torch.tensor(angle, device=device))
d2 = (rr - sr) ** 2 + (cc - sc) ** 2
spots += 0.1 * torch.exp(-d2 / (2 * 2.0 ** 2))
# Base pattern: (det_rows, det_cols)
base = bg + bf + spots
# Scan-position modulation (thickness/orientation variation)
si = torch.arange(scan_rows, device=device, dtype=torch.float32)
sj = torch.arange(scan_cols, device=device, dtype=torch.float32)
si_g, sj_g = torch.meshgrid(si, sj, indexing="ij")
modulation = 1.0 + 0.15 * torch.sin(2 * torch.pi * si_g / scan_rows) * torch.cos(2 * torch.pi * sj_g / scan_cols)
# Broadcast: (scan_rows, scan_cols, 1, 1) * (1, 1, det_rows, det_cols)
data = base[None, None, :, :] * modulation[:, :, None, None]
# Poisson shot noise (use NumPy — torch.poisson not available on MPS)
data_np = data.clamp(min=0).cpu().numpy() * 200
data_np = np.random.poisson(data_np).astype(np.float32) / 200
return data_np
data = make_4dstem()
import quantem.widget
print(f"quantem.widget {quantem.widget.__version__}")
print(f"Shape: {data.shape}, dtype: {data.dtype}")
print(f"Range: [{data.min():.3f}, {data.max():.3f}]")
Using device: mps
quantem.widget 0.4.0a3
Shape: (16, 16, 64, 64), dtype: float32
Range: [0.000, 1.595]
2. Basic 4D-STEM Viewer#
Default view with auto-detected BF disk center and radius.
[4]:
w_basic = Show4DSTEM(data)
w_basic
[4]:
3. Flattened 3D Input with scan_shape#
When data arrives as a flat stack (N, det_x, det_y) from a detector readout, pass scan_shape to tell the widget how to reshape it into a 2D scan grid.
[5]:
# Flatten 4D -> 3D: (16*16, 64, 64) = (256, 64, 64)
data_flat = data.reshape(-1, data.shape[2], data.shape[3])
print(f"Flattened shape: {data_flat.shape}")
w_flat = Show4DSTEM(data_flat, scan_shape=(16, 16))
w_flat
Flattened shape: (256, 64, 64)
[5]:
4. Auto-Detect Center#
The auto_detect_center() method uses centroid analysis of the summed diffraction pattern to find the BF disk center and estimate its radius.
[6]:
w_auto = Show4DSTEM(data)
w_auto.auto_detect_center()
print(f"Auto-detected center: ({w_auto.center_row:.2f}, {w_auto.center_col:.2f})")
print(f"Auto-detected BF radius: {w_auto.bf_radius:.2f} px")
print(f"Detector center (expected): ({data.shape[2]//2}, {data.shape[3]//2})")
w_auto
Auto-detected center: (31.91, 32.06)
Auto-detected BF radius: 9.77 px
Detector center (expected): (32, 32)
[6]:
5. Manual Center and BF Radius#
Override auto-detection with explicit center and bright-field radius values. Useful when the auto-detect does not work well (e.g., very noisy data or off-axis diffraction patterns).
[7]:
w_manual = Show4DSTEM(data, center=(32.0, 32.0), bf_radius=9.0)
print(f"Manual center: ({w_manual.center_row}, {w_manual.center_col})")
print(f"Manual BF radius: {w_manual.bf_radius}")
w_manual
Manual center: (32.0, 32.0)
Manual BF radius: 9.0
[7]:
6. ROI Modes#
Different ROI shapes for virtual imaging. Each mode integrates diffraction intensity within the ROI to produce a virtual image in real-space.
point — single pixel (fastest)
circle — virtual BF detector
square — square integration region
annular — ADF/HAADF ring detector
rect — rectangular region
[8]:
# Point ROI (single-pixel virtual image)
w_point = Show4DSTEM(data)
w_point.roi_point()
w_point
[8]:
[9]:
# Circle ROI (virtual bright-field)
w_circle = Show4DSTEM(data)
w_circle.roi_circle(radius=8.0)
w_circle
[9]:
[10]:
# Square ROI
w_square = Show4DSTEM(data)
w_square.roi_square(half_size=6.0)
w_square
[10]:
[11]:
# Annular ROI (ADF — collects scattered electrons outside BF disk)
w_annular = Show4DSTEM(data)
w_annular.roi_annular(inner_radius=10.0, outer_radius=25.0)
w_annular
[11]:
[12]:
# Rectangle ROI
w_rect = Show4DSTEM(data)
w_rect.roi_rect(width=20.0, height=10.0)
w_rect
[12]:
7. Log Scale#
Diffraction patterns have very high dynamic range. The bright BF disk can be orders of magnitude brighter than Bragg spots. Log scale compresses this range to reveal weak features.
[13]:
w_log = Show4DSTEM(data)
w_log.dp_scale_mode = "log"
w_log.roi_circle()
w_log
[13]:
8. Precomputed Virtual Images#
When precompute_virtual_images=True, the widget pre-calculates BF, ABF, and ADF virtual images at startup. Switching between these presets is then instantaneous (no recomputation needed).
[14]:
w_precomp = Show4DSTEM(data, precompute_virtual_images=True)
w_precomp.roi_circle()
print(f"BF cached: {w_precomp._cached_bf_virtual is not None}")
print(f"ABF cached: {w_precomp._cached_abf_virtual is not None}")
print(f"ADF cached: {w_precomp._cached_adf_virtual is not None}")
w_precomp
BF cached: True
ABF cached: True
ADF cached: True
[14]:
9. Raster Scan Animation#
Animate the scan position across the sample in a raster pattern (row by row, left to right), mimicking real STEM acquisition. The crosshair moves through the virtual image while the diffraction pattern updates live.
[15]:
w_raster = Show4DSTEM(data)
w_raster.roi_circle(radius=8.0)
w_raster.raster(step=2, interval_ms=150, loop=True)
w_raster
[15]:
10. Custom Path Animation#
Define an arbitrary sequence of scan positions. Here we create a diagonal path and a spiral path to explore specific regions of the sample.
[16]:
# Diagonal path from corner to corner
diagonal_path = [(i, i) for i in range(16)]
w_diag = Show4DSTEM(data)
w_diag.roi_circle(radius=8.0)
w_diag.set_path(diagonal_path, interval_ms=200, loop=True)
print(f"Diagonal path: {len(diagonal_path)} positions")
w_diag
Diagonal path: 16 positions
[16]:
[17]:
# Spiral path from center outward
spiral_path = []
cx, cy = 8, 8 # center of scan
for r in range(1, 8):
n_pts = max(4, r * 4)
for t in range(n_pts):
angle = 2 * np.pi * t / n_pts
x = int(round(cx + r * np.cos(angle)))
y = int(round(cy + r * np.sin(angle)))
if 0 <= x < 16 and 0 <= y < 16:
spiral_path.append((x, y))
w_spiral = Show4DSTEM(data)
w_spiral.roi_circle(radius=8.0)
w_spiral.set_path(spiral_path, interval_ms=100, loop=True)
print(f"Spiral path: {len(spiral_path)} positions")
w_spiral
Spiral path: 112 positions
[17]:
11. Playback Controls#
Programmatic control of path animations: pause, play, stop, and jump to specific positions.
[18]:
w_ctrl = Show4DSTEM(data)
w_ctrl.roi_circle(radius=8.0)
w_ctrl.raster(step=1, interval_ms=100)
w_ctrl
[18]:
[19]:
# Pause the animation
w_ctrl.pause()
print(f"Paused at index: {w_ctrl.path_index}, position: {w_ctrl.position}")
Paused at index: 0, position: (8, 8)
[20]:
# Jump to a specific frame and resume
w_ctrl.goto(50)
print(f"Jumped to index: {w_ctrl.path_index}, position: {w_ctrl.position}")
w_ctrl.play()
Jumped to index: 50, position: (3, 2)
[20]:
[21]:
# Stop and reset to beginning
w_ctrl.stop()
print(f"Stopped at index: {w_ctrl.path_index}")
Stopped at index: 0
12. Scale Bars#
Set pixel_size for real-space scale bar (in angstroms) and k_pixel_size for k-space / diffraction scale bar (in milliradians).
[22]:
w_scale = Show4DSTEM(
data,
pixel_size=2.39, # 2.39 angstrom per scan pixel
k_pixel_size=0.46, # 0.46 mrad per detector pixel
)
w_scale.roi_circle(radius=8.0)
print(f"Real-space pixel size: {w_scale.pixel_size} angstrom")
print(f"K-space pixel size: {w_scale.k_pixel_size} mrad")
print(f"K-space calibrated: {w_scale.k_calibrated}")
w_scale
Real-space pixel size: 2.39 angstrom
K-space pixel size: 0.46 mrad
K-space calibrated: True
[22]:
13. Rectangular Scan Shape#
Non-square scan grids are common when the scan region is not square. Here we generate a 24x12 scan with the same diffraction physics.
[23]:
data_rect = make_4dstem(scan_rows=24, scan_cols=12)
print(f"Rectangular scan shape: {data_rect.shape}")
w_rect_scan = Show4DSTEM(data_rect)
w_rect_scan.roi_circle(radius=8.0)
print(f"Scan shape: {w_rect_scan.scan_shape}")
print(f"Detector shape: {w_rect_scan.detector_shape}")
w_rect_scan
Rectangular scan shape: (24, 12, 64, 64)
Scan shape: (24, 12)
Detector shape: (64, 64)
[23]:
14. VI ROI (Real-Space Region for Summed DP)#
The VI ROI selects a region in the virtual image (real-space) and sums all diffraction patterns within that region. This produces an averaged diffraction pattern with better signal-to-noise, useful for identifying crystallographic features from a specific area of the sample.
[24]:
w_vi = Show4DSTEM(data)
w_vi.roi_circle(radius=8.0)
# Enable circular VI ROI in real-space
w_vi.vi_roi_mode = "circle"
w_vi.vi_roi_center_row = 8.0
w_vi.vi_roi_center_col = 8.0
w_vi.vi_roi_radius = 4.0
print(f"VI ROI: circle at ({w_vi.vi_roi_center_row}, {w_vi.vi_roi_center_col}), r={w_vi.vi_roi_radius}")
print(f"Summed {w_vi.summed_dp_count} positions")
w_vi
VI ROI: circle at (8.0, 8.0), r=4.0
Summed 49 positions
[24]:
15. Mask DC Component#
The central pixel of the diffraction pattern (DC component) often saturates the detector. mask_dc excludes the center 3x3 region from DP statistics calculations, giving more meaningful contrast metrics.
[25]:
# DC masking enabled (default)
w_dc_on = Show4DSTEM(data)
w_dc_on.mask_dc = True
print(f"mask_dc=True -> DP stats: {w_dc_on.dp_stats}")
# DC masking disabled
w_dc_off = Show4DSTEM(data)
w_dc_off.mask_dc = False
w_dc_off.position = w_dc_on.position # same position for comparison
print(f"mask_dc=False -> DP stats: {w_dc_off.dp_stats}")
print()
print("With mask_dc=True, the center bright spot is excluded from stats.")
w_dc_on
mask_dc=True -> DP stats: [0.08778076618909836, 0.0, 1.215000033378601, 0.19274771213531494]
mask_dc=False -> DP stats: [0.08778076618909836, 0.0, 1.215000033378601, 0.19274771213531494]
With mask_dc=True, the center bright spot is excluded from stats.
[25]:
16. State Persistence#
Save and restore all display settings — center, BF radius, ROI config, log scale, calibration — to a JSON file. Resume analysis after a kernel restart or share exact display state with a colleague.
[26]:
# Inspect current state
w_state = Show4DSTEM(data, center=(32.0, 32.0), bf_radius=9.0)
w_state.dp_scale_mode = "log"
w_state.roi_annular(inner_radius=10.0, outer_radius=25.0)
w_state.summary()
w_state
Show4DSTEM
════════════════════════════════
Scan: 16×16 (1.00 Å/px)
Detector: 64×64 (1.0000 px/px)
Position: (8, 8)
Center: (32.0, 32.0) BF r=9.0 px
Display: DC masked
ROI: annular at (32.0, 32.0) r=25.0
DP view: inferno, log, 0.0-100.0%
VI view: inferno, linear, 0.0-100.0%
[26]:
[27]:
# Save state to JSON
w_state.save("show4dstem_state.json")
print("Saved to show4dstem_state.json")
# Inspect the state dict
import json
print(json.dumps(w_state.state_dict(), indent=2))
Saved to show4dstem_state.json
{
"title": "",
"pos_row": 8,
"pos_col": 8,
"pixel_size": 1.0,
"k_pixel_size": 1.0,
"k_calibrated": false,
"center_row": 32.0,
"center_col": 32.0,
"bf_radius": 9.0,
"roi_active": true,
"roi_mode": "annular",
"roi_center_row": 32.0,
"roi_center_col": 32.0,
"roi_radius": 25.0,
"roi_radius_inner": 10.0,
"roi_width": 20.0,
"roi_height": 10.0,
"vi_roi_mode": "off",
"vi_roi_center_row": 8.0,
"vi_roi_center_col": 8.0,
"vi_roi_radius": 3.0,
"vi_roi_width": 6.0,
"vi_roi_height": 3.0,
"mask_dc": true,
"dp_colormap": "inferno",
"vi_colormap": "inferno",
"fft_colormap": "inferno",
"dp_scale_mode": "log",
"vi_scale_mode": "linear",
"fft_scale_mode": "linear",
"dp_power_exp": 0.5,
"vi_power_exp": 0.5,
"fft_power_exp": 0.5,
"dp_vmin_pct": 0.0,
"dp_vmax_pct": 100.0,
"vi_vmin_pct": 0.0,
"vi_vmax_pct": 100.0,
"fft_vmin_pct": 0.0,
"fft_vmax_pct": 100.0,
"fft_auto": true,
"show_fft": false,
"show_controls": true,
"dp_show_colorbar": false,
"export_default_view": "all",
"export_default_format": "png",
"export_include_overlays": true,
"export_include_scalebar": true,
"export_default_dpi": 300,
"path_interval_ms": 100,
"path_loop": true,
"profile_line": [],
"profile_width": 1,
"frame_idx": 0,
"frame_dim_label": "Frame",
"frame_loop": true,
"frame_fps": 5.0,
"frame_reverse": false,
"frame_boomerang": false,
"disabled_tools": [],
"hidden_tools": []
}
[28]:
# Restore from file — all settings come back
w_restored = Show4DSTEM(data, state="show4dstem_state.json")
print(f"Restored: center=({w_restored.center_row}, {w_restored.center_col}), bf_radius={w_restored.bf_radius}")
w_restored
Restored: center=(32.0, 32.0), bf_radius=9.0
[28]:
[29]:
# Clean up
from pathlib import Path
Path("show4dstem_state.json").unlink(missing_ok=True)