Skip to content

Add orientation and phase mapping, Bloch-wave dynamical diffraction, calibration, strain and DDF - #297

Open
cophus wants to merge 35 commits into
electronmicroscopy:devfrom
cophus:acom
Open

cophus wants to merge 35 commits into
electronmicroscopy:devfrom
cophus:acom

Conversation

@cophus

@cophus cophus commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What problem this PR addresses

This PR adds automated crystal orientation mapping (ACOM) and phase mapping of 4D-STEM data to quantem.diffraction, along with the crystallography, calibration, disk detection and dynamical diffraction code these methods depend on. It also includes the digital dark field (DDF) analysis from Ian MacLaren (PR #286), and a reverse Monte Carlo (RMC) fit of diffuse scattering. The full test suite (1061 tests) passes in about 2 minutes.

Tutorial notebooks:

diffraction_simulation.ipynb
ACOM_orientation_phase_strain_Ti.ipynb
ACOM_dynamical_refinement_Ti.ipynb

The new modules in quantem.diffraction are:

  • crystal.py: Crystal wraps an ase.Atoms cell from a CIF file, finds the space group and Laue class with spglib, and computes kinematical structure factors from the Lobato and Van Dyck electron scattering factors. We use the absorptive Weickenmeier-Kohl factors (wk_scattering_factors.py) for Bloch-wave calculations. A pseudo_symmetry_tol lets the matching use a higher pseudo-symmetry than the exact space group.
  • rotations.py: quaternions in torch, scalar first, rotating crystal to lab coordinates, with the zone axis pointing toward the source.
  • disk_detection.py, bragg_vectors.py, strain.py: template matching disk detection, BraggVectors on the torch-backed Vector, lattice fitting and StrainMap.
  • calibration.py: origins, scan rotation from the curl of the centre of mass, pixel size, and elliptic distortion. calibrate() runs these steps and returns a DiffractionCalibration, which can be applied to peaks, rebinned, saved and reloaded.
  • illumination.py: excitation error envelopes for convergent beams and precession, and relrod factors.
  • orientation.py: OrientationMap matches every measured pattern against a library of simulated zone axis patterns using a sparse polar correlation (cosine similarity from 0 to 1), then refines each orientation by least squares on the paired peaks. Reliability is the margin between the best and second-best match outside a 15 degree exclusion ball. Overlapping grains are matched by deflation (num_matches, suppress_matched, match_residual).
  • phase.py and crystal_map.py: PhaseMap assigns the phase at each position by non-negative least squares over candidate subsets, with a complexity penalty and a null hypothesis for vacuum and amorphous regions. CrystalMap runs this workflow for several crystals and provides masks, phase fractions, and orientation, pole figure, correlation and match plots.
  • bloch.py: Bloch-wave dynamical diffraction in torch, including thickness refinement, CBED, LACBED and Kossel patterns, Kossel reference patterns, and refine_dynamical, which refines tilt, thickness and in-plane strain at every position against the measured Bragg intensities.
  • digital_dark_field.py: virtual apertures, polar and radial DDF images, and clustering of Bragg peaks into grain maps, ported from Ian MacLaren's py4DSTEM functions.
  • reverse_monte_carlo.py: ReverseMonteCarlo fits a supercell with chemical order and static displacements to the diffuse scattering measured along several zone axes.

We also made the following changes to shared core code:

  • read_4dstem reads 3D stacks as 4D data, and resolves the RosettaSciIO plugin from a plugin name or file extension. Extensions shared by several plugins (such as .h5) now raise an error listing the plugins.
  • New Polar4dstem, Dataset4dstem.polar_transform, Dataset4dstem.from_file, show_virtual_images and show_virtual_detectors.
  • AutoSerialize saves torch.device and ase.Atoms (including partial occupancy), and the new Bundle saves several objects in one file.
  • DBSCAN clustering of Vector peaks in core/utils/clustering.py.
  • show_2d colorbars use set_powerlimits((-3, 4)), so values from 1e-3 to 1e4 are printed without a shared exponent. This changes the appearance of existing colorbars.
  • New dependencies: ase>=3.23 and spglib>=2.5.

What should the reviewer(s) do

Please review and merge. Three points need attention from the reviewers:

  • PR Adding various strain mapping algorithms #284 (strain) added the same five disk detection and strain files independently. We merged the newer Adding various strain mapping algorithms #284 versions into this branch, keeping the g1/g2 names, the detector-to-scan rotation (q_to_r_rotation_ccw_deg, q_transpose), GPU caching and the CPU template average from Adding various strain mapping algorithms #284, and the correlation options and origin calibration from this branch. Two choices differ from Adding various strain mapping algorithms #284. Disk detection keeps subtract_mean=False, because the ACOM detection thresholds assume a positive template. Shear and rotation follow the corrected sign convention of Adding various strain mapping algorithms #284, so e_rc and phi from OrientationMap.calculate_strain change sign relative to earlier versions of this branch. Adding various strain mapping algorithms #284 can be rebased onto this PR with mostly formatting conflicts.

  • PR DDF and clustering updates #286 (DDF) is merged here, and the DDF function fit_lattice is renamed to refine_lattice_vectors to avoid a clash with BraggVectors.fit_lattice.

  • The diffraction simulator widget developed on this branch has moved to the quantem.widget repository, so this PR contains no widget code.

  • This PR introduces a public-facing change (e.g., figures, CLI input/output, API).

    • For functional and algorithmic changes, tests are written or updated.
    • Documentation (e.g., tutorials, examples, README) has been updated.
    • A tracking issue or plan to update documentation exists.

Generated with Claude Code

cophus and others added 30 commits August 30, 2026 16:54
Some corrections to the new DDF and clustering routines.  "noise" was renamed to "unclustered".  An additional function added to put grain labels into a vector.  The dbscan was rewritten with
help from Claude to eliminate some inefficiencies that made it slow on CPU. An updated ipynb will be sent to accompany this PR.
Brings in the torch Vector (electronmicroscopy#272), spectroscopy, em-database and the
widget package removal.

- pyproject.toml, quantem/__init__.py: keep both sides (ase, spglib,
  em-database; diffraction and spectroscopy).
- file_readers.py: keep acom's 3D-as-4D reading, add dev's
  _print_available_datasets and read_3d_spectroscopy, restore the
  scan_length/scan_axis/transpose_scan_axes parameters an earlier merge
  dropped from the signature, and remove conflict markers committed in
  the read_4dstem docstring.
- widget/: follow dev's removal except the diffraction widget (diffsim)
  and the files it needs to build; restore the widget .gitignore rules.
- uv.lock regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read Vector data with .numpy().astype(np.float64) instead of .array and
  flatten(), which now return tensors. This keeps the old writable float64
  arrays, so consumers mixing peaks with float64 tensors also work for
  float32 Vectors, and torch.as_tensor no longer warns about read-only input.
- Detected peaks stay float64 (dtype=torch.float64); Vectors derived from
  other Vectors (calibrated peaks, residual peaks, masked copies) keep the
  source dtype.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
float32 holds peak positions to ~3e-5 px, well below detection precision,
and every read casts to float64 before computing, so the float64 storage
only doubled memory and file size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pdates from Ian MacLaren

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port aperture_array_generator, aperture_array_subtract, DDFimage,
pointlist_to_array (rphi), DDF_radial_image and DDFradialazimuthimage
from py4DSTEM into digital_dark_field.py as aperture_array,
aperture_array_subtract, aperture_ddf_image, add_polar_fields,
polar_mask and radial_ddf_image, all reading peaks from a Vector.
Add ddf_image(peaks, mask) and plot_apertures. Fix assign_grain_labels
and plot_cluster_scatter from PR electronmicroscopy#286 for the torch-backed Vector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cophus
cophus requested review from mitis1 and smribet October 6, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants