pyextal.acom.tilt¶
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 withxaxis = -2 0 0,conv = 4is 40 mrad, which is what the instrument was set to. Covering a 4 degree offset as well needsconv >= 11.The beam list must be explicit.
selechooses beams geometrically from the convergence, so raising conv inflates it – 661 beams to 1257 going fromconv 4toconv 12on this crystal. The eigenproblem is cubic in that, so the run goes from seconds to forty minutes. With a frozenhklblock 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. Leavingsampat 40 while raisingconvto 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¶
Classes¶
Where the scan sits in K_t space, relative to the zone axis. |
Functions¶
|
Clones an explicit-beam .dat with a wide convergence for this alignment. |
|
Reads zone, xaxis and kt back out of a .dat. |
|
Coarse-refines the direct beam alone to find where the scan sits in K_t. |
|
The incident direction a tilt corresponds to, in crystal coordinates. |
|
Names the beam direction as a rational zone axis. |
|
Re-solves the pattern with the beam along a higher-order zone axis. |
|
Cartesian frames, the beam unit vector and a detector basis normal to it. |
|
Reflections on the frame, with how close the scan brings each to Bragg. |
|
Fits the detector map in the beam-normal frame from labelled positions. |
|
The low-index zone axis whose ZOLZ the indexed reflections lie in. |
|
Refines the beam direction from where the HOLZ spots actually are. |
|
Relabels an extraction for the orientation found, picking up FOLZ and HOLZ. |
|
Writes the refinement .dat for a tilted orientation. |
Module Contents¶
- pyextal.acom.tilt.TARGET_ZOOM = 3.05¶
- pyextal.acom.tilt.MEMORY_WARN_GB = 3.0¶
- class pyextal.acom.tilt.TiltResult¶
Where the scan sits in K_t space, relative to the zone axis.
- Variables:
offset (np.ndarray) – (2,) scan-step offset of the scan centre from the zone axis. This is the measurement; it does not depend on gl.
tilt (np.ndarray) – (2,) the same offset in degrees, which does.
rotation (float) – Scan-to-simulation rotation, degrees.
thickness (float) – Thickness the match was found at, Angstroms.
correlation (float) – Peak cross-correlation.
significance (float) – Peak over
mean + 3 sigmaof the correlation.on_edge (bool) – 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.
gl (float) – The gl used for the degree conversion.
half_angle (float) – Half-width of the simulated K_t range, degrees.
start_tilt (np.ndarray) – (2,) tilt the search started from, degrees.
rotation_scan (dict) – Best correlation at each rotation tried.
- offset: numpy.ndarray¶
- tilt: numpy.ndarray¶
- rotation: float¶
- thickness: float¶
- correlation: float¶
- significance: float¶
- on_edge: bool¶
- gl: float¶
- half_angle: float¶
- start_tilt: numpy.ndarray¶
- rotation_scan: dict¶
- property magnitude¶
Tilt magnitude, degrees.
- Type:
float
- property steps¶
Offset magnitude, scan steps.
- Type:
float
- 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
- gl_for(angle)¶
The gl that would make this offset a given angle.
- Parameters:
angle (float) – Known tilt magnitude, degrees – from the goniometer.
- Returns:
The implied gl.
- Return type:
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.
- __str__()¶
- pyextal.acom.tilt.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.
- Parameters:
template (str | Path) – A .dat with explicit hkl and out blocks – what
pyextal.acom.write_dat()produces. The beam list is carried over unchanged, which is the point: it must not grow with conv.dst (str | Path) – Where to write.
conv (float) – Convergence semi-angle, in units of
|g_xaxis| / |K|.gl (float) – Tilt step size, needed to choose samp.
samp (int, optional) – Sampling. Derived from zoom when None.
zoom (float, optional) – Simulation-to-experiment tilt step ratio to aim for. Defaults to
TARGET_ZOOM.out_beam (tuple, optional) – The single output reflection. Defaults to the direct beam, which is all this alignment uses and which keeps the stored eigenvectors small.
verbose_cost (bool, optional) – Print the tilt count and memory estimate. Defaults to True.
- Returns:
dst.
- Return type:
Path
- Raises:
ValueError – If the template has no explicit beam block.
- pyextal.acom.tilt._orientation(path)¶
Reads zone, xaxis and kt back out of a .dat.
- Parameters:
path (str | Path) – The file.
- Returns:
(zone, xaxis, kt), each a 3-tuple of ints.- Return type:
tuple
- Raises:
ValueError – If zone or xaxis is missing.
- pyextal.acom.tilt.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 –
pyextal.optimize.CoarseOptimizewithgInclude = [(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. Seewide_dat().- Parameters:
dp (np.ndarray) – (nbeam, ny, nx) integrated rocking curves, from
LARBEDAnalysis.stack().gindex (array-like) – (nbeam, 3) Miller indices matching dp.
dat (str | Path) – Wide-field .dat from
wide_dat().thickness (float) – Starting thickness, Angstroms.
gl (float) – Tilt step size, in scan steps per
|g_xaxis|.rotation (float) – Scan-to-simulation rotation, degrees.
region (np.ndarray) – selectROI region covering the rocking curve.
gx (array-like, optional) – Horizontal reference reflection.
variance (np.ndarray, optional) – Matching variance maps.
start_tilt (tuple, optional) – (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).
ncores (int, optional) – Cores for the simulation. Defaults to 1.
verbose (bool, optional) – Report progress. Defaults to True.
- Returns:
The scan centre relative to the zone axis.
- Return type:
Note
The tilt is read from
dinfo.tiltX/dinfo.tiltY, whichoptimizeOrientationThickness()accumulates – so it carries the engine’s sign convention already and needs no reinterpretation here.Check
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 inwide_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 >= 11and 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, soTiltResult.tiltis always the total tilt off the zone axis – seed included – and never something the caller has to add the seed back onto.TiltResult.correctionreports what the search contributed.
- pyextal.acom.tilt.beam_direction(param, tilt, zone=None, handed=1)¶
The incident direction a tilt corresponds to, in crystal coordinates.
- Parameters:
param (SimParams) – Parsed simulation parameters.
tilt (array-like) – (2,) tilt along the pattern x and y axes, degrees –
TiltResult.tilt.zone (array-like, optional) – Zone axis. Read from the parsed geometry (
blochwave.difpar.zone) when None.handed (int, optional) –
-1if the scan’s second axis runs opposite togh, which negates that component of the tilt. Defaults to +1.
- Returns:
(3,) direction in direct-space (uvw) components, normalised so the largest component is 1.
- Return type:
np.ndarray
Note
param.tilt0is the in-plane deviation from the zone axis, not the incident wavevector – it is[0, 0, 0]forkt 0 0 0. The wavevector has to be built from the zone axis and the deviation added to it; starting fromtilt0alone 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 topyextal.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 andghand 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.
- pyextal.acom.tilt.higher_order_zone(param, tilt, max_index=20, tol_deg=None, handed=1, verbose=True)¶
Names the beam direction as a rational zone axis.
- Parameters:
param (SimParams) – Parsed simulation parameters.
tilt (array-like) – (2,) tilt in degrees, from
TiltResult.tilt.max_index (int, optional) – Largest index to consider. Defaults to 20.
tol_deg (float, optional) – Only accept an axis within this angle.
handed (int, optional) – Passed to
beam_direction();-1when the scan’s second axis runs opposite togh. Defaults to +1.verbose (bool, optional) – Print the ranked candidates. Defaults to True.
- Returns:
(uvw, angle_deg, ranked)– the closest rational axis, how far the beam is from it, and the ranked list behind the choice.- Return type:
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.
- pyextal.acom.tilt.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, movingktto0.05 0.05 0leaves 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.- Parameters:
image (np.ndarray) – The averaged pattern, as passed to
pyextal.acom.auto_index().param (SimParams) – Parsed simulation parameters.
zone (array-like) – The higher-order zone axis.
center (tuple) – Direct-beam pixel hint.
gmax (float, optional) – Library extent. Defaults to 1.2.
max_index (int, optional) – Miller index bound. Defaults to 6.
handed (int, optional) – Pin the handedness, as for auto_index.
laue_max (int, optional) – Laue zones in the library. Sized with
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.verbose (bool, optional) – Report. Defaults to True.
**kw – Passed to
pyextal.acom.auto_index().
- Returns:
Indexed at the new orientation, ready for
to_gridandwrite_dat.- Return type:
- pyextal.acom.tilt._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.
- pyextal.acom.tilt.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.
- Parameters:
param (SimParams) – Parsed simulation parameters.
beam (array-like) – (3,) beam direction in uvw, as from
beam_direction().zone (array-like) – Zone axis the Laue index is counted against.
xaxis (array-like) – In-plane reference reflection of zone.
mapping (DetectorMap) – Detector map in the beam-normal frame, as fitted by
fit_beam_map().shape (tuple[int, int]) – Detector shape.
laue_max (int, optional) – Highest |Laue index| considered. Defaults to 3.
smax (float, optional) – Excitation error, A^-1, below which a reflection counts as excited. Defaults to 0.02.
scan_half (float, optional) – 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.gmax (float, optional) – Largest |g|; the frame’s reach when None.
max_index (int, optional) – Miller search box; sized from gmax when None.
- 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, andexcited(n,) bool.- Return type:
dict
- pyextal.acom.tilt.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
pyextal.acom.resolve_label_symmetry()still fit.- Parameters:
hkl (array-like) – (n, 3) labels, ZOLZ of zone.
pixels (array-like) – (n, 2) their detector positions.
param (SimParams) – Parsed simulation parameters.
beam (array-like) – (3,) beam direction in uvw.
zone (array-like) – Zone axis of the labels.
xaxis (array-like) – In-plane reference reflection of zone.
- Returns:
The fitted map,
rmsin pixels.- Return type:
- pyextal.acom.tilt.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.
fit_beam_to_holz()andrelabel_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.
- Parameters:
labels (array-like) – (n, 3) current labels.
param (SimParams) – Parsed simulation parameters.
beam (array-like) – (3,) beam direction, uvw.
max_index (int, optional) – Largest index tried. Defaults to 6.
max_angle (float, optional) – Largest beam-to-axis angle, degrees. Defaults to 20.
verbose (bool, optional) – Report. Defaults to True.
- Returns:
(uvw, angle_deg, n_zolz).- Return type:
tuple
- pyextal.acom.tilt.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.
- Parameters:
peaks (array-like) – (n, 2) detected peak positions.
hkl (array-like) – (m, 3) ZOLZ labels the map is fitted from.
pixels (array-like) – (m, 2) their positions.
param (SimParams) – Parsed simulation parameters.
beam (array-like) – (3,) starting beam direction, uvw.
zone (array-like, optional) – Zone axis of the net. Defaults to [001].
xaxis (array-like, optional) – In-plane reference reflection; the shortest label in hkl when None.
shape (tuple, optional) – Detector shape. Defaults to (124, 124).
search_deg (float, optional) – Half-width of the search, degrees, in both directions normal to beam. Defaults to 4.
step_deg (float, optional) – Coarse grid step, degrees. Defaults to 0.25.
laue_max – As for
predict_excited().smax – As for
predict_excited().scan_half – As for
predict_excited().node_tol (float, optional) – Peaks closer than this to a ZOLZ node, pixels, are ZOLZ spots. Defaults to 0.9.
match_px (float, optional) – A HOLZ spot counts as explained within this many pixels. Defaults to 1.0.
verbose (bool, optional) – Report. Defaults to True.
- Returns:
(beam, info)– the refined direction in uvw, scaled so its largest component is 1, and a dict withhits,n_holz_peaks,unexplained(excited predictions with no peak),offset_deg(shift from beam along the two beam-normal axes) andangle_deg. beam is returned unchanged when fewer than four HOLZ spots are found.- Return type:
tuple
- pyextal.acom.tilt.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:
the detector map is refitted from the ZOLZ entries already in larbed, in the plane normal to the beam;
every reflection with
|laue| <= laue_maxon the frame is predicted, with the excitation error the scan brings it closest to;ZOLZ reflections are all kept, as before, and HOLZ reflections are kept when excited somewhere in the scan;
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.
- Parameters:
larbed (LARBEDAnalysis) – Extraction whose grid and g_vectors to replace. Its ZOLZ entries define the map, so run this after the label symmetry is settled.
param (SimParams) – Parsed simulation parameters.
beam (array-like) – (3,) beam direction in uvw, in the frame of the current labels.
zone (array-like, optional) – Zone axis of the net. Defaults to [001].
xaxis (array-like, optional) – In-plane reference reflection; the shortest ZOLZ label present when None.
laue_max (int, optional) – Highest |Laue index| considered. Defaults to 3.
smax (float, optional) – Excitation error, A^-1, counting as excited. Defaults to 0.02.
scan_half (float, optional) – Half-range of the rocking scan, radians; see
predict_excited(). Defaults to 0.min_sep (float, optional) – Predictions closer than this, pixels, are one spot. Defaults to 3.0, the integration aperture.
verbose (bool, optional) – Report. Defaults to True.
- Returns:
mapping(DetectorMap),added/removedlabel lists,renamedas(zolz, holz)pairs, andpredictedfrompredict_excited().- Return type:
dict
- pyextal.acom.tilt.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:
outis what the experiment measured – the reflections that fall on the detector and have integrated rocking curves. It cannot be widened without integrating more.hklis 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: movingktleaves 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 withl != 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.
- Parameters:
template (str | Path) – A .dat for this crystal whose beam block is still implicit (
sele), so the engine re-selects at the new zone.dst (str | Path) – Where to write.
zone (array-like) – The tilted zone axis.
out_beams (list) – Reflections with integrated rocking curves.
xaxis (tuple, optional) – Horizontal reference. Kept from template when None – worth keeping, since gl is expressed in units of it and changing it rescales gl proportionally.
kt (tuple, optional) – Residual tilt. Defaults to (0, 0, 0).
conv (float, optional) – Convergence. Kept from template when None.
samp (int, optional) – Sampling. Kept from template when None.
verbose (bool, optional) – Report the beam counts. Defaults to True.
- Returns:
dst.
- Return type:
Path