ShowDiffraction#

Open in Colab

ShowDiffraction is an interactive d-spacing analyzer for 2D diffraction patterns and 3D pattern stacks. It accepts a NumPy array, a PyTorch tensor, or a quantem Dataset. Auto runs center refinement, ring detection, phase calibration, fitting, and indexing, and Phase selects a library or custom phase.

Quickstart#

One command takes a pattern to an analyzed, shareable page:

# In Colab, run the installation cell below before using the CLI.
quantem showdiffraction pattern.npy --phase Au   # your data: center, rings, calibration, hkl
quantem showdiffraction --demo                   # Fe3O4 SAED from the public hub, when uploaded

The deterministic spot and ring patterns below keep the core workflow executable without a network connection. The detection-denoise comparison later uses the public Fe3O4 nanoparticle SAED fixture when it is available and reports when it uses the deterministic magnetite control instead; measured diffraction arrays use the same API.

# @title Install QuantEM { display-mode: "form" }
if get_ipython().__class__.__module__.startswith("google.colab"):
    !wget -q https://raw.githubusercontent.com/electronmicroscopy/quantem.widget/main/scripts/install_colab.py -O install_quantem.py
    %run install_quantem.py

from quantem.widget import ShowDiffraction

Why ShowDiffraction#

Diffraction analysis is usually split across tools: hand measurement in an image viewer, a separate desktop program for indexing, a script for distortion or profiles. ShowDiffraction is the complete workflow in one widget that runs wherever Python runs, including Colab, and exports a self-contained HTML page that opens in any browser, including on a phone.

  • One-click pipeline: Auto chains center refinement, ring detection, phase calibration, profile fitting, and hkl indexing; every step is also a plain Python call.

  • Exact crystallography: d-spacings and interplanar angles from the full metric tensor for any crystal system, systematic absences for the common structure types, and a phase library of 100+ standards with cited lattice constants.

  • Honest identification: candidates are ranked by plain facts (matched lines, mean Δd, missing strong lines), and ranking stays a verification aid; nothing is applied silently.

  • Distortion-aware: a fitted elliptical distortion corrects every radius, profile, and calibration rather than being a separate pass.

  • Reproducible: JSON state save/load, CSV/JSON measurement tables, and the same analysis from the notebook, the exported HTML, or the quantem CLI.

import numpy as np

from quantem.widget import ShowDiffraction
from quantem.widget import Phase

rng = np.random.default_rng(0)
size = 256
center = (size - 1) / 2
rows, cols = np.mgrid[0:size, 0:size]


def bragg_lattice(rotation_deg=0.0, spacing_px=28.0):
    angle = np.deg2rad(rotation_deg)
    cos_a, sin_a = np.cos(angle), np.sin(angle)
    pattern = np.zeros((size, size), np.float32)
    for h in range(-4, 5):
        for k in range(-4, 5):
            spot_row = center + (h * cos_a - k * sin_a) * spacing_px
            spot_col = center + (h * sin_a + k * cos_a) * spacing_px
            amplitude = 6.0 if h == 0 and k == 0 else 1.0 / (1 + 0.4 * (h * h + k * k))
            pattern += amplitude * np.exp(-((rows - spot_row) ** 2 + (cols - spot_col) ** 2) / 8.0)
    return rng.poisson(1000.0 * (pattern + 0.05)).astype(np.float32)  # shot noise


single_crystal = bragg_lattice()

size_m = 512
center_m = (size_m - 1) / 2
m_rows, m_cols = np.mgrid[0:size_m, 0:size_m]
m_radius = np.hypot(m_rows - center_m, m_cols - center_m)
magnetite_pattern = 4.0 * np.exp(-(m_radius ** 2) / 80.0)
k_synth = 0.004
for d_ref, strength, width in [
    (2.967, 1.7, 3.0),
    (2.532, 1.3, 3.5),
    (2.099, 1.0, 4.0),
    (1.715, 0.8, 4.5),
    (1.485, 0.55, 5.0),
]:
    ring_radius = 1.0 / (d_ref * k_synth)
    magnetite_pattern += strength * np.exp(-((m_radius - ring_radius) ** 2) / (2 * width ** 2))
magnetite_pattern = rng.poisson(100.0 * (magnetite_pattern + 0.05)).astype(np.float32)

Single-crystal spots#

Use Spots or click reflections. Add a custom cubic phase, then Index Spots to fill hkl and zone axis.

saed = ShowDiffraction(
    single_crystal,
    center=(center, center),
    bf_radius=14,
    k_pixel_size=0.018,
    title="Single-crystal SAED",
    offline=True,
    verbose=False,
)
saed.detect_spots(max_spots=12)
saed.custom_phases = [{"name": "Cubic", "a": 1.984, "absences": "none"}]
saed.phase_name = "Cubic"
saed.index_spots(Phase.from_cubic("Cubic", 1.984, absences="none"))
saed

Polycrystalline rings#

Pick a phase, then press Auto. Profile, Azim, Mask View, and Quality in the side menu show the radial profile, azimuthal intensity, excluded-region overlay, and quality checks. Fit refines ring radius and width, Fit Ellipse measures distortion, and Identify ranks phase candidates; use element filters when chemistry is known.

magnetite = ShowDiffraction(
    magnetite_pattern,
    title="Magnetite-like rings",
    offline=True,
    verbose=False,
)
magnetite.phase_name = "Fe3O4"
magnetite.run_auto(max_rings=5)
magnetite.dp_colormap = "viridis"
magnetite

Verify the phase#

Identification works best as verification: rank the phases you expect with identify_phase (build candidates with library_phase or Phase.from_cubic, or pick them in the Phase menu and flip candidates only for Identify). Library-wide search_phases is the fallback when nothing is expected; filter by chemistry such as Fe, O. Five rings can keep related spinels close in the ranking.

from quantem.widget import library_phase

expected = [library_phase(n) for n in ("Fe3O4", "γ-Fe2O3", "α-Fe2O3 (hematite)", "α-Fe")]
verified = magnetite.identify_phase(expected)

for candidate in verified:
    mean_err = candidate["mean_err"]
    error_text = "n/a" if mean_err is None else f"{100 * mean_err:.2f}%"
    missing = candidate["n_missing_strong"]
    print(
        f"{candidate['name']}: {candidate['matched']}/{candidate['n_obs']} lines, "
        f"mean Δd {error_text}, "
        f"missing strong {'n/a' if missing is None else missing}"
    )

magnetite.identify_elements = "Fe, O"
candidates = magnetite.search_phases()
print(candidates[0]["name"])
Fe3O4: 6/6 lines, mean Δd 0.24%, missing strong n/a
γ-Fe2O3: 6/6 lines, mean Δd 0.73%, missing strong n/a
α-Fe2O3 (hematite): 4/6 lines, mean Δd 0.67%, missing strong n/a
α-Fe: 0/6 lines, mean Δd n/a, missing strong n/a
Fe3O4

Detection denoise#

The public Fe3O4 SAED fixture is used when available; otherwise this page reports that it is using the deterministic magnetite control generated above. The source is thinned to a median of 0.5 counts per pixel and analyzed twice side by side. Detection and Auto run on a matched-filter denoised view (detect_denoise, default auto; Anscombe on counting data) while ring fits and measurements stay on the raw counts. The right panel also sets the display-only denoise = "nlm" so the pattern itself is readable; show_detection_view shows what detection saw instead.

from ipywidgets import HBox, Layout

from quantem.widget.data import showdiffraction_fe3o4

try:
    fe3o4_saed = showdiffraction_fe3o4(verbose=False)
except FileNotFoundError:
    fe3o4_saed = magnetite_pattern
    source_label = "deterministic magnetite control"
else:
    source_label = "public Fe3O4 SAED fixture"
print(f"Detection-denoise source: {source_label}")

scale = 0.5 / np.median(fe3o4_saed[fe3o4_saed > 0])
sparse_saed = np.random.default_rng(2).poisson(np.clip(fe3o4_saed, 0, None) * scale).astype(np.float32)


def low_dose_auto(mode, title):
    w = ShowDiffraction(sparse_saed, title=title, offline=True, verbose=False, panel_width_px=430)
    w.detect_denoise = mode
    if mode != "none":
        w.denoise = "nlm"
    w.phase_name = "Fe3O4"
    w.run_auto()
    indexed = sum(1 for r in w.rings if r.get("hkl"))
    print(f"{title}: calibration rms {w.calibration_rms_px:.2f} px, {indexed}/{len(w.rings)} rings indexed")
    return w


raw = low_dose_auto("none", "raw")
denoised = low_dose_auto("auto", "denoised")
HBox([raw, denoised], layout=Layout(overflow="auto"))
Detection-denoise source: deterministic magnetite control
raw: calibration rms 0.57 px, 6/6 rings indexed
denoised: calibration rms 0.57 px, 6/6 rings indexed

Save#

save writes JSON state. measurements_from_state rebuilds the table. export_html writes a standalone page.

magnetite.summary()
magnetite.save("magnetite_state.json")

ShowDiffraction.measurements_from_state("magnetite_state.json")[:2]

# HTML export
export_path = saed.export_html("showdiffraction_saed.html", title="Single-crystal SAED")
export_path.name
Magnetite-like rings
Frames:   1 (showing #0)
Detector: 512x512 (0.0040 1/Å/px)
Calibration: phase (rms 0.31 px)
Center:   (255.5, 255.5)  BF r=13.1 px
Spots:    0
Rings:    6
Phase:    Fe3O4: 6/6 lines; next: γ-Fe2O3 (also 6/6), α-Fe2O3 (hematite)
Display:  viridis | log
'showdiffraction_saed.html'