Open In Colab # Show2D — All Features Comprehensive demo of every Show2D feature using realistic electron microscopy synthetic data: HRTEM lattice fringes, HAADF-STEM atomic columns, SAED diffraction patterns, focal series, and more.

[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
[3]:
import numpy as np
import torch
import quantem.widget
from quantem.widget import Show2D
from IPython.display import display
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
np.random.seed(42)
def make_hrtem(size=256):
    """Simulate HRTEM lattice fringes (like Si [110])."""
    y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
                           torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
    img = torch.zeros((size, size), device=device)
    freqs = [(0.08, 0.0), (0.06, np.pi / 3), (0.10, np.pi / 6)]
    for freq, angle in freqs:
        img += torch.cos(2 * np.pi * freq * (x * np.cos(angle) + y * np.sin(angle)))
    envelope = 1.0 / (1 + torch.exp(-0.05 * (min(size // 2, 100) - torch.sqrt((x - size // 2)**2 + (y - size // 2)**2))))
    img = img * envelope
    noise = torch.from_numpy(np.random.normal(0, 0.3, (size, size)).astype(np.float32)).to(device)
    img = img + noise
    return img.cpu().numpy()
def make_haadf(size=256, spacing=16, sigma=2.5):
    """Simulate HAADF-STEM image with atomic columns on a hexagonal lattice."""
    y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
                           torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
    img = torch.from_numpy(np.random.normal(0.1, 0.02, (size, size)).astype(np.float32)).to(device)
    a1 = np.array([spacing, 0])
    a2 = np.array([spacing / 2, spacing * np.sqrt(3) / 2])
    # Precompute all atom column positions
    i_range = torch.arange(-1, size // spacing + 2, device=device, dtype=torch.float32)
    j_range = torch.arange(-1, size // spacing + 2, device=device, dtype=torch.float32)
    ii, jj = torch.meshgrid(i_range, j_range, indexing="ij")
    cx_all = (ii * a1[0] + jj * a2[0]).reshape(-1)
    cy_all = (ii * a1[1] + jj * a2[1]).reshape(-1)
    # Filter to valid positions
    mask = (cx_all > -spacing) & (cx_all < size + spacing) & (cy_all > -spacing) & (cy_all < size + spacing)
    cx_all = cx_all[mask]
    cy_all = cy_all[mask]
    # Vectorized: compute all Gaussians at once using broadcasting
    intensities = 0.8 + 0.2 * torch.rand(cx_all.shape[0], device=device)
    # x: (size, size), cx_all: (N,) -> broadcast (N, size, size)
    dx = x.unsqueeze(0) - cx_all.reshape(-1, 1, 1)
    dy = y.unsqueeze(0) - cy_all.reshape(-1, 1, 1)
    gaussians = intensities.reshape(-1, 1, 1) * torch.exp(-(dx**2 + dy**2) / (2 * sigma**2))
    img = img + gaussians.sum(dim=0)
    return img.cpu().numpy()
def make_diffraction(size=256, n_rings=4):
    """Simulate SAED diffraction pattern with Bragg spots."""
    y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
                           torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
    cx, cy = size // 2, size // 2
    pattern = 8.0 * torch.exp(-((x - cx)**2 + (y - cy)**2) / (2 * 4**2))
    for ring in range(1, n_rings + 1):
        r = ring * 22
        n_spots = 6
        for k in range(n_spots):
            angle = k * 2 * np.pi / n_spots + ring * np.pi / 12
            sx = cx + r * np.cos(angle)
            sy = cy + r * np.sin(angle)
            intensity = 2.0 / ring
            spot_sigma = max(1.5, 3.0 - ring * 0.3)
            pattern += intensity * torch.exp(-((x - sx)**2 + (y - sy)**2) / (2 * spot_sigma**2))
    dist = torch.sqrt((x - cx)**2 + (y - cy)**2)
    pattern += 0.3 * torch.exp(-dist / 60)
    pattern = torch.clamp(pattern, min=0)
    # Use NumPy for Poisson noise (torch.poisson may not work on MPS)
    pattern_np = pattern.cpu().numpy()
    pattern_np = np.random.poisson(np.clip(pattern_np * 100, 0, 1e6)).astype(np.float32) / 100
    return pattern_np
print(f"Generators ready (device={device})")
print(f"quantem.widget {quantem.widget.__version__}")
Generators ready (device=mps)
quantem.widget 0.4.0a3

1. Single HRTEM Image#

Crystal lattice fringes with amorphous edge envelope and shot noise.

[4]:
hrtem = make_hrtem(256)
Show2D(hrtem, title="HRTEM — Si [110] Lattice Fringes", cmap="gray")
[4]:

2. Single HAADF-STEM Image#

Atomic columns arranged on a hexagonal lattice with intensity variations simulating Z-contrast.

[5]:
haadf = make_haadf(256, spacing=20, sigma=2.5)
Show2D(haadf, title="HAADF-STEM — Hexagonal Atomic Columns", cmap="inferno")
[5]:

5. Different-sized Images#

Images from different detectors with varying pixel counts. Show2D resizes them to a common size.

[8]:
img_128 = make_haadf(128, spacing=10, sigma=1.5)
img_256 = make_hrtem(256)
img_200 = make_diffraction(200, n_rings=3)
Show2D(
    [img_128, img_256, img_200],
    labels=["HAADF 128x128", "HRTEM 256x256", "Diffraction 200x200"],
    title="Mixed Detector Sizes",
    ncols=3,
)
[8]:

6. Colormaps#

The same HAADF image under all available colormaps.

[9]:
haadf_cmap = make_haadf(192, spacing=18, sigma=2.2)
for cmap in ["inferno", "viridis", "magma", "plasma", "gray"]:
    display(Show2D(haadf_cmap, title=f"HAADF — {cmap}", cmap=cmap))

7. FFT — Single Image#

HRTEM lattice fringes with FFT enabled. The reciprocal lattice spots should be visible at spatial frequencies corresponding to the real-space lattice periodicities.

[10]:
hrtem_fft = make_hrtem(256)
Show2D(hrtem_fft, title="HRTEM + FFT", cmap="gray", show_fft=True)
[10]:

8b. ROI + FFT — Region-specific Frequency Analysis#

Enable FFT and ROI together. The FFT panel shows the frequency content of just the selected ROI region. Drag the ROI to compare crystal structure across different areas of the image in real time.

[12]:
hrtem_roi = make_hrtem(512)
w_roi_fft = Show2D(
    hrtem_roi,
    title="HRTEM — ROI + FFT",
    cmap="gray",
    show_fft=True,
)
# Place a square ROI and select it
w_roi_fft.add_roi(row=256, col=256, shape="square")
w_roi_fft.roi_square(half_size=64)
w_roi_fft
[12]:

9. Log Scale#

Diffraction pattern with log scale — the pattern naturally spans several orders of magnitude from the intense central beam to weak high-order reflections.

[13]:
diff_log = make_diffraction(256, n_rings=5)
display(Show2D(diff_log, title="Diffraction — Linear Scale", cmap="inferno"))
display(Show2D(diff_log, title="Diffraction — Log Scale", cmap="inferno", log_scale=True))

10. Auto Contrast#

HAADF with one extremely bright outlier atom column. Auto contrast uses percentiles to clip the display range so the rest of the image is visible.

[14]:
haadf_outlier = make_haadf(256, spacing=20, sigma=2.5)
# Add one very bright outlier column
oy, ox = torch.meshgrid(torch.arange(256, device=device, dtype=torch.float32),
                         torch.arange(256, device=device, dtype=torch.float32), indexing="ij")
outlier = 50.0 * torch.exp(-((ox - 128)**2 + (oy - 100)**2) / (2 * 3**2))
haadf_outlier = haadf_outlier + outlier.cpu().numpy()
display(Show2D(haadf_outlier, title="HAADF with Outlier — No Auto Contrast"))
display(Show2D(haadf_outlier, title="HAADF with Outlier — Auto Contrast", auto_contrast=True))

11. Scale Bar#

HRTEM with a realistic pixel size. At 0.05 nm/px (0.5 angstrom/px), the scale bar displays physical dimensions.

[15]:
hrtem_scale = make_hrtem(512)
Show2D(
    hrtem_scale,
    title="HRTEM with Scale Bar (0.5 A/px)",
    cmap="gray",
    pixel_size_angstrom=0.5,
)
[15]:

12. Custom Columns#

Gallery of 8 simulated SAED patterns at different zone axes, displayed in 4 columns.

[16]:
zone_axes = [
    ("[001]", 3, 0.0),
    ("[110]", 4, np.pi / 12),
    ("[111]", 3, np.pi / 6),
    ("[112]", 5, np.pi / 8),
    ("[011]", 4, np.pi / 4),
    ("[012]", 3, np.pi / 3),
    ("[113]", 5, np.pi / 5),
    ("[102]", 4, np.pi / 10),
]
saed_images = []
saed_labels = []
size = 128
y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
                       torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
cx, cy = size // 2, size // 2
dist = torch.sqrt((x - cx)**2 + (y - cy)**2)
for label, n_rings, rot in zone_axes:
    pattern = 8.0 * torch.exp(-((x - cx)**2 + (y - cy)**2) / (2 * 4**2))
    for ring in range(1, n_rings + 1):
        r = ring * 18
        for k in range(6):
            angle = k * 2 * np.pi / 6 + rot + ring * np.pi / 12
            sx = cx + r * np.cos(angle)
            sy = cy + r * np.sin(angle)
            intensity = 2.0 / ring
            spot_sigma = max(1.5, 3.0 - ring * 0.3)
            pattern += intensity * torch.exp(-((x - sx)**2 + (y - sy)**2) / (2 * spot_sigma**2))
    pattern += 0.3 * torch.exp(-dist / 50)
    pattern = torch.clamp(pattern, min=0)
    # Use NumPy for Poisson noise
    pattern_np = pattern.cpu().numpy()
    pattern_np = np.random.poisson(np.clip(pattern_np * 100, 0, 1e6)).astype(np.float32) / 100
    saed_images.append(pattern_np)
    saed_labels.append(label)
Show2D(
    saed_images,
    labels=saed_labels,
    title="SAED Patterns at Different Zone Axes",
    ncols=4,
    log_scale=True,
)
[16]:

13. Hide Controls & Stats#

Minimal view — no UI controls or statistics, just the image.

[17]:
hrtem_minimal = make_hrtem(256)
Show2D(
    hrtem_minimal,
    title="HRTEM — Minimal View",
    cmap="gray",
    show_controls=False,
    show_stats=False,
)
[17]:

14. Custom Panel & Image Size with FFT#

Custom sizing: larger FFT/histogram panel and specified image width.

[18]:
hrtem_custom = make_hrtem(256)
Show2D(
    hrtem_custom,
    title="HRTEM — Custom Sizes + FFT",
    cmap="gray",
    show_fft=True,
    panel_size_px=200,
    image_width_px=400,
)
[18]:

15. State Persistence#

Save and restore all display settings — colormap, log scale, auto contrast, ROI, selected index, profile line — to a JSON file. Resume analysis after a kernel restart or share exact display state with a colleague.

[19]:
# Inspect current state
w_state = Show2D(hrtem, title="HRTEM Analysis", cmap="viridis", log_scale=True, auto_contrast=True)
w_state.summary()
w_state
HRTEM Analysis
════════════════════════════════
Image:    256×256
Data:     min=-3.285  max=3.442  mean=0.00108
Display:  viridis | auto contrast | log
[19]:
[20]:
# Save state to JSON
w_state.save("show2d_state.json")
print("Saved to show2d_state.json")
# Inspect the state dict
import json
print(json.dumps(w_state.state_dict(), indent=2))
Saved to show2d_state.json
{
  "title": "HRTEM Analysis",
  "cmap": "viridis",
  "log_scale": true,
  "auto_contrast": true,
  "show_stats": true,
  "show_fft": false,
  "show_controls": true,
  "disabled_tools": [],
  "hidden_tools": [],
  "pixel_size": 0.0,
  "scale_bar_visible": true,
  "canvas_size": 0,
  "ncols": 3,
  "selected_idx": 0,
  "roi_active": false,
  "roi_list": [],
  "roi_selected_idx": -1,
  "profile_line": []
}
[21]:
# Restore from file — all settings come back
w_restored = Show2D(hrtem, state="show2d_state.json")
print(f"Restored: cmap={w_restored.cmap}, log_scale={w_restored.log_scale}")
w_restored
Restored: cmap=viridis, log_scale=True
[21]:
[22]:
# Clean up
from pathlib import Path
Path("show2d_state.json").unlink(missing_ok=True)