pyextal.acom.tilt ================= .. py:module:: pyextal.acom.tilt .. autoapi-nested-parse:: Beam tilt from the direct beam alone, by aligning K_t space. `auto_index` determines the zone axis of the *net* -- the reciprocal lattice plane the pattern samples. It says nothing about where the beam sits within that plane, because peak positions do not depend on it. For a scan taken off zone that missing piece is the whole tilt. This module recovers it from the (000) rocking curve. The direct beam's LARBED map is a bright-field pattern in K_t space, crossed by Kikuchi and Bragg bands whose intersections pin it in two dimensions. Simulating the same map over a *wider* K_t range than the scan covers, and cross-correlating, locates the scan's centre relative to the zone axis. One beam is enough, and the whole thing runs before any structure-factor refinement. Validated on two SrTiO3 scans of the same crystal: the on-zone scan returns 11.6 scan steps of offset (0.22 degrees) and the scan taken off zone returns 344.0, reproducibly. Four things have to be right, and three of them are easy to get wrong: * **`conv` must cover the scan plus the offset.** It is a semi-angle in units of ``|g_xaxis| / |K|``: on SrTiO3 with ``xaxis = -2 0 0``, ``conv = 4`` is 40 mrad, which is what the instrument was set to. Covering a 4 degree offset as well needs ``conv >= 11``. * **The beam list must be explicit.** ``sele`` chooses beams geometrically from the convergence, so raising `conv` inflates it -- 661 beams to 1257 going from ``conv 4`` to ``conv 12`` on this crystal. The eigenproblem is cubic in that, so the run goes from seconds to forty minutes. With a frozen ``hkl`` block the matrix does not move and the cost is linear in the tilt count. * **`samp` must rise with `conv`.** What matters is the tilt step, ``gl * conv / samp``, which the refinement path keeps near 3. Leaving ``samp`` at 40 while raising ``conv`` to 12 pushes it to 9 and the simulated map aliases into a radial star; its correlation against the experiment falls from 0.82 to 0.39. * **Rotate the simulation, not the experiment.** Rotating the experiment forces a crop to avoid the corners, and the corners are where the band crossings are. Without them the correlation is a ridge along the bands rather than a peak. The offset is measured in **scan steps**, which is what the correlation actually determines. Turning it into an angle divides by `gl`, so an uncalibrated `gl` scales the answer: 344 steps is 6.5 degrees at ``gl = 30.5`` and 4.0 at ``gl = 49.7``. Both are reported for that reason, and the relation runs backwards just as well -- a tilt known from the goniometer calibrates `gl`. Attributes ---------- .. autoapisummary:: pyextal.acom.tilt.TARGET_ZOOM pyextal.acom.tilt.MEMORY_WARN_GB Classes ------- .. autoapisummary:: pyextal.acom.tilt.TiltResult Functions --------- .. autoapisummary:: pyextal.acom.tilt.wide_dat pyextal.acom.tilt._orientation pyextal.acom.tilt.tilt_from_zero_beam pyextal.acom.tilt.beam_direction pyextal.acom.tilt.higher_order_zone pyextal.acom.tilt.reindex_at_zone pyextal.acom.tilt._beam_frame pyextal.acom.tilt.predict_excited pyextal.acom.tilt.fit_beam_map pyextal.acom.tilt.net_zone pyextal.acom.tilt.fit_beam_to_holz pyextal.acom.tilt.relabel_holz pyextal.acom.tilt.write_tilted_dat Module Contents --------------- .. py:data:: TARGET_ZOOM :value: 3.05 .. py:data:: MEMORY_WARN_GB :value: 3.0 .. 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:: _orientation(path) Reads `zone`, `xaxis` and `kt` back out of a `.dat`. :param path: The file. :type path: str | Path :returns: ``(zone, xaxis, kt)``, each a 3-tuple of ints. :rtype: tuple :raises ValueError: If `zone` or `xaxis` is missing. .. 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:: _beam_frame(param, zone, xaxis, beam) Cartesian frames, the beam unit vector and a detector basis normal to it. The detector plane is normal to the beam, not to the zone axis. For the ZOLZ the difference is a sub-pixel stretch; for a HOLZ reflection it is not: g = (h, k, 1) of SrTiO3 has 0.256 A^-1 along [001], which a beam 7 degrees off moves by ~1 px against its zone-axis projection. .. 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: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