# 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]:
3. Gallery with Labels#
Six different EM image types displayed together.
[6]:
# HRTEM
img_hrtem = make_hrtem(128)
# HAADF
img_haadf = make_haadf(128, spacing=12, sigma=1.8)
# Diffraction pattern
img_diff = make_diffraction(128, n_rings=3)
# Phase contrast — cosine of HRTEM (simulates CTF-filtered phase image)
img_phase = np.cos(make_hrtem(128) * np.pi).astype(np.float32)
# Dark field — select one lattice frequency
y, x = torch.meshgrid(torch.arange(128, device=device, dtype=torch.float32),
torch.arange(128, device=device, dtype=torch.float32), indexing="ij")
lattice = torch.cos(2 * np.pi * 0.08 * x)
envelope = 1.0 / (1 + torch.exp(-0.05 * (50 - torch.sqrt((x - 64)**2 + (y - 64)**2))))
noise = torch.from_numpy(np.random.normal(0, 0.1, (128, 128)).astype(np.float32)).to(device)
img_darkfield = (torch.abs(lattice) * envelope + noise).cpu().numpy()
# Strain map — gradient of lattice displacement
displacement = (torch.sin(2 * np.pi * x / 128) * torch.sin(2 * np.pi * y / 128) * 3.0).cpu().numpy()
strain_xx = np.gradient(displacement, axis=1).astype(np.float32)
gallery_images = [img_hrtem, img_haadf, img_diff, img_phase, img_darkfield, strain_xx]
gallery_labels = ["HRTEM", "HAADF", "Diffraction", "Phase Contrast", "Dark Field", "Strain (exx)"]
Show2D(gallery_images, labels=gallery_labels, title="EM Image Gallery", ncols=3)
[6]:
4. Gallery from 3D Array — Focal Series#
Simulated focal series: the same crystal structure at different defocus values, modeled by varying the CTF envelope width.
[7]:
n_focal = 6
size = 128
y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
# Base lattice with multiple periodicities
base_lattice = (
torch.cos(2 * np.pi * 0.08 * x)
+ torch.cos(2 * np.pi * 0.06 * (x * np.cos(np.pi / 3) + y * np.sin(np.pi / 3)))
)
focal_series = np.zeros((n_focal, size, size), dtype=np.float32)
defocus_values = np.linspace(-60, 60, n_focal)
for i, defocus in enumerate(defocus_values):
# CTF-like transfer: defocus changes envelope width
sigma_env = max(20, 60 - abs(defocus) * 0.5)
env = torch.exp(-((x - size // 2)**2 + (y - size // 2)**2) / (2 * sigma_env**2))
# Phase shift from defocus
phase_shift = defocus * 0.02
img = base_lattice * np.cos(phase_shift) * env
noise = torch.from_numpy(np.random.normal(0, 0.2, (size, size)).astype(np.float32)).to(device)
img = img + noise
focal_series[i] = img.cpu().numpy()
focal_labels = [f"Defocus {d:.0f} nm" for d in defocus_values]
Show2D(focal_series, labels=focal_labels, title="Focal Series", ncols=3, cmap="gray")
[7]:
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]:
8. FFT — Gallery#
Multiple lattice orientations with different spatial frequencies. Click each to see its FFT showing different reciprocal lattice spots.
[11]:
size = 192
y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
fft_gallery = []
fft_labels = []
orientations = [
(0.05, 0.0, "Horizontal 0.05"),
(0.08, np.pi / 4, "45 deg 0.08"),
(0.12, np.pi / 6, "30 deg 0.12"),
(0.06, np.pi / 2, "Vertical 0.06"),
]
for freq, angle, label in orientations:
lattice = torch.cos(2 * np.pi * freq * (x * np.cos(angle) + y * np.sin(angle)))
noise = torch.from_numpy(np.random.normal(0, 0.2, (size, size)).astype(np.float32)).to(device)
lattice = lattice + noise
fft_gallery.append(lattice.cpu().numpy())
fft_labels.append(label)
Show2D(fft_gallery, labels=fft_labels, title="Lattice Orientations + FFT", show_fft=True, cmap="gray", ncols=2)
[11]:
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)