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#

A few lines take a pattern to an analyzed, shareable page:

from quantem.widget import ShowDiffraction, library_phase, read_image

widget = ShowDiffraction(read_image("pattern.tif").array)
widget.run_auto(library_phase("Au"))   # center, rings, calibration, hkl
widget.export_html("pattern_showdiffraction.html")

The deterministic spot and ring patterns below keep the core workflow executable without a network connection. The optional measured Fe3O4 SAED fixture is not available in the pinned public data revision. The detection-denoise comparison therefore reports and uses the deterministic magnetite control; this is synthetic data, not a measured Fe3O4 pattern. Your measured 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

import quantem.widget as qw
qw.profile()

from quantem.widget import ShowDiffraction
quantem.widget  0.0.1rc44.dev0
  install       editable checkout
  commit        2d009a7f61ee (dirty checkout)
quantem.gpu     0.0.1rc18.dev0
  install       local build
  commit        not recorded
quantem         0.1.10
  install       installed package
  commit        not recorded
webgpu kernels  fa4a86c (fa4a86cfee85), pinned fa4a86cfee85ea719be2dd98206d2098f0dd3344
torch           2.14.1+cu130  device=cpu
python          3.12.15

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 quantem.widget as qw
qw.profile()

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)
quantem.widget  0.0.1rc44.dev0
  install       editable checkout
  commit        2d009a7f61ee (dirty checkout)
quantem.gpu     0.0.1rc18.dev0
  install       local build
  commit        not recorded
quantem         0.1.10
  install       installed package
  commit        not recorded
webgpu kernels  fa4a86c (fa4a86cfee85), pinned fa4a86cfee85ea719be2dd98206d2098f0dd3344
torch           2.14.1+cu130  device=cpu
python          3.12.15

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,
    bright_field_center_px=(center, center),
    bright_field_radius_px=14,
    detector_sampling_inv_A=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
quantem.widget: device="auto" selected cpu.

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: 5/5 lines, mean Δd 0.04%, missing strong n/a
γ-Fe2O3: 5/5 lines, mean Δd 0.57%, missing strong n/a
α-Fe2O3 (hematite): 4/5 lines, mean Δd 0.69%, missing strong n/a
α-Fe: 0/5 lines, mean Δd n/a, missing strong n/a
Fe3O4

Detection denoise#

The pinned public data revision has no measured Fe3O4 SAED fixture, so this page reports that it is using the deterministic magnetite control generated above. A future supplied measured fixture may use the same optional helper. 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.

from ipywidgets import HBox, Layout

fe3o4_saed = magnetite_pattern
print("Detection-denoise source: deterministic synthetic magnetite control")

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 synthetic magnetite control
raw: calibration rms 0.07 px, 5/5 rings indexed
denoised: calibration rms 0.07 px, 5/5 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.05 px)
Center:   (255.5, 255.5)  BF r=13.1 px
Spots:    0
Rings:    5
Phase:    Fe3O4: 5/5 lines; next: γ-Fe2O3 (also 5/5), α-Fe2O3 (hematite)
Display:  viridis | log
'showdiffraction_saed.html'