pyextal.acom ============ .. py:module:: pyextal.acom .. autoapi-nested-parse:: Automated crystal orientation mapping for indexed diffraction patterns. Determines the orientation of an averaged diffraction pattern from its Bragg peak positions, predicts every reflection the detector should see -- including weak ones no peak finder would pick up -- and writes a `.dat` ready for structure-factor refinement. The search is kinematic: peak positions follow from the metric tensor alone, so a candidate orientation costs microseconds rather than the ~10 s a `bloch_run` takes. The Bloch engine is used once, on the winner, to confirm it. .. rubric:: Example >>> from pyextal.callBloch import bloch_parse >>> from pyextal.acom import auto_index >>> param = bloch_parse("STO_001.dat") >>> res = auto_index(larbed.average_aligned, param, center=(61.35, 61.5)) >>> res.zone, round(res.mapping.rms, 3) ((0, 0, 1), 0.18) >>> res.write_dat("STO_001.dat", "STO_001_acom.dat") .. note:: Peak positions fix the orientation only up to the Laue symmetry of the zone. On a cubic [001] pattern the solutions at theta, theta+90 and their mirrors are all exact, and every reflection they assign is symmetry-equivalent, so they are interchangeable for structure-factor refinement. Resolving the absolute setting needs intensities -- that is what `confirm` is for. Submodules ---------- .. toctree:: :maxdepth: 1 /autoapi/pyextal/acom/datfile/index /autoapi/pyextal/acom/detect/index /autoapi/pyextal/acom/geometry/index /autoapi/pyextal/acom/library/index /autoapi/pyextal/acom/plotting/index /autoapi/pyextal/acom/quality/index /autoapi/pyextal/acom/solve/index /autoapi/pyextal/acom/tilt/index Attributes ---------- .. autoapisummary:: pyextal.acom.LABEL_OPS Classes ------- .. autoapisummary:: pyextal.acom.ReflectionLibrary pyextal.acom.TiltResult pyextal.acom.DetectorMap pyextal.acom.AcomResult Functions --------- .. autoapisummary:: pyextal.acom.min_laue_max pyextal.acom.nearest_zone_axis pyextal.acom.build_library pyextal.acom.candidate_zone_axes pyextal.acom.visible_holz pyextal.acom.frame_gmax pyextal.acom.auto_max_index pyextal.acom.detect_peaks pyextal.acom.direct_beam pyextal.acom.wide_dat pyextal.acom.tilt_from_zero_beam pyextal.acom.beam_direction pyextal.acom.higher_order_zone pyextal.acom.reindex_at_zone pyextal.acom.write_tilted_dat pyextal.acom.predict_excited pyextal.acom.fit_beam_map pyextal.acom.net_zone pyextal.acom.fit_beam_to_holz pyextal.acom.relabel_holz pyextal.acom.solve_orientation pyextal.acom.assign pyextal.acom.write_dat pyextal.acom.write_oriented_dat pyextal.acom.read_dat_beams pyextal.acom.plot_zone_map pyextal.acom.plot_indexed pyextal.acom.reflection_snr pyextal.acom.prune_noisy pyextal.acom.resolve_label_sign pyextal.acom.resolve_label_symmetry pyextal.acom.apply_label_op pyextal.acom.auto_index pyextal.acom.confirm Package Contents ---------------- .. py:function:: min_laue_max(param, zone, gmax, max_index=6, cover=0.8, limit=40) Laue zones a zone axis needs before its library forms a 2D net. A reflection's Laue index is ``n = g . [uvw]``. For a low-index axis the reflections inside a frame nearly all have ``n = 0``, so the ZOLZ alone is a 2D net and ``laue_max=0`` is right. For a high-index axis they are spread over many zones and the ZOLZ collapses: the [1,1,15] ZOLZ of SrTiO3 inside 1.2 A^-1 holds seven reflections, every one on the single row ``(n,-n,0)``. Fitting a 2D net to a 1D row is what produced a scale 1.4x too small and indexed (200) as (1,-1,0). This returns the smallest ``laue_max`` whose library spans two dimensions out to `cover` of the requested `gmax`. Counting instead to ``gmax * |[uvw]|`` -- the largest Laue index any reflection inside `gmax` can carry -- gives 71 here rather than 6, because it counts reflections lying nearly along the zone axis, which project onto the pattern centre and form no part of the net. :param param: Parsed simulation parameters. :type param: SimParams :param zone: (3,) zone axis, integer Miller indices. :type zone: array-like :param gmax: Reflection library extent, A^-1. :type gmax: float :param max_index: Miller index bound. Defaults to 6. :type max_index: int, optional :param cover: Fraction of `gmax` both in-plane directions must reach. Defaults to 0.8. :type cover: float, optional :param limit: Give up above this. Defaults to 40. :type limit: int, optional :returns: Smallest workable ``laue_max``, or `limit` if none was found. :rtype: int .. note:: Raising `laue_max` weakens the zone-axis search in exchange. The ZOLZ restriction is what makes different axes predict different nets; a library carrying every Laue zone projects to something dense enough to match almost anything. Raise it to test a specific high-index axis against a solution you already have, not to widen a blind search. .. py:function:: nearest_zone_axis(param, direction, max_index=20, tol_deg=None) Rational zone axes closest to a beam direction. A pattern taken off zone has a beam direction that is not, in general, along any lattice row. Naming a high-index axis for it means finding the rational directions that come closest -- and there are usually several at nearly the same angle, so this returns a ranked list rather than an answer. :param param: Parsed simulation parameters. :type param: SimParams :param direction: (3,) beam direction in crystal coordinates, as ``zone`` tilted by the refined ``tiltX``/``tiltY``. Need not be normalised or rational. :type direction: array-like :param max_index: Largest index to enumerate. Defaults to 20. :type max_index: int, optional :param tol_deg: Keep only axes within this angle. All are ranked when None. :type tol_deg: float, optional :returns: ``(uvw, angle_deg)``, closest first. :rtype: list[tuple] .. note:: This is bookkeeping, not a measurement. The projected net of a tilted pattern is the net of the *low-index* axis it is near, and every rational direction close to that axis reproduces the same reflections with the same Miller indices -- verified on the SrTiO3 scan taken 4 degrees off [001], where [1,1,15] with enough Laue zones labelled all 23 shared peaks exactly as [001] did. What the geometry cannot do is say which of them the beam was along. That comes from excitation -- a visible HOLZ ring, or the tilt that :class:`pyextal.optimize.CoarseOptimize` refines against the dynamical simulation. .. py:class:: ReflectionLibrary Reflections of one zone axis, projected onto the detector plane. :ivar zone: The zone axis, direct-space indices. :vartype zone: tuple[int, int, int] :ivar xaxis: Reflection defining the in-plane x axis. :vartype xaxis: tuple[int, int, int] :ivar hkl: (n, 3) int Miller indices. :vartype hkl: np.ndarray :ivar xy: (n, 2) in-plane coordinates, inverse Angstroms. :vartype xy: np.ndarray :ivar gmag: (n,) |g| in inverse Angstroms. :vartype gmag: np.ndarray :ivar laue: (n,) Laue zone index, ``g . zone``; 0 is ZOLZ. :vartype laue: np.ndarray :ivar weight: (n,) kinematic strength |U_g|, 0 for the direct beam. :vartype weight: np.ndarray :ivar e1: In-plane x unit vector, Cartesian. :vartype e1: np.ndarray :ivar e2: In-plane y unit vector, Cartesian. :vartype e2: np.ndarray :ivar n: Zone-axis unit vector, Cartesian. :vartype n: np.ndarray .. py:attribute:: zone :type: tuple .. py:attribute:: xaxis :type: tuple .. py:attribute:: hkl :type: numpy.ndarray .. py:attribute:: xy :type: numpy.ndarray .. py:attribute:: gmag :type: numpy.ndarray .. py:attribute:: laue :type: numpy.ndarray .. py:attribute:: weight :type: numpy.ndarray .. py:attribute:: e1 :type: numpy.ndarray .. py:attribute:: e2 :type: numpy.ndarray .. py:attribute:: n :type: numpy.ndarray .. py:method:: __len__() .. py:method:: select(mask) Returns a new library keeping only the reflections `mask` selects. .. py:method:: strong(rel=1e-06) The subset whose kinematic weight is worth matching against. Extinct and near-extinct reflections are dropped: they may or may not be visible depending on the multiple scattering, so requiring a peak at one, or allowing one to explain a peak, both mislead the orientation search. The direct beam is always kept. :param rel: Keep |U_g| above this fraction of the strongest. Defaults to 1e-6. :type rel: float, optional :returns: The filtered view. :rtype: ReflectionLibrary .. py:property:: zolz Only the zero-order Laue zone. :type: ReflectionLibrary .. py:function:: build_library(param, zone, gmax=2.0, max_index=8, laue_max=0, xaxis=None, weights=True, min_weight_rel=0.0) Builds the reflection library for one zone axis. :param param: Parsed simulation parameters; supplies the metric tensors and |K|. :type param: SimParams :param zone: Zone axis, direct-space indices. :type zone: tuple[int, int, int] :param gmax: Largest |g| to include, inverse Angstroms. Defaults to 2.0. :type gmax: float, optional :param max_index: Search box for Miller indices. Defaults to 8. :type max_index: int, optional :param laue_max: Highest Laue zone to include. 0 keeps the ZOLZ only; 1 adds FOLZ, and so on. Defaults to 0. :type laue_max: int, optional :param xaxis: Reflection defining the in-plane x axis. The shortest in-plane reflection is chosen when None; the choice only sets where the fitted rotation angle is measured from. :type xaxis: tuple[int, int, int], optional :param weights: Look up kinematic |U_g| for each reflection. Defaults to True. :type weights: bool, optional :param min_weight_rel: Drop reflections whose |U_g| falls below this fraction of the strongest. Defaults to 0.0, i.e. keep everything. In electron diffraction a kinematically forbidden reflection still carries intensity, because multiple scattering routes electrons into it -- SrTiO3 (100) is nearly extinct kinematically yet plainly visible. Such reflections therefore have to be predicted and integrated. They are useless for *finding* the orientation, though, since whether they appear is not something the kinematic weight can say; use :meth:`ReflectionLibrary.strong` for the matching subset. :type min_weight_rel: float, optional :returns: Reflections with |g| <= `gmax` in the kept Laue zones. :rtype: ReflectionLibrary :raises ValueError: If the zone axis admits no in-plane reflection within `gmax`. .. py:function:: candidate_zone_axes(max_index=3) Enumerates primitive low-index zone axes. Sign-equivalent axes ([uvw] and [-u-v-w]) give the same pattern, so only one of each pair is returned. Symmetry-equivalent axes are *not* removed: they score identically, and that degeneracy is a real property of the measurement worth reporting rather than hiding. :param max_index: Largest absolute index considered. Defaults to 3. :type max_index: int, optional :returns: Candidate zone axes, shortest first. :rtype: list[tuple[int, int, int]] .. py:function:: visible_holz(param, lib, mapping, shape, smax=0.05, laue_max=3, gmax=None, max_index=8) Finds higher-order-Laue-zone reflections that land on the detector. Run this once the ZOLZ orientation is settled. A HOLZ reflection is kept when its predicted pixel falls inside the frame and, if `smax` is given, when its excitation error is small enough for it to be excited at all. :param param: Parsed simulation parameters. :type param: SimParams :param lib: The solved ZOLZ library; supplies zone and axes. :type lib: ReflectionLibrary :param mapping: The fitted detector mapping. :type mapping: DetectorMap :param shape: Detector shape (n_x, n_y). :type shape: tuple[int, int] :param smax: Largest |s_g| accepted, inverse Angstroms. Defaults to 0.05. Passing None drops the filter, which is rarely what you want: a reflection can project onto the frame while sitting far off the Ewald sphere, so the unfiltered count is dominated by beams that are never excited. For a zone axis the first Laue ring sits at roughly sqrt(2 |K| H), H being the reciprocal layer spacing -- if that exceeds the detector's reach, no HOLZ is excited at all. :type smax: float, optional :param laue_max: Highest Laue zone to consider. Defaults to 3. :type laue_max: int, optional :param gmax: Largest |g|; defaults to what the frame can hold. :type gmax: float, optional :param max_index: Search box for Miller indices. Defaults to 8. :type max_index: int, optional :returns: The HOLZ reflections predicted to be on the frame. :rtype: ReflectionLibrary .. py:function:: frame_gmax(mapping, shape) Largest |g| the detector can reach, in inverse Angstroms. :param mapping: The fitted detector mapping. :type mapping: DetectorMap :param shape: Detector shape. :type shape: tuple[int, int] :returns: Distance from the direct beam to the furthest frame corner. :rtype: float .. py:function:: auto_max_index(gmx, gmax) Miller search box big enough to reach `gmax`. A reflection (h00) has |g| = h / a, so the box has to grow with the longest cell edge. Too small a box silently truncates the outer reflections. :param gmx: (3, 3) direct-space metric tensor. :type gmx: np.ndarray :param gmax: Largest |g| wanted, inverse Angstroms. :type gmax: float :returns: Half-width of the index box. :rtype: int .. py:function:: detect_peaks(image, min_distance=4, threshold_rel=0.02, num_peaks=200, refine=True, crop=5) Finds Bragg peaks and refines them to sub-pixel positions. :param image: 2D averaged diffraction pattern. :type image: np.ndarray :param min_distance: Smallest separation between peaks, in pixels. Defaults to 4. :type min_distance: int, optional :param threshold_rel: Peak height floor as a fraction of the brightest pixel. Defaults to 0.02. :type threshold_rel: float, optional :param num_peaks: Most peaks to return. Defaults to 200. :type num_peaks: int, optional :param refine: Fit a 2D Gaussian to each peak for sub-pixel accuracy. Defaults to True. :type refine: bool, optional :param crop: Half-width of the patch fitted per peak. Defaults to 5. :type crop: int, optional :returns: ``(positions, intensities)`` -- (n, 2) float pixel coordinates in (x, y) = (row, col) detector convention, and the peak height at each. :rtype: tuple[np.ndarray, np.ndarray] .. py:function:: direct_beam(positions, intensities, hint=None, search=8.0) Picks the direct beam from a set of detected peaks. :param positions: (n, 2) peak positions. :type positions: np.ndarray :param intensities: (n,) peak heights. :type intensities: np.ndarray :param hint: Approximate position; the nearest peak within `search` pixels is taken. The brightest peak is used when None. :type hint: tuple[float, float], optional :param search: Radius around `hint`, in pixels. Defaults to 8. :type search: float, optional :returns: Index into `positions`, and the position itself. :rtype: tuple[int, np.ndarray] :raises ValueError: If no peak was found, or none lies near `hint`. .. py:class:: TiltResult Where the scan sits in K_t space, relative to the zone axis. :ivar offset: (2,) scan-step offset of the scan centre from the zone axis. This is the measurement; it does not depend on `gl`. :vartype offset: np.ndarray :ivar tilt: (2,) the same offset in degrees, which does. :vartype tilt: np.ndarray :ivar rotation: Scan-to-simulation rotation, degrees. :vartype rotation: float :ivar thickness: Thickness the match was found at, Angstroms. :vartype thickness: float :ivar correlation: Peak cross-correlation. :vartype correlation: float :ivar significance: Peak over ``mean + 3 sigma`` of the correlation. :vartype significance: float :ivar on_edge: The peak reached the edge of the simulated range, so the true optimum lies outside it and the answer is a lower bound. Raise `conv` and re-run. :vartype on_edge: bool :ivar gl: The `gl` used for the degree conversion. :vartype gl: float :ivar half_angle: Half-width of the simulated K_t range, degrees. :vartype half_angle: float :ivar start_tilt: (2,) tilt the search started from, degrees. :vartype start_tilt: np.ndarray :ivar rotation_scan: Best correlation at each rotation tried. :vartype rotation_scan: dict .. py:attribute:: offset :type: numpy.ndarray .. py:attribute:: tilt :type: numpy.ndarray .. py:attribute:: rotation :type: float .. py:attribute:: thickness :type: float .. py:attribute:: correlation :type: float .. py:attribute:: significance :type: float .. py:attribute:: on_edge :type: bool .. py:attribute:: gl :type: float .. py:attribute:: half_angle :type: float .. py:attribute:: start_tilt :type: numpy.ndarray .. py:attribute:: rotation_scan :type: dict .. py:property:: magnitude Tilt magnitude, degrees. :type: float .. py:property:: steps Offset magnitude, scan steps. :type: float .. py:property:: correction (2,) how far the search moved from `start_tilt`, degrees. A correction much larger than the field is a sign the estimate was not close enough to help; one near zero means the estimate was already right, not that the search did nothing. :type: np.ndarray .. py:method:: gl_for(angle) The `gl` that would make this offset a given angle. :param angle: Known tilt magnitude, degrees -- from the goniometer. :type angle: float :returns: The implied `gl`. :rtype: float .. note:: The inverse of the `gl` dependence, and the more useful direction when the specimen tilt is known but the tilt step size is not. .. py:method:: __str__() .. py:function:: wide_dat(template, dst, conv, gl, samp=None, zoom=TARGET_ZOOM, out_beam=(0, 0, 0), verbose_cost=True) Clones an explicit-beam `.dat` with a wide convergence for this alignment. :param template: A `.dat` with explicit `hkl` and `out` blocks -- what :func:`pyextal.acom.write_dat` produces. The beam list is carried over unchanged, which is the point: it must not grow with `conv`. :type template: str | Path :param dst: Where to write. :type dst: str | Path :param conv: Convergence semi-angle, in units of ``|g_xaxis| / |K|``. :type conv: float :param gl: Tilt step size, needed to choose `samp`. :type gl: float :param samp: Sampling. Derived from `zoom` when None. :type samp: int, optional :param zoom: Simulation-to-experiment tilt step ratio to aim for. Defaults to :data:`TARGET_ZOOM`. :type zoom: float, optional :param out_beam: The single output reflection. Defaults to the direct beam, which is all this alignment uses and which keeps the stored eigenvectors small. :type out_beam: tuple, optional :param verbose_cost: Print the tilt count and memory estimate. Defaults to True. :type verbose_cost: bool, optional :returns: `dst`. :rtype: Path :raises ValueError: If the template has no explicit beam block. .. py:function:: tilt_from_zero_beam(dp, gindex, dat, thickness, gl, rotation, region, gx=None, variance=None, start_tilt=(0.0, 0.0), ncores=1, verbose=True) Coarse-refines the direct beam alone to find where the scan sits in K_t. Runs the ordinary coarse refinement -- :class:`pyextal.optimize.CoarseOptimize` with ``gInclude = [(0, 0, 0)]`` -- against a wide-field `.dat`, so rotation, `gl`, thickness and the tilt bookkeeping all come from the code path the rest of the workflow uses. What makes it work on an off-zone scan is the `.dat`: the simulated K_t range has to be wider than the scan plus the offset, or the correlation peak has nowhere to land. See :func:`wide_dat`. :param dp: (nbeam, ny, nx) integrated rocking curves, from ``LARBEDAnalysis.stack()``. :type dp: np.ndarray :param gindex: (nbeam, 3) Miller indices matching `dp`. :type gindex: array-like :param dat: Wide-field `.dat` from :func:`wide_dat`. :type dat: str | Path :param thickness: Starting thickness, Angstroms. :type thickness: float :param gl: Tilt step size, in scan steps per ``|g_xaxis|``. :type gl: float :param rotation: Scan-to-simulation rotation, degrees. :type rotation: float :param region: `selectROI` region covering the rocking curve. :type region: np.ndarray :param gx: Horizontal reference reflection. :type gx: array-like, optional :param variance: Matching variance maps. :type variance: np.ndarray, optional :param start_tilt: (2,) estimate of the tilt off the zone axis, degrees, in the same convention the result reports. The simulation is centred there instead of on the zone axis, so the correlation only has to find what is left. Defaults to (0, 0). :type start_tilt: tuple, optional :param ncores: Cores for the simulation. Defaults to 1. :type ncores: int, optional :param verbose: Report progress. Defaults to True. :type verbose: bool, optional :returns: The scan centre relative to the zone axis. :rtype: TiltResult .. note:: The tilt is read from ``dinfo.tiltX``/``dinfo.tiltY``, which :meth:`~pyextal.optimize.CoarseOptimize.optimizeOrientationThickness` accumulates -- so it carries the engine's sign convention already and needs no reinterpretation here. Check :attr:`TiltResult.on_edge`. A correlation peak against the edge of the simulated range means the true optimum lies outside it, and the answer is a lower bound; raise `conv` in :func:`wide_dat` and re-run. `start_tilt` is the cheaper way out of that. The simulated range has to cover the scan *plus* the offset, and its cost grows as the square of the range at fixed sampling -- covering 4 degrees on this instrument needs ``conv >= 11`` and tens of thousands of tilts. Seeding the search with an estimate leaves only the residual to find, so a narrower `conv` suffices and the peak lands near the middle of the field rather than against its edge. A goniometer reading is usually good enough; so is the answer from a previous scan of the same specimen. The estimate is applied to the simulation itself, through the same ``bloch_parse(dat, tiltX, tiltY)`` the engine uses, so :attr:`TiltResult.tilt` is always the **total** tilt off the zone axis -- seed included -- and never something the caller has to add the seed back onto. :attr:`TiltResult.correction` reports what the search contributed. .. py:function:: beam_direction(param, tilt, zone=None, handed=1) The incident direction a tilt corresponds to, in crystal coordinates. :param param: Parsed simulation parameters. :type param: SimParams :param tilt: (2,) tilt along the pattern x and y axes, degrees -- :attr:`TiltResult.tilt`. :type tilt: array-like :param zone: Zone axis. Read from the parsed geometry (``blochwave.difpar.zone``) when None. :type zone: array-like, optional :param handed: ``-1`` if the scan's second axis runs opposite to ``gh``, which negates that component of the tilt. Defaults to +1. :type handed: int, optional :returns: (3,) direction in direct-space (uvw) components, normalised so the largest component is 1. :rtype: np.ndarray .. note:: ``param.tilt0`` is the in-plane *deviation* from the zone axis, not the incident wavevector -- it is ``[0, 0, 0]`` for ``kt 0 0 0``. The wavevector has to be built from the zone axis and the deviation added to it; starting from ``tilt0`` alone yields a purely in-plane direction with no component along the beam at all. The wavevector lives in reciprocal space, so converting it to a lattice direction goes through the metric: ``uvw = gmxr @ K``. Feed the result to :func:`pyextal.acom.library.nearest_zone_axis` to name it, bearing in mind that many high-index axes sit within a fraction of a degree of one another, so the ranking is only ever as sharp as the tilt is. `handed` exists because the tilt that best aligns the simulation to the experiment is not automatically the crystal's tilt: if the scan's second axis runs opposite to ``gh``, the two differ by a mirror. Round-tripping a known tilt through the simulation recovers it exactly (applied ``(0, +2.00)`` came back as ``(-0.005, +1.992)``), so the arithmetic is not at fault -- the mirror sits between the scan and ``gh`` and has to be supplied. On the SrTiO3 scan taken off zone it is ``-1``: taken at face value the tilt names ``(0,-1,15)``, which refines to a thickness of 122 Angstrom against 578 for the same crystal on zone, while the mirrored reading names ``(0,1,15)`` and refines to 517. .. py:function:: higher_order_zone(param, tilt, max_index=20, tol_deg=None, handed=1, verbose=True) Names the beam direction as a rational zone axis. :param param: Parsed simulation parameters. :type param: SimParams :param tilt: (2,) tilt in degrees, from :attr:`TiltResult.tilt`. :type tilt: array-like :param max_index: Largest index to consider. Defaults to 20. :type max_index: int, optional :param tol_deg: Only accept an axis within this angle. :type tol_deg: float, optional :param handed: Passed to :func:`beam_direction`; ``-1`` when the scan's second axis runs opposite to ``gh``. Defaults to +1. :type handed: int, optional :param verbose: Print the ranked candidates. Defaults to True. :type verbose: bool, optional :returns: ``(uvw, angle_deg, ranked)`` -- the closest rational axis, how far the beam is from it, and the ranked list behind the choice. :rtype: tuple :raises ValueError: If nothing lies within `tol_deg`. .. note:: The ranking is dense: a beam 4 degrees off [001] toward [110] has (1,1,20) at 0.045 degrees, (1,1,19) at 0.257 and (1,1,18) at 0.492, and they do not all select the same beams. The choice is only as good as the tilt, which is why `max_index` is worth keeping modest -- a very high index buys an axis closer in angle without buying more physics. .. py:function:: reindex_at_zone(image, param, zone, center, gmax=1.2, max_index=6, handed=None, laue_max=None, verbose=True, **kw) Re-solves the pattern with the beam along a higher-order zone axis. The point is the beam list. A zone axis selects which reflections the engine treats as excited, and that selection follows the *zone*, not ``kt``: on SrTiO3, moving ``kt`` to ``0.05 0.05 0`` leaves the 661-beam list identical, while changing the zone from [001] to [1,1,20] returns 544 beams of which 152 appear in neither. Re-solving here is what brings the ZOLZ and HOLZ of the real tilted condition into the refinement. :param image: The averaged pattern, as passed to :func:`pyextal.acom.auto_index`. :type image: np.ndarray :param param: Parsed simulation parameters. :type param: SimParams :param zone: The higher-order zone axis. :type zone: array-like :param center: Direct-beam pixel hint. :type center: tuple :param gmax: Library extent. Defaults to 1.2. :type gmax: float, optional :param max_index: Miller index bound. Defaults to 6. :type max_index: int, optional :param handed: Pin the handedness, as for `auto_index`. :type handed: int, optional :param laue_max: Laue zones in the library. Sized with :func:`pyextal.acom.library.min_laue_max` when None, which is essential here: a high-index ZOLZ alone is a single row of reflections and cannot index a 2D net. :type laue_max: int, optional :param verbose: Report. Defaults to True. :type verbose: bool, optional :param \*\*kw: Passed to :func:`pyextal.acom.auto_index`. :returns: Indexed at the new orientation, ready for ``to_grid`` and ``write_dat``. :rtype: AcomResult .. py:function:: write_tilted_dat(template, dst, zone, out_beams, xaxis=None, kt=(0, 0, 0), conv=None, samp=None, verbose=True) Writes the refinement `.dat` for a tilted orientation. The two blocks come from different places, and that is the point: * ``out`` is what the experiment measured -- the reflections that fall on the detector and have integrated rocking curves. It cannot be widened without integrating more. * ``hkl`` is the matrix, and should hold every beam the tilted condition excites, on the detector or not. That list comes from the **engine's own selection at the new zone axis**, which is the only thing that responds to the orientation: moving ``kt`` leaves the selection untouched, while changing the zone from [001] to [0,-1,15] on SrTiO3 swaps 240 beams out and 187 in, 119 of them with ``l != 0``. Those are the HOLZ reflections that come into excitation when the beam tilts off the low-order axis, and a library built from the detector image can never contain them -- it only holds what lands on the frame. :param template: A `.dat` for this crystal whose beam block is still implicit (``sele``), so the engine re-selects at the new zone. :type template: str | Path :param dst: Where to write. :type dst: str | Path :param zone: The tilted zone axis. :type zone: array-like :param out_beams: Reflections with integrated rocking curves. :type out_beams: list :param xaxis: Horizontal reference. Kept from `template` when None -- worth keeping, since `gl` is expressed in units of it and changing it rescales `gl` proportionally. :type xaxis: tuple, optional :param kt: Residual tilt. Defaults to (0, 0, 0). :type kt: tuple, optional :param conv: Convergence. Kept from `template` when None. :type conv: float, optional :param samp: Sampling. Kept from `template` when None. :type samp: int, optional :param verbose: Report the beam counts. Defaults to True. :type verbose: bool, optional :returns: `dst`. :rtype: Path .. py:function:: predict_excited(param, beam, zone, xaxis, mapping, shape, laue_max=3, smax=0.02, scan_half=0.0, gmax=None, max_index=None) Reflections on the frame, with how close the scan brings each to Bragg. :param param: Parsed simulation parameters. :type param: SimParams :param beam: (3,) beam direction in uvw, as from :func:`beam_direction`. :type beam: array-like :param zone: Zone axis the Laue index is counted against. :type zone: array-like :param xaxis: In-plane reference reflection of `zone`. :type xaxis: array-like :param mapping: Detector map in the beam-normal frame, as fitted by :func:`fit_beam_map`. :type mapping: DetectorMap :param shape: Detector shape. :type shape: tuple[int, int] :param laue_max: Highest |Laue index| considered. Defaults to 3. :type laue_max: int, optional :param smax: Excitation error, A^-1, below which a reflection counts as excited. Defaults to 0.02. :type smax: float, optional :param scan_half: Half-range of the rocking scan, radians. A tilt d changes s_g by about |g_t| d, so a reflection is excited somewhere in the scan if ``|s_g| - |g_t| scan_half <= smax``. Defaults to 0, the scan centre only. :type scan_half: float, optional :param gmax: Largest |g|; the frame's reach when None. :type gmax: float, optional :param max_index: Miller search box; sized from `gmax` when None. :type max_index: int, optional :returns: ``hkl`` (n, 3), ``pixels`` (n, 2), ``laue`` (n,), ``s0`` (n,) the excitation error at the scan centre, ``s_reach`` (n,) the smallest |s_g| the scan reaches, and ``excited`` (n,) bool. :rtype: dict .. py:function:: fit_beam_map(hkl, pixels, param, beam, zone, xaxis) Fits the detector map in the beam-normal frame from labelled positions. Both handednesses are fitted and the better kept, so labels that went through a mirror in :func:`pyextal.acom.resolve_label_symmetry` still fit. :param hkl: (n, 3) labels, ZOLZ of `zone`. :type hkl: array-like :param pixels: (n, 2) their detector positions. :type pixels: array-like :param param: Parsed simulation parameters. :type param: SimParams :param beam: (3,) beam direction in uvw. :type beam: array-like :param zone: Zone axis of the labels. :type zone: array-like :param xaxis: In-plane reference reflection of `zone`. :type xaxis: array-like :returns: The fitted map, ``rms`` in pixels. :rtype: DetectorMap .. py:function:: net_zone(labels, param, beam, max_index=6, max_angle=20.0, verbose=True) The low-index zone axis whose ZOLZ the indexed reflections lie in. :func:`fit_beam_to_holz` and :func:`relabel_holz` fit the detector map from the ZOLZ labels of `zone`, and count Laue zones from it. That has to be the net the pattern actually shows, which far enough off the nominal axis is a different one. SrTiO3 10 deg off [001] still shows the [001] net plus the l = +-1 bands; 15 deg off, the beam is 0.6 deg from [014], and every label the (0,5,19) re-index gives lies in the [014] ZOLZ while only the (h00) row lies in the [001] one. Fitting the map to that row is degenerate: it comes out at rms 0 with the wrong handedness, and every (hk0) node is then predicted on the frame. Candidates are the axes up to `max_index` within `max_angle` of the beam whose ZOLZ holds a 2D set of the labels; the one holding the most wins, the closer to the beam on a tie. The sign facing the beam is returned, so HOLZ layers count positive along it. :param labels: (n, 3) current labels. :type labels: array-like :param param: Parsed simulation parameters. :type param: SimParams :param beam: (3,) beam direction, uvw. :type beam: array-like :param max_index: Largest index tried. Defaults to 6. :type max_index: int, optional :param max_angle: Largest beam-to-axis angle, degrees. Defaults to 20. :type max_angle: float, optional :param verbose: Report. Defaults to True. :type verbose: bool, optional :returns: ``(uvw, angle_deg, n_zolz)``. :rtype: tuple .. py:function:: fit_beam_to_holz(peaks, hkl, pixels, param, beam, zone=(0, 0, 1), xaxis=None, shape=(124, 124), search_deg=4.0, step_deg=0.25, laue_max=3, smax=0.01, scan_half=0.0, node_tol=0.9, match_px=1.0, verbose=True) Refines the beam direction from where the HOLZ spots actually are. The (000) tilt search measures the offset in scan steps and turns it into an angle through `gl`, so it is only as good as `gl`. The HOLZ rings are not: where the Ewald sphere cuts the l = +-1 layers depends on the beam direction and |K| alone. On the SrTiO3 scan taken off [001] the (000) search gave 7.0 degrees while the two HOLZ bands sit where a 10.3 degree tilt puts them, matching 18 of 25 off-node peaks to within a pixel. Peaks further than `node_tol` from every ZOLZ node are taken as HOLZ spots. Beam directions on a grid around `beam` are scored by how many of them fall within `match_px` of an excited HOLZ prediction, ties going to fewer predictions with no peak nearby; the best is then refined on a finer grid. :param peaks: (n, 2) detected peak positions. :type peaks: array-like :param hkl: (m, 3) ZOLZ labels the map is fitted from. :type hkl: array-like :param pixels: (m, 2) their positions. :type pixels: array-like :param param: Parsed simulation parameters. :type param: SimParams :param beam: (3,) starting beam direction, uvw. :type beam: array-like :param zone: Zone axis of the net. Defaults to [001]. :type zone: array-like, optional :param xaxis: In-plane reference reflection; the shortest label in `hkl` when None. :type xaxis: array-like, optional :param shape: Detector shape. Defaults to (124, 124). :type shape: tuple, optional :param search_deg: Half-width of the search, degrees, in both directions normal to `beam`. Defaults to 4. :type search_deg: float, optional :param step_deg: Coarse grid step, degrees. Defaults to 0.25. :type step_deg: float, optional :param laue_max: As for :func:`predict_excited`. :param smax: As for :func:`predict_excited`. :param scan_half: As for :func:`predict_excited`. :param node_tol: Peaks closer than this to a ZOLZ node, pixels, are ZOLZ spots. Defaults to 0.9. :type node_tol: float, optional :param match_px: A HOLZ spot counts as explained within this many pixels. Defaults to 1.0. :type match_px: float, optional :param verbose: Report. Defaults to True. :type verbose: bool, optional :returns: ``(beam, info)`` -- the refined direction in uvw, scaled so its largest component is 1, and a dict with ``hits``, ``n_holz_peaks``, ``unexplained`` (excited predictions with no peak), ``offset_deg`` (shift from `beam` along the two beam-normal axes) and ``angle_deg``. `beam` is returned unchanged when fewer than four HOLZ spots are found. :rtype: tuple .. py:function:: relabel_holz(larbed, param, beam, zone=(0, 0, 1), xaxis=None, laue_max=3, smax=0.02, scan_half=0.0, min_sep=3.0, verbose=True) Relabels an extraction for the orientation found, picking up FOLZ and HOLZ. Off zone, the Ewald sphere cuts the upper layers close to the direct beam: 7-10 degrees off SrTiO3 [001] puts the l = +-1 rings 1.3-1.8 A^-1 out, inside the frame. Their reflections then sit on the detector as spots 1-2.5 px from ZOLZ nodes, and indexing on the projected net alone labels them as the ZOLZ reflection nearby. Which of the two a spot is cannot be decided by position; it is decided by which one the beam excites. So, with the beam direction known: 1. the detector map is refitted from the ZOLZ entries already in `larbed`, in the plane normal to the beam; 2. every reflection with ``|laue| <= laue_max`` on the frame is predicted, with the excitation error the scan brings it closest to; 3. ZOLZ reflections are all kept, as before, and HOLZ reflections are kept when excited somewhere in the scan; 4. where two predictions fall within `min_sep` pixels -- one spot on the detector -- the more strongly excited one keeps it. That is the relabel: a ZOLZ node that sits under an excited HOLZ reflection is renamed. Labels only; the positions are the predicted ones, as `to_grid` gives them, so the extraction has to be re-integrated afterwards. :param larbed: Extraction whose `grid` and `g_vectors` to replace. Its ZOLZ entries define the map, so run this after the label symmetry is settled. :type larbed: LARBEDAnalysis :param param: Parsed simulation parameters. :type param: SimParams :param beam: (3,) beam direction in uvw, in the frame of the current labels. :type beam: array-like :param zone: Zone axis of the net. Defaults to [001]. :type zone: array-like, optional :param xaxis: In-plane reference reflection; the shortest ZOLZ label present when None. :type xaxis: array-like, optional :param laue_max: Highest |Laue index| considered. Defaults to 3. :type laue_max: int, optional :param smax: Excitation error, A^-1, counting as excited. Defaults to 0.02. :type smax: float, optional :param scan_half: Half-range of the rocking scan, radians; see :func:`predict_excited`. Defaults to 0. :type scan_half: float, optional :param min_sep: Predictions closer than this, pixels, are one spot. Defaults to 3.0, the integration aperture. :type min_sep: float, optional :param verbose: Report. Defaults to True. :type verbose: bool, optional :returns: ``mapping`` (DetectorMap), ``added`` / ``removed`` label lists, ``renamed`` as ``(zolz, holz)`` pairs, and ``predicted`` from :func:`predict_excited`. :rtype: dict .. py:class:: DetectorMap Linear map from in-plane reciprocal coordinates to detector pixels. :ivar center: (2,) direct-beam pixel position. :vartype center: np.ndarray :ivar pixel_size: Inverse Angstroms per pixel. :vartype pixel_size: float :ivar theta: Rotation from the library's x axis to the detector, radians. :vartype theta: float :ivar handed: +1, or -1 if the pattern is mirrored relative to the library frame. :vartype handed: int :ivar rms: Residual of the fit, in pixels. :vartype rms: float :ivar n_used: Reflections the fit was based on. :vartype n_used: int .. py:attribute:: center :type: numpy.ndarray .. py:attribute:: pixel_size :type: float .. py:attribute:: theta :type: float .. py:attribute:: handed :type: int :value: 1 .. py:attribute:: rms :type: float .. py:attribute:: n_used :type: int :value: 0 .. py:method:: to_pixels(xy) Maps in-plane coordinates (inverse Angstroms) to pixels. :param xy: (n, 2) in-plane coordinates. :type xy: np.ndarray :returns: (n, 2) pixel positions. :rtype: np.ndarray .. py:method:: to_reciprocal(pixels) Maps pixels back to in-plane coordinates (inverse Angstroms). :param pixels: (n, 2) pixel positions. :type pixels: np.ndarray :returns: (n, 2) in-plane coordinates. :rtype: np.ndarray .. py:function:: 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. :param peaks: (n, 2) absolute peak positions, in pixels. :type peaks: np.ndarray :param center: (2,) direct-beam pixel position. :type center: array-like :param lib: Candidate reflections for one zone axis. :type lib: ReflectionLibrary :param shape: Detector shape. :type shape: tuple[int, int] :param tol_px: Match radius, pixels. Defaults to 3.0. :type tol_px: float, optional :param n_candidates: Pair-matching proposals to refine and score. Defaults to 12. :type n_candidates: int, optional :param intensities: (n,) peak heights, used to check that bright peaks land on reflections that can be bright. :type intensities: np.ndarray, optional :param pixel_size: 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. :type pixel_size: float, optional :param pixel_tol: Fractional tolerance on `pixel_size`. Defaults to 0.02. :type pixel_tol: float, optional :param \*\*match_kw: Passed to :func:`match_pairs` -- ``handed=`` among them. :returns: Best mapping and its score, or ``(None, None)`` if nothing could be fitted. The score carries ``handed_margin`` and ``handed_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 with ``handed=`` 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. :rtype: tuple[DetectorMap, dict] | tuple[None, None] .. py:function:: assign(peak_vectors, lib, mapping, tol_px=3.0) Assigns each detected peak the nearest predicted reflection. :param peak_vectors: (n, 2) peak positions relative to the direct beam, in pixels. :type peak_vectors: np.ndarray :param lib: Candidate reflections. :type lib: ReflectionLibrary :param mapping: Mapping to test. :type mapping: DetectorMap :param tol_px: Largest accepted mismatch, in pixels. Defaults to 3.0. :type tol_px: float, optional :returns: Indices into `peak_vectors` and into `lib`, for peaks that found a reflection within `tol_px`. :rtype: tuple[np.ndarray, np.ndarray] .. py:function:: write_dat(template, dst, zone, xaxis, beams, out_beams, kt=(0, 0, 0), norm=None, conv=None, samp=None, sgmax=None, sgmin=None, omeg=None, abs_=None) Writes a `.dat` for one orientation, with explicit beam and output blocks. The output beams are written as a **prefix** of the beam list, so ``hkl[:nout] == hklout`` and the engine's `idptr` mapping is the identity. That is not cosmetic: `bloch_parse` resolves each `out` entry against `hkl` and *appends* any it cannot find, writing past the array it already allocated from the declared beam count. :param template: Existing `.dat` supplying the crystal block. :type template: str | Path :param dst: File to write. :type dst: str | Path :param zone: Zone axis. :type zone: tuple[int, int, int] :param xaxis: Reflection defining the horizontal axis. :type xaxis: tuple[int, int, int] :param beams: (n, 3) every beam in the calculation. :type beams: array-like :param out_beams: (m, 3) reflections to output. :type out_beams: array-like :param kt: Tangential incident wavevector. Defaults to (0,0,0). :type kt: tuple, optional :param norm: Surface normal; `zone` is used when None. :type norm: tuple, optional :param conv: Convergence angle; the template's value is kept when None. :type conv: float, optional :param samp: Sampling; the template's value is kept when None. :type samp: int, optional :param sgmax: Maximum excitation error. Defaults to 3.5 when the template used `sele` and gave no standalone value. :type sgmax: float, optional :param sgmin: Minimum excitation error; template value kept when None. :type sgmin: float, optional :param omeg: Bethe parameter; template value kept when None. :type omeg: float, optional :param abs_: Mean absorption; template value kept when None. :type abs_: float, optional :returns: The file written. :rtype: Path :raises ValueError: If an output beam is missing from `beams`, or the template lacks a block that has to be replaced. .. py:function:: write_oriented_dat(template, dst, zone, xaxis, kt=(0, 0, 0), norm=None) Copies a `.dat`, changing only the orientation. The beam-selection blocks are left exactly as the template had them, so a template using `sele`/`aper` still selects its beams geometrically -- at the new orientation. That makes it the natural reference to compare an explicit ACOM beam list against: same crystal, same cutoffs, same engine, only the zone axis replaced. :param template: Existing `.dat`. :type template: str | Path :param dst: File to write. :type dst: str | Path :param zone: Zone axis. :type zone: tuple[int, int, int] :param xaxis: Reflection defining the horizontal axis. :type xaxis: tuple[int, int, int] :param kt: Tangential incident wavevector. Defaults to (0,0,0). :type kt: tuple, optional :param norm: Surface normal; `zone` is used when None. :type norm: tuple, optional :returns: The file written. :rtype: Path .. py:function:: read_dat_beams(path) Reads the explicit `hkl` and `out` blocks back out of a `.dat`. Lets downstream code take its reflection list from the file the engine will actually parse, rather than from a variable that may have drifted from it. :param path: A `.dat` with explicit blocks, as written by :func:`write_dat`. :type path: str | Path :returns: ``(beams, out_beams)`` as (h, k, l) tuples. :rtype: tuple[list, list] :raises ValueError: If either block is missing, or a declared count does not match the rows that follow it. .. py:function:: plot_zone_map(result, param, ax=None, annotate=6, cmap='viridis', show_all=False) Plots candidate zone axes on a symmetry-reduced stereographic map. Every zone axis in `result.ranking` is folded onto its symmetry representative and the best score of each group is kept, so a cubic crystal shows one point for <100> rather than three. Zones in `result.failed`, tried but left without a mapping, are drawn as grey crosses so the map shows every axis the search tested. A wide search packs its axes closely: to index 15 a cubic crystal gives 624 distinct orientations inside the fundamental triangle, and neighbouring high-index axes sit a fraction of a degree apart. The view is zoomed to the region the points occupy, and each marker is sized to its nearest neighbour, so close axes stay separate instead of hiding under one another. :param result: A solved orientation carrying `ranking`. :type result: AcomResult :param param: Parsed parameters, for the direct-space frame. :type param: SimParams :param ax: Axes to draw on; created if None. :type ax: matplotlib.axes.Axes, optional :param annotate: Label this many top groups. Defaults to 6. :type annotate: int, optional :param cmap: Colormap for the score. Defaults to 'viridis'. :type cmap: str, optional :param show_all: Draw every equivalent instead of reducing. Defaults to False. :type show_all: bool, optional :returns: ``(ax, groups)`` where `groups` maps representative zone to score. :rtype: tuple .. py:function:: plot_indexed(image, result, ax=None, max_label=12, vmax_pct=99.5, include_holz=True) Overlays the indexed reflections on the diffraction pattern. Predicted reflections are drawn whether or not a peak was detected there, which is the point: the weak ones are exactly what a peak finder misses and what the integration still needs. :param image: The averaged pattern that was indexed. :type image: np.ndarray :param result: The solved orientation. :type result: AcomResult :param ax: Axes to draw on. :type ax: matplotlib.axes.Axes, optional :param max_label: Label this many of the strongest predicted reflections. Defaults to 12. :type max_label: int, optional :param vmax_pct: Display ceiling percentile. Defaults to 99.5. :type vmax_pct: float, optional :param include_holz: Include HOLZ. Defaults to True. :type include_holz: bool, optional :returns: The axes drawn on. :rtype: matplotlib.axes.Axes .. 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: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] .. 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:: 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:data:: LABEL_OPS .. py:class:: AcomResult Outcome of an orientation solve. :ivar zone: The zone axis found. :vartype zone: tuple[int, int, int] :ivar mapping: Reflection-to-pixel mapping. :vartype mapping: DetectorMap :ivar lib: Reflections of the winning zone. :vartype lib: ReflectionLibrary :ivar score: Diagnostics from :func:`pyextal.acom.solve.score_mapping`. :vartype score: dict :ivar peaks: (n, 2) detected peak positions. :vartype peaks: np.ndarray :ivar peak_idx: Indices of peaks that were assigned. :vartype peak_idx: np.ndarray :ivar lib_idx: Matching indices into `lib`. :vartype lib_idx: np.ndarray :ivar shape: Detector shape. :vartype shape: tuple[int, int] :ivar ranking: ``(zone, score)`` for every zone tried, best first. :vartype ranking: list :ivar failed: Zones tried that gave no mapping, so carry no score. :vartype failed: list :ivar holz: HOLZ reflections predicted on the frame. :vartype holz: ReflectionLibrary | None .. py:attribute:: zone :type: tuple .. py:attribute:: mapping :type: solve.DetectorMap .. py:attribute:: lib :type: library.ReflectionLibrary .. py:attribute:: score :type: dict .. py:attribute:: peaks :type: numpy.ndarray .. py:attribute:: peak_idx :type: numpy.ndarray .. py:attribute:: lib_idx :type: numpy.ndarray .. py:attribute:: shape :type: tuple .. py:attribute:: ranking :type: list :value: [] .. py:attribute:: holz :type: library.ReflectionLibrary :value: None .. py:attribute:: failed :type: list :value: [] .. py:property:: assigned ``(hkl, pixel)`` for every assigned peak. :type: list[tuple] .. py:method:: predicted(include_holz=True, margin=0.0) Every reflection predicted to fall on the detector. This is the point of the exercise: reflections too weak to be detected are still predicted, so they can be integrated rather than missed. :param include_holz: Add the HOLZ reflections found by :meth:`add_holz`. Defaults to True. :type include_holz: bool, optional :param margin: Shrink the accepted frame by this many pixels on each side. Defaults to 0. :type margin: float, optional :returns: ``(hkl, pixels)``. :rtype: tuple[np.ndarray, np.ndarray] .. py:method:: invert() Flips the indexing through the direct beam, so g becomes -g. Applied when :func:`pyextal.acom.quality.resolve_label_sign` finds the labels inverted. It is done by turning the detector mapping through 180 degrees rather than by negating Miller indices: that keeps every library entry consistent with its own projection, and lands each reflection on the position its opposite used to hold. Positions are unchanged as a set; what changes is which reflection sits at each one. :returns: self, re-assigned in place. :rtype: AcomResult .. py:method:: add_holz(param, smax=0.05, laue_max=3, **kw) Finds HOLZ reflections predicted to land on the frame. Run once the ZOLZ orientation is settled; it does not change the mapping. :param param: Parsed simulation parameters. :type param: SimParams :param smax: Largest accepted |s_g|, inverse Angstroms. Defaults to 0.05; None drops the excitation check and returns every reflection that merely projects onto the frame. :type smax: float, optional :param laue_max: Highest Laue zone. Defaults to 3. :type laue_max: int, optional :param \*\*kw: Passed to :func:`pyextal.acom.library.visible_holz`. :returns: The HOLZ reflections found. :rtype: ReflectionLibrary .. py:method:: to_grid(include_holz=True) Returns the indexing in the form `LARBEDAnalysis` consumes. Assigning these to `grid` and `g_vectors` lets `IntegrateLarbed` run without any manual `assign_gvector` step. :param include_holz: Include HOLZ reflections. Defaults to True. :type include_holz: bool, optional :returns: ``(grid, g_vectors)`` -- grid entries are ``((i, j), (x, y))`` and g_vectors are ``(h, k, l)``. :rtype: tuple[list, list] .. py:method:: write_dat(template, dst, out_beams=None, include_holz=True, out_gmax=None, zone=None, xaxis=None, **kw) Writes a `.dat` for this orientation, ready for refinement. :param template: Existing `.dat` supplying the crystal block. :type template: str | Path :param dst: File to write. :type dst: str | Path :param out_beams: Reflections to output, i.e. the ones a refinement can touch. When None every predicted reflection out to `out_gmax` is used. :type out_beams: array-like, optional :param out_gmax: Radius of the output set, inverse Angstroms. Defaults to the largest |g| that was actually assigned, so the output covers the indexed part of the pattern. Weak and near-extinct reflections inside that radius are included deliberately: they carry intensity through multiple scattering, and they are often the most informative to refine, since a near-cancelling structure factor is sensitive to exactly the bonding charge one is trying to measure. :type out_gmax: float, optional :param include_holz: Include HOLZ in the beam list. Defaults to True. :type include_holz: bool, optional :param zone: Orientation written to the file, when it should differ from the one solved at. Off zone the net can be solved at the low-index axis the pattern shows ([014] for SrTiO3 15 deg off [001]) while the tilt is still measured from the nominal axis: the beams are this solution's, the frame is `zone`. `xaxis` must be normal to `zone`. :type zone: array-like, optional :param xaxis: Orientation written to the file, when it should differ from the one solved at. Off zone the net can be solved at the low-index axis the pattern shows ([014] for SrTiO3 15 deg off [001]) while the tilt is still measured from the nominal axis: the beams are this solution's, the frame is `zone`. `xaxis` must be normal to `zone`. :type xaxis: array-like, optional :param \*\*kw: Passed to :func:`pyextal.acom.datfile.write_dat`. :returns: The file written. :rtype: Path .. py:method:: write_reference_dat(template, dst, zone=None, xaxis=None, **kw) Writes the template's own beam selection at the solved orientation. Same crystal and cutoffs as `template`, only the zone axis and x axis replaced, so `sele`/`aper` reselect geometrically. Comparing this against the explicit file from :meth:`write_dat` checks the ACOM beam list against the engine's own choice at the same orientation. :param template: Existing `.dat`. :type template: str | Path :param dst: File to write. :type dst: str | Path :param zone: Orientation to write instead of the solved one, as for :meth:`write_dat`. :type zone: array-like, optional :param xaxis: Orientation to write instead of the solved one, as for :meth:`write_dat`. :type xaxis: array-like, optional :param \*\*kw: Passed to :func:`pyextal.acom.datfile.write_oriented_dat`. :returns: The file written. :rtype: Path .. py:function:: auto_index(image, param, center=None, zones=None, max_zone_index=2, gmax=1.2, max_index=6, tol_px=3.0, detect_kw=None, match_min_weight_rel=1e-06, extend_to_frame=True, laue_max=0, pixel_size=None, pixel_tol=0.02, theta_ref=None, **solve_kw) Determines the orientation of a diffraction pattern automatically. :param image: 2D averaged diffraction pattern. :type image: np.ndarray :param param: Parsed simulation parameters for the crystal. :type param: SimParams :param center: Approximate direct-beam pixel. The brightest peak is used when None. :type center: tuple[float, float], optional :param zones: Zone axes to try. Low-index axes up to `max_zone_index` are used when None. :type zones: list, optional :param max_zone_index: Largest zone index enumerated. Defaults to 2. :type max_zone_index: int, optional :param laue_max: Highest Laue zone in the search library. Defaults to 0, the ZOLZ, which is what makes different zone axes predict different nets. A high-index axis cannot be indexed at 0 -- its ZOLZ is nearly empty, and the [1,1,15] ZOLZ of SrTiO3 inside 1.2 A^-1 is seven reflections on one row, which is what made it return a scale 1.4x too small and call (200) a (1,-1,0). Use :func:`pyextal.acom.library.min_laue_max` to size it, and pass a single axis in `zones` when you do: raising it for a blind search makes every candidate library dense enough to match anything. :type laue_max: int, optional :param gmax: Largest |g| in the library. Defaults to 1.2. :type gmax: float, optional :param max_index: Miller search box. Defaults to 6. :type max_index: int, optional :param tol_px: Match radius, pixels. Defaults to 3.0. :type tol_px: float, optional :param detect_kw: Passed to :func:`detect_peaks`. :type detect_kw: dict, optional :param match_min_weight_rel: Reflections below this fraction of the strongest |U_g| are excluded from the *search* only. Defaults to 1e-6. :type match_min_weight_rel: float, optional :param extend_to_frame: Once the mapping is known, rebuild the library large enough to cover the whole detector, so the outer orders are predicted too. Defaults to True. :type extend_to_frame: bool, optional :param pixel_size: Known detector scale, inverse Angstroms per pixel -- ``result.mapping.pixel_size`` from a solve of an on-zone pattern taken at the same camera length. Only mappings within `pixel_tol` of it are considered, which removes the sqrt(2)-scaled nets a tilted pattern otherwise admits. Free when None, the default. :type pixel_size: float, optional :param pixel_tol: Fractional tolerance on `pixel_size`. Defaults to 0.02. :type pixel_tol: float, optional :param theta_ref: Detector rotation to settle the net's own rotational symmetry against, degrees. The [001] net of a cubic crystal fits equally well turned by any multiple of 90 degrees, so which of those labelings comes out depends on the detected peaks -- on the SrTiO3 scans R1 gave +79.0 and R2 +169.7. The labeling fixes the frame the tilt is measured in, so `START_TILT` and `ROTATION` only carry over between scans if it does too. When given, the solved mapping is turned by the zone's rotation step onto the branch closest to `theta_ref`. Like `handed` and `pixel_size`, it belongs to the instrument: R4 and R1 agree to 0.3 degrees. Free when None, the default. :type theta_ref: float, optional :param \*\*solve_kw: Passed to :func:`solve_orientation`. :returns: The best orientation found. :rtype: AcomResult .. note:: Two stages, for two different jobs. The search matches against the *strong* reflections only: whether an extinct one shows up depends on multiple scattering, so it can neither be required nor used as evidence. Prediction then uses the *complete* library out to the frame corners, because those same extinct reflections do carry intensity in electron diffraction and still have to be integrated. :raises ValueError: If no peaks are found, or no zone axis explains them. .. py:function:: confirm(result, dat_path, thickness, ncores=1) Confirms an orientation dynamically, with the Bloch engine. Runs the full simulation for the solved orientation and returns the per-reflection intensities, so the kinematic solution can be checked against what the dynamical theory predicts. This is the expensive step, which is why it runs once rather than inside the search. :param result: The orientation to confirm. :type result: AcomResult :param dat_path: `.dat` written for that orientation. :type dat_path: str | Path :param thickness: Sample thickness, Angstroms. :type thickness: float :param ncores: Cores for the spawned workers. Defaults to 1. :type ncores: int, optional :returns: ``(param, exitwave)`` -- the parsed parameters and the simulated intensity of each output reflection. :rtype: tuple