pyextal.acom.solve¶
Solving the detector mapping and the orientation behind it.
The mapping from a reflection to its pixel is linear:
pixel = center + (1 / pixel_size) * Rot(theta) * [x, y]
so once a handful of reflections are identified the whole thing is a least-squares problem. Getting those first identifications is the only awkward part, and is done by matching pairs of peaks: the ratio of two |g| values and the angle between them are independent of both pixel_size and theta, which turns a four-parameter search into a table lookup.
Attributes¶
Classes¶
Linear map from in-plane reciprocal coordinates to detector pixels. |
Functions¶
|
Length ratios and included angles for every ordered pair of vectors. |
|
Votes for a detector mapping by matching pairs of peaks to the library. |
|
Assigns each detected peak the nearest predicted reflection. |
|
Least-squares fit of the detector mapping to assigned peaks. |
|
Scores how well a mapping explains the pattern. |
|
Finds the detector mapping that best explains a set of detected peaks. |
Module Contents¶
- pyextal.acom.solve.EVIDENCE_PRIOR = 6.0¶
- class pyextal.acom.solve.DetectorMap¶
Linear map from in-plane reciprocal coordinates to detector pixels.
- Variables:
center (np.ndarray) – (2,) direct-beam pixel position.
pixel_size (float) – Inverse Angstroms per pixel.
theta (float) – Rotation from the library’s x axis to the detector, radians.
handed (int) – +1, or -1 if the pattern is mirrored relative to the library frame.
rms (float) – Residual of the fit, in pixels.
n_used (int) – Reflections the fit was based on.
- center: numpy.ndarray¶
- pixel_size: float¶
- theta: float¶
- handed: int = 1¶
- rms: float¶
- n_used: int = 0¶
- to_pixels(xy)¶
Maps in-plane coordinates (inverse Angstroms) to pixels.
- Parameters:
xy (np.ndarray) – (n, 2) in-plane coordinates.
- Returns:
(n, 2) pixel positions.
- Return type:
np.ndarray
- to_reciprocal(pixels)¶
Maps pixels back to in-plane coordinates (inverse Angstroms).
- Parameters:
pixels (np.ndarray) – (n, 2) pixel positions.
- Returns:
(n, 2) in-plane coordinates.
- Return type:
np.ndarray
- pyextal.acom.solve._pair_features(vectors)¶
Length ratios and included angles for every ordered pair of vectors.
- pyextal.acom.solve.match_pairs(peak_vectors, lib, ratio_tol=0.02, angle_tol=np.deg2rad(2.0), max_peaks=40, handed=None)¶
Votes for a detector mapping by matching pairs of peaks to the library.
- Parameters:
peak_vectors (np.ndarray) – (n, 2) detected peak positions relative to the direct beam, in pixels.
lib (ReflectionLibrary) – Candidate reflections.
ratio_tol (float, optional) – Fractional tolerance on |g| ratios. Defaults to 0.02.
angle_tol (float, optional) – Tolerance on the included angle, radians. Defaults to 2 degrees.
max_peaks (int, optional) – Use only the this many peaks closest to the direct beam; pair matching is quadratic. Defaults to 40.
handed (int, optional) – Restrict proposals to
+1or-1. Both are tried when None, which is the default.
- Returns:
Candidate mappings, best first. Each holds
pixel_size,theta,handedandvotes.- Return type:
list[dict]
Note
The two handedness families are often exactly degenerate. A projected zone-axis net is centrosymmetric, so a mirrored mapping reproduces every peak position to the same residual; on both SrTiO3 datasets here the two best scores agreed to 0.0000. Which one wins is then decided by candidate ordering, and it can differ between two scans of the same crystal on the same detector – the mirror sends
(h, k, 0)to(h, -k, 0), so the labels differ while the positions do not.Handedness is a property of the instrument, not of the specimen tilt. Determine it once, then pass it here. Note that
pyextal.acom.resolve_label_sign()does not cover this: it testsgagainst-g, an inversion, which is a different degeneracy.
- pyextal.acom.solve.assign(peak_vectors, lib, mapping, tol_px=3.0)¶
Assigns each detected peak the nearest predicted reflection.
- Parameters:
peak_vectors (np.ndarray) – (n, 2) peak positions relative to the direct beam, in pixels.
lib (ReflectionLibrary) – Candidate reflections.
mapping (DetectorMap) – Mapping to test.
tol_px (float, optional) – Largest accepted mismatch, in pixels. Defaults to 3.0.
- Returns:
Indices into peak_vectors and into lib, for peaks that found a reflection within tol_px.
- Return type:
tuple[np.ndarray, np.ndarray]
- pyextal.acom.solve.refine(peaks, lib, peak_idx, lib_idx, handed=1)¶
Least-squares fit of the detector mapping to assigned peaks.
The mapping is linear in its parameters once the assignment is fixed, so this is a direct solve rather than an iterative search.
- Parameters:
peaks (np.ndarray) – (n, 2) absolute peak positions, in pixels.
lib (ReflectionLibrary) – Candidate reflections.
peak_idx (np.ndarray) – Indices into peaks.
lib_idx (np.ndarray) – Matching indices into lib.
handed (int, optional) – +1, or -1 for a mirrored pattern. Defaults to 1.
- Returns:
The fitted mapping, with residual RMS in pixels.
- Return type:
- Raises:
ValueError – If fewer than two reflections were assigned.
- pyextal.acom.solve.score_mapping(peak_vectors, lib, mapping, shape, center, tol_px=3.0, strong_quantile=0.6, intensities=None, strong_rel=0.1)¶
Scores how well a mapping explains the pattern.
Residual alone cannot separate a correct solution from a symmetry-related one: on a square lattice a mirrored, sqrt(2)-rescaled mapping fits the matched peaks just as tightly. What separates them is the reflections the mapping predicts but the image does not show. So the score rewards explaining detected peaks and penalises strong predicted reflections that sit on empty frame.
- Parameters:
peak_vectors (np.ndarray) – (n, 2) peak positions relative to the direct beam.
lib (ReflectionLibrary) – Candidate reflections.
mapping (DetectorMap) – Mapping to score.
shape (tuple[int, int]) – Detector shape.
center (np.ndarray) – (2,) direct-beam pixel position.
tol_px (float, optional) – Match radius, pixels. Defaults to 3.0.
strong_quantile (float, optional) – Reflections above this quantile of |U_g| are the ones expected to be visible. Defaults to 0.6.
intensities (np.ndarray, optional) – (n,) peak heights. Supplying them adds the sharpest discriminator available – see below.
strong_rel (float, optional) – A reflection counts as strong when its |U_g| exceeds this fraction of the strongest. Defaults to 0.1.
- Returns:
scoreplus the parts it is built from.- Return type:
dict
Note
Each fraction is weighted by the evidence behind it,
n / (n + EVIDENCE_PRIOR). Without that, explained_strong and intensity_on_strong are computed over the candidate’s own predictions and a zone axis that predicts almost nothing scores a perfect 1.0 on both. That is not hypothetical: on the SrTiO3 dataset taken 4 degrees off zone, [001] matched 17 of 20 detected peaks at the right scale and scored 1.44, while (5,-12,-1) matched 3 peaks at a scale 2.2x too large and scored 2.03.matched_peaks is biased towards mappings with too fine a scale: a smaller pixel_size puts more library reflections inside the same pixel radius, so more peaks find something to match. On SrTiO3 [001] a mapping wrong by a factor of sqrt(2) matched every peak while the correct one matched 61%. What separates them is intensity_on_strong: the wrong mapping put more than half the observed brightness onto near-extinct reflections, which cannot happen. matched_peaks is therefore carried at half weight.
- pyextal.acom.solve.solve_orientation(peaks, center, lib, shape, tol_px=3.0, n_candidates=12, intensities=None, pixel_size=None, pixel_tol=0.02, **match_kw)¶
Finds the detector mapping that best explains a set of detected peaks.
- Parameters:
peaks (np.ndarray) – (n, 2) absolute peak positions, in pixels.
center (array-like) – (2,) direct-beam pixel position.
lib (ReflectionLibrary) – Candidate reflections for one zone axis.
shape (tuple[int, int]) – Detector shape.
tol_px (float, optional) – Match radius, pixels. Defaults to 3.0.
n_candidates (int, optional) – Pair-matching proposals to refine and score. Defaults to 12.
intensities (np.ndarray, optional) – (n,) peak heights, used to check that bright peaks land on reflections that can be bright.
pixel_size (float, optional) – Known detector scale, inverse Angstroms per pixel, from an earlier solve on the same camera length. Mappings further than pixel_tol from it are discarded, both as proposals and after the fit. Free when None, the default.
pixel_tol (float, optional) – Fractional tolerance on pixel_size. Defaults to 0.02.
**match_kw – Passed to
match_pairs()–handed=among them.
- Returns:
Best mapping and its score, or
(None, None)if nothing could be fitted. The score carrieshanded_marginandhanded_degenerate: when the mirror is degenerate the choice of handedness came from candidate ordering, not from the pattern, and two scans of the same crystal can disagree. Pin it withhanded=once it is known.pixel_size exists for the same reason. Peak positions alone admit nets related by a scale of sqrt(2): on the SrTiO3 scan taken 7 degrees off [001] a blind search found (2,-3,-3) and (1,-6,-1) at 1.41x the true scale, scoring above [001]. The camera length does not change between scans, so a scale fitted once on an on-zone pattern removes that branch.
- Return type:
tuple[DetectorMap, dict] | tuple[None, None]