pyextal.acom.quality ==================== .. py:module:: pyextal.acom.quality .. autoapi-nested-parse:: Judging whether a reflection carries signal inside the chosen ROI. Indexing says where a reflection *is*; it says nothing about whether the data there is worth refining against. A reflection can be correctly indexed and still be nothing but noise over the sampled region -- too weak, or clipped by the detector edge -- and scoring it only adds noise to the goodness-of-fit while costing a template correlation every iteration. The measure has to suit how LARBED rocking curves actually look. A weak reflection is not weak everywhere: it is dark over most of the rocking range and lights up where it is dynamically excited. Background subtraction in `IntegrateLarbed` then clips the dark part to zero, so a *typical* pixel of a weak-but-real reflection reads zero. On SrTiO3 [001] the (100) family comes out of the extraction 69-82% zeroed while still peaking near the mean of (200) -- judging it on a median would throw away exactly the reflections worth refining. So the test is whether a reasonable *fraction* of the ROI rises above the noise, not whether the typical point does. Attributes ---------- .. autoapisummary:: pyextal.acom.quality.LABEL_OPS Functions --------- .. autoapisummary:: pyextal.acom.quality.reflection_snr pyextal.acom.quality.prune_noisy pyextal.acom.quality.apply_label_op pyextal.acom.quality.resolve_label_symmetry pyextal.acom.quality.resolve_label_sign Module Contents --------------- .. py:function:: reflection_snr(roi, min_snr=3.0) Per-reflection signal statistics over the ROI. :param roi: An ROI with `selectROI` already called. :type roi: LARBEDROI :param min_snr: The per-point threshold that counts as signal. Defaults to 3.0. :type min_snr: float, optional :returns: ``(snr_high, fraction, valid)`` -- the 90th-percentile of ``I / sqrt(sigma^2)``, the fraction of points at or above `min_snr`, and the fraction of the ROI that landed on measured data, one entry per reflection. :rtype: tuple[np.ndarray, np.ndarray, np.ndarray] :raises AttributeError: If the ROI carries no variance, i.e. it is not a LARBEDROI. .. py:function:: prune_noisy(roi, min_snr=3.0, min_fraction=0.1, min_valid_fraction=0.9, always_keep=((0, 0, 0), )) Splits the ROI's reflections into those worth refining and those that are not. :param roi: An ROI with `selectROI` already called. :type roi: LARBEDROI :param min_snr: Per-point signal-to-noise counting as signal. Defaults to 3.0. :type min_snr: float, optional :param min_fraction: Fraction of the ROI that must reach `min_snr` for the reflection to be kept. Defaults to 0.1 -- low on purpose, because a weak reflection is only excited over part of the rocking range. :type min_fraction: float, optional :param min_valid_fraction: Fraction of the ROI that must land on measured data. Defaults to 0.9. :type min_valid_fraction: float, optional :param always_keep: Reflections kept whatever their score. The direct beam is kept by default: it anchors the intensity scaling and the engine's output list has to start with it. :type always_keep: tuple, optional :returns: ``(keep, drop, stats)``. `stats` maps each reflection to ``(snr_high, fraction, valid_fraction)``. :rtype: tuple[list, list, dict] .. py:data:: LABEL_OPS .. py:function:: apply_label_op(larbed, op) Relabels an extraction in place by an integer operation on (h, k, l). Only the labels move: the rocking curves and their detector positions stay where they are, so nothing has to be re-integrated. :param larbed: Extraction whose `g_vectors` and `grid` to relabel. :type larbed: LARBEDAnalysis :param op: 3x3 integer matrix, applied as ``g -> op @ g``. :type op: array-like :returns: `larbed`, relabelled. :rtype: LARBEDAnalysis .. py:function:: resolve_label_symmetry(larbed, dat_path, thickness, gl, rotation, region, n_beams=5, ops=None, gx=None, thickness_scan=None, ncores=1, plot=True, verbose=True) Decides which symmetry-related labeling of the extracted reflections fits the dynamics. Peak positions fix the reflection net only up to its own point group. On a square (hk0) net that is 4mm: the labels can come out rotated by 90 degrees, mirrored (h,k) -> (h,-k), or inverted, and every candidate puts reflections on exactly the same spots with the same kinematic strengths. Dynamical diffraction does tell them apart. With the `.dat` orientation and the ROI rotation held fixed, each candidate pairs every measured rocking curve with a different simulated reflection, and only the right pairing reproduces the structure *inside* each disk. How it is scored matters, and is why this does not simply run `CoarseOptimize` per candidate. `CoarseOptimize` correlates the whole (beam, y, x) stack as one normalised block, so its score is dominated by the brightness *differences between* beams -- (000) bright, {200} medium, {100} dark -- and a relabeling within a family leaves those untouched: on SrTiO3 every candidate scored 0.88-0.91 at the same tilt. Here instead: * the simulation is run once. It depends on the `.dat`, not on the labels, so one Bloch run serves every candidate and a thickness scan; * the tilt offset is fixed by the (000) disk alone, which no relabeling touches, so a wrong labeling cannot move the ROI to suit itself; * each of the other disks is correlated on its own, normalised on its own, at that offset, and a candidate's score is the mean over those disks. Hold `rotation` at the instrument value. A 90-degree label rotation fits just as well as a 90-degree rotation of the ROI with the labels left as they are, so letting both vary makes the answer arbitrary. :param larbed: Extraction holding `Store_Larbed`, `g_vectors` and the propagated variance. :type larbed: LARBEDAnalysis :param dat_path: The `.dat` describing this orientation. :type dat_path: str | Path :param thickness: Starting thickness estimate, Angstroms. :type thickness: float :param gl: Tilt step size. :type gl: float :param rotation: ROI rotation, degrees. :type rotation: float :param region: The `selectROI` region to score over. It must be rectangular, as for `CoarseOptimize`; a large one carrying plenty of in-disk contrast separates the candidates best. :type region: np.ndarray :param n_beams: Strongest reflections to test, besides the direct beam. Defaults to 5. :type n_beams: int, optional :param ops: Name -> 3x3 integer matrix to try. Defaults to :data:`LABEL_OPS`. An op that sends more than half the measured reflections outside the measured set is dropped: it is not a positional ambiguity of this net (off zone, the 90-degree turns about [001] are not). :type ops: dict, optional :param gx: Horizontal reference reflection for the ROI. :type gx: array-like, optional :param thickness_scan: Thicknesses to try, Angstroms. Defaults to 0.7-1.3 x `thickness` in 13 steps; each candidate keeps its best. :type thickness_scan: array-like, optional :param ncores: Unused; kept for call compatibility. :type ncores: int, optional :param plot: Show measured disks beside the simulated disk each of the best candidates pairs them with. Defaults to True. :type plot: bool, optional :param verbose: Report. Defaults to True. :type verbose: bool, optional :returns: ``(name, op, scores)`` -- the best operation's name and matrix, and the score each candidate reached. Apply the winner with :func:`apply_label_op`. :rtype: tuple[str, np.ndarray, dict] :raises ValueError: If no op maps the measured reflections onto themselves, or no reflection's images under every remaining op are in the `.dat` output block. .. note:: This does not call `terminate()`: it deallocates the engine's arrays, and a later `CoarseOptimize` then simulates as all zeros. .. py:function:: resolve_label_sign(larbed, dat_path, thickness, gl, rotation, region, n_beams=6, gx=None, ncores=1, verbose=True) Decides whether the extracted labels or their negatives fit the dynamics. The g/-g special case of :func:`resolve_label_symmetry`: for a centrosymmetric crystal Friedel's law gives |U_g| = |U_-g|, so peak positions cannot separate the two, but their rocking curves differ away from exact zone-axis incidence. :param As for :func:`resolve_label_symmetry`: :param with `n_beams` defaulting to 6.: :returns: ``(sign, scores)`` -- ``+1`` to keep the labels as they are, ``-1`` to negate every one of them; `scores` is keyed by sign. :rtype: tuple[int, dict]