upxo.xtalphy.crystal_orientation module
crystal_orientation.py
Stand-alone module for crystallographic orientation mathematics.
All definitions are extracted (by reference, not moved) from
upxo.pxtal.mcgs3_temporal_slice so that the original class is
unchanged. Callers should import from here rather than reaching into
the temporal-slice module.
Public API
- Constants
FCC_TEXTURE_COMPONENTS – dict {name: (phi1, Phi, phi2)} in degrees (Bunge) CUBIC_SYMM_OPS_CACHE – module-level cache for 24 cubic SO(3) operators
- Primitive rotation helpers (pure functions, no class needed)
Rz(a) – 3x3 rotation about Z axis (angle in radians) Rx(a) – 3x3 rotation about X axis (angle in radians) euler_bunge_to_matrix(phi1, Phi, phi2, degrees=True) – single Bunge ZXZ R euler_bunge_to_matrix_batch(phi1, Phi, phi2, …) – vectorized version matrix_to_euler_bunge(R, degrees=True) – R -> (phi1, Phi, phi2) axis_angle_to_R(axis, angle_rad) – Rodrigues formula normalize_euler_bunge(ea, degrees=True) – canonical Euler range proj_to_so3(R) – SVD-project to SO(3) unique_rotations(rotations, tol=1e-8) – deduplicate R matrices
- Symmetry helpers
cubic_symmetry_operators() – list of 24 proper cubic rotation matrices get_cubic_ops_np() – (24,3,3) ndarray (cached) fcc_symmetrise_ori(bea) – symmetric equivalents of one orientation rand_unit_vector(rng) – uniform S2 sample rand_uniform_SO3(rng) – uniform SO(3) sample
- Misorientation
cubic_rotation_angle(R) – disorientation angle from single dR cubic_rotation_angle_batch(rstack) – vectorized version cubic_rotation_axis(R, angle) – axis for a single dR cubic_rotation_axis_batch(rstack, angles) – vectorized version cubic_misorientation(EA1, EA2, …) – fast vectorized cubic misorientation cubic_misorientation_scalar(EA1, EA2, …) – scalar (loop) reference version
- High-level utilities
- grain_boundary_misorientation_distribution(euler_array, pairs, …)
Compute the GBMD (misorientation angle distribution) for a list of grain-boundary pairs given per-grain Bunge Euler angles.
- gbmd_from_lfi(lfi, euler_array, connectivity=4, …)
Convenience wrapper: auto-detects grain boundary pairs from a label-field image and returns the GBMD.
- ipf_color(euler_deg, sample_direction=[0,0,1])
IPF colour (R,G,B) tuple.
- Special orientation relationships
get_ks_rotations() – 24 Kurdjumov-Sachs BCC variant matrices
Usage example
>>> from upxo.xtalphy.crystal_orientation import (
... cubic_misorientation,
... grain_boundary_misorientation_distribution,
... gbmd_from_lfi,
... FCC_TEXTURE_COMPONENTS,
... )
>>> angle, axis, top3 = cubic_misorientation([0, 0, 0], [45, 0, 0])
>>> print(f"Misorientation: {angle:.2f} deg, axis: {axis}")
- upxo.xtalphy.crystal_orientation.Rz(a: float) numpy.ndarray[source]
3x3 rotation matrix about Z axis. a in radians.
- upxo.xtalphy.crystal_orientation.Rx(a: float) numpy.ndarray[source]
3x3 rotation matrix about X axis. a in radians.
- upxo.xtalphy.crystal_orientation.euler_bunge_to_matrix(phi1: float, Phi: float, phi2: float, degrees: bool = True) numpy.ndarray[source]
Bunge ZXZ convention: R = Rz(phi1) · Rx(Phi) · Rz(phi2).
- upxo.xtalphy.crystal_orientation.euler_bunge_to_matrix_batch(phi1: numpy.ndarray, Phi: numpy.ndarray, phi2: numpy.ndarray, degrees: bool = True, dtype=numpy.float64) numpy.ndarray[source]
Vectorized Bunge ZXZ rotation matrix from arrays of Euler angles.
- upxo.xtalphy.crystal_orientation.matrix_to_euler_bunge(R: numpy.ndarray, degrees: bool = True) tuple[float, float, float][source]
Convert a (3×3) rotation matrix to Bunge ZXZ Euler angles.
- upxo.xtalphy.crystal_orientation.normalize_euler_bunge(ea: numpy.ndarray, degrees: bool = True, eps: float = 1e-06) numpy.ndarray[source]
- Normalize Bunge ZXZ Euler angles to canonical ranges:
phi1, phi2 in [0, 360°) Phi in [0, 180°]
- upxo.xtalphy.crystal_orientation.axis_angle_to_R(axis: numpy.ndarray, angle_rad: float) numpy.ndarray[source]
Rodrigues’ rotation formula: axis-angle → 3×3 rotation matrix.
- upxo.xtalphy.crystal_orientation.proj_to_so3(R: numpy.ndarray) numpy.ndarray[source]
Project R to the nearest proper rotation matrix via SVD.
- upxo.xtalphy.crystal_orientation.unique_rotations(rotations: list[numpy.ndarray], tol: float = 1e-08) list[numpy.ndarray][source]
Return deduplicated list of rotation matrices (Frobenius norm criterion).
- upxo.xtalphy.crystal_orientation.cubic_symmetry_operators() list[numpy.ndarray][source]
Return the 24 proper rotation matrices for cubic m-3m symmetry.
Uses signed-permutation matrices with determinant +1.
- Return type:
list of ndarray, each shape (3, 3)
- upxo.xtalphy.crystal_orientation.get_cubic_ops_np() numpy.ndarray[source]
Return the 24 cubic symmetry operators as a cached (24, 3, 3) ndarray.
- upxo.xtalphy.crystal_orientation.fcc_symmetrise_ori(bea: tuple[float, float, float], dtype=numpy.float32) numpy.ndarray[source]
Generate all symmetrically equivalent Bunge Euler angle triplets for one FCC orientation.
- Parameters:
bea ((phi1, Phi, phi2) in degrees)
dtype (numpy dtype for output)
- Return type:
ndarray, shape (M, 3), M ≤ 24
- upxo.xtalphy.crystal_orientation.rand_unit_vector(rng) numpy.ndarray[source]
Uniform random unit vector on S². rng must expose
.gauss(mu, s).
- upxo.xtalphy.crystal_orientation.rand_uniform_SO3(rng) numpy.ndarray[source]
Draw a uniform random rotation from SO(3) using the Shoemake (1992) method.
- Parameters:
rng (object exposing
.random()(e.g.random.Random()))- Returns:
R
- Return type:
ndarray, shape (3, 3)
- upxo.xtalphy.crystal_orientation.cubic_rotation_angle(R: numpy.ndarray) float[source]
Disorientation angle (radians) of a proper rotation matrix R.
- upxo.xtalphy.crystal_orientation.cubic_rotation_angle_batch(rstack: numpy.ndarray) numpy.ndarray[source]
Vectorized rotation angle for a stack of matrices.
- Parameters:
rstack (ndarray, shape (..., 3, 3))
- Returns:
angles
- Return type:
ndarray, same leading shape as rstack minus last two dims
- upxo.xtalphy.crystal_orientation.cubic_rotation_axis(R: numpy.ndarray, angle: float) numpy.ndarray[source]
Return unit rotation axis for a proper rotation R given its angle (rad). Falls back to [1,0,0] for near-zero angles.
- upxo.xtalphy.crystal_orientation.cubic_rotation_axis_batch(rstack: numpy.ndarray, angles: numpy.ndarray) numpy.ndarray[source]
Vectorized rotation axis for a stack of rotation matrices.
- upxo.xtalphy.crystal_orientation.cubic_misorientation(EA1, EA2, unique_tol_deg: float = 0.0001, degrees: bool = True) tuple[float, numpy.ndarray, list[float]][source]
Vectorized cubic (m-3m) misorientation between two orientations.
- Parameters:
EA1 (array-like) – Each can be a Bunge Euler triplet (phi1, Phi, phi2) or a (3×3) rotation matrix.
EA2 (array-like) – Each can be a Bunge Euler triplet (phi1, Phi, phi2) or a (3×3) rotation matrix.
unique_tol_deg (float) – Tolerance (degrees) for deduplicating the top-3 angles.
degrees (bool) – Applies to Euler inputs only. Default True.
- Returns:
angle_deg_min (float) – Minimum (fundamental) misorientation angle in degrees.
axis_min (ndarray, shape (3,)) – Corresponding rotation axis (sample frame).
top3_angles_deg (list[float]) – Up to 3 smallest unique misorientation angles (degrees), ascending.
- upxo.xtalphy.crystal_orientation.cubic_misorientation_scalar(EA1, EA2, unique_tol_deg: float = 0.0001, degrees: bool = True) tuple[float, numpy.ndarray, list[float]][source]
Scalar (double-loop) reference implementation of cubic misorientation. Slower but more transparent. Same return signature as
cubic_misorientation.
- upxo.xtalphy.crystal_orientation.ipf_color(euler_deg, sample_direction=(0.0, 0.0, 1.0)) tuple[float, float, float][source]
Approximate IPF colour for a single cubic orientation.
- upxo.xtalphy.crystal_orientation.grain_boundary_misorientation_distribution(euler_array: np.ndarray, pairs: np.ndarray, grain_ids: np.ndarray | None = None, degrees: bool = True, n_bins: int = 36, angle_range: tuple[float, float] = (0.0, 65.0)) dict[source]
Compute the Grain Boundary Misorientation Distribution (GBMD) for a set of grain-boundary pairs.
- Parameters:
euler_array (ndarray, shape (N_grains, 3)) – Bunge Euler angles (phi1, Phi, phi2) for each grain. If grain_ids is given, row i corresponds to
grain_ids[i]. Otherwise row i corresponds to grain ID i.pairs (ndarray, shape (N_pairs, 2), dtype int) – Each row [gid_a, gid_b] is one grain-boundary pair.
grain_ids (ndarray, shape (N_grains,) or None) – Grain ID labels that index into euler_array. If None, grain IDs are assumed to be 0, 1, …, N_grains-1.
degrees (bool) – If True, euler_array is in degrees. Default True.
n_bins (int) – Number of histogram bins over angle_range. Default 36.
angle_range ((float, float)) – (min_deg, max_deg) for the histogram. Default (0, 65).
- Returns:
result –
'misorientation_angles'– ndarray of angle per pair (degrees)'misorientation_axes'– ndarray, shape (N_pairs, 3)'pairs'– same as input pairs'hist_counts'– bin counts'hist_bin_edges'– bin edge values (degrees)'hist_bin_centers'– bin centre values (degrees)'mean_angle'– mean misorientation angle (degrees)'median_angle'– median misorientation angle (degrees)'std_angle'– std deviation of angles (degrees)'n_pairs'– total number of pairs processed- Return type:
dict with keys
Notes
The computation uses the full 24×24 cubic symmetry exhaustive search (
cubic_misorientation). For very large datasets (> 10 000 pairs) this may be slow; consider sub-sampling or parallelising the loop.Examples
>>> from upxo.xtalphy.crystal_orientation import ( ... grain_boundary_misorientation_distribution, gbmd_from_lfi) # With explicit pairs >>> ea = np.random.rand(100, 3) * [360, 180, 360] # random orientations >>> pairs = np.array([[0, 1], [1, 2], [3, 4]]) >>> result = grain_boundary_misorientation_distribution(ea, pairs) >>> import matplotlib.pyplot as plt >>> plt.bar(result['hist_bin_centers'], result['hist_counts'], ... width=np.diff(result['hist_bin_edges'])[0]) >>> plt.xlabel('Misorientation angle (°)'); plt.ylabel('Count') >>> plt.title('GBMD'); plt.show()
- upxo.xtalphy.crystal_orientation.gbmd_from_lfi(lfi: np.ndarray, euler_array: np.ndarray, grain_ids: np.ndarray | None = None, connectivity: int = 4, degrees: bool = True, n_bins: int = 36, angle_range: tuple[float, float] = (0.0, 65.0)) dict[source]
Convenience wrapper: detect grain-boundary pairs from a label-field image lfi and compute the GBMD.
- Parameters:
lfi (ndarray, shape (ny, nx), dtype int) – Integer label field. Each unique positive integer is a grain ID. Pixels with value ≤ 0 are ignored (unindexed).
euler_array (ndarray, shape (N_grains, 3)) – Bunge Euler angles for each grain (row-order matches grain_ids).
grain_ids (ndarray or None) – Grain ID labels corresponding to rows of euler_array. If None, grain IDs are 0 … N_grains-1.
connectivity ({4, 8}) – Pixel adjacency for boundary detection. Default 4.
degrees (bool) – Default True.
n_bins (int) – Default 36.
- Returns:
Same dict as
grain_boundary_misorientation_distributionplusan extra key
'gb_pairs'containing the deduplicated set ofadjacent grain-ID pairs.
Notes
The pair-detection uses integer-shift neighbour comparison, which is O(ny·nx) and memory-efficient.
- upxo.xtalphy.crystal_orientation.quat_to_R_batch(q: numpy.ndarray) numpy.ndarray[source]
Convert a stack of unit quaternions to rotation matrices.
- Parameters:
q (ndarray, shape (N, 4)) – Unit quaternions in [w, x, y, z] convention.
- Returns:
R
- Return type:
Examples
from upxo.xtalphy.crystal_orientation import quat_to_R_batch import numpy as np q = np.array([[1., 0., 0., 0.]]) # identity quat_to_R_batch(q) # → [[[1,0,0],[0,1,0],[0,0,1]]]
- upxo.xtalphy.crystal_orientation.grain_avg_quats(lfi: numpy.ndarray, quat: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray][source]
Compute per-grain average quaternion from a pixel-wise quaternion map.
Handles the cyclic ambiguity of Euler angles by averaging in quaternion space (enforcing positive-w hemisphere before accumulation).
- Parameters:
- Returns:
gids (ndarray, shape (N_grains,), dtype int) – Sorted array of unique positive grain IDs found in lfi.
q_mean (ndarray, shape (N_grains, 4)) – Normalised mean quaternion for each grain (row order matches gids).
Examples
from upxo.xtalphy.crystal_orientation import grain_avg_quats gids, q_mean = grain_avg_quats(rg.lfi_ebsd, rg.quat_ebsd)
- upxo.xtalphy.crystal_orientation.compute_mdf_from_quats(lfi: numpy.ndarray, quat: numpy.ndarray, neigh_gid: dict, n_bins: int = 65, angle_range: tuple[float, float] = (0.0, 65.0)) dict[source]
Compute the grain-boundary Misorientation Distribution Function (MDF) from a pixel quaternion map and a neighbour-graph dict.
Uses per-grain quaternion averaging (correct for cyclic Euler angles) and a fully vectorised 24×24 cubic symmetry exhaustive search.
- Parameters:
lfi (ndarray, shape (ny, nx), dtype int) – Integer grain label field.
quat (ndarray, shape (ny, nx, 4)) – Per-pixel unit quaternions [w, x, y, z].
neigh_gid (dict) – Mapping
{grain_id: array_of_neighbour_ids}as produced byEBSDReader.characterise()orfind_neighs2d().n_bins (int) – Number of histogram bins. Default 65 (1° per bin over 0–65°).
angle_range ((float, float)) – Histogram range in degrees. Default (0, 65).
- Returns:
result –
'miso_deg'– ndarray (N_pairs,) of disorientation angles'pairs'– ndarray (N_pairs, 2) of grain-ID pairs used'hist_counts'– bin counts (int)'hist_density'– probability density (float, integrates to 1)'hist_bin_edges'– bin edges (degrees)'hist_bin_centers'– bin centres (degrees)'mean_angle'– mean disorientation (degrees)'std_angle'– std deviation (degrees)'n_pairs'– number of unique boundary pairs- Return type:
dict with keys
Examples
from upxo.xtalphy.crystal_orientation import compute_mdf_from_quats mdf = compute_mdf_from_quats(rg.lfi_ebsd, rg.quat_ebsd, rg.neigh_gid_ebsd) print(mdf[‘mean_angle’], ‘°’)
- upxo.xtalphy.crystal_orientation.CUBIC_CSL: dict[str, float] = {'S11': 50.48, 'S13b': 27.8, 'S3 (twin)': 60.0, 'S5': 36.87, 'S7': 38.21, 'S9': 38.94}
Default cubic CSL reference angles (degrees)
- upxo.xtalphy.crystal_orientation.detect_mdf_peaks(mdf: dict, prominence: float = 0.002, distance: int = 3, csl: dict[str, float] | None = None, csl_tol: float = 2.0, bw_method: str | float = 'scott', n_kde: int = 500, angle_max: float = 65.0) dict[source]
Detect peaks in a pre-computed MDF, match them against CSL angles, and compute a KDE curve over the raw disorientation angles.
- Parameters:
mdf (dict) – Output of
compute_mdf_from_quats(). Must contain keys'hist_bin_centers','hist_density', and'miso_deg'.prominence (float) – Minimum peak prominence for
scipy.signal.find_peaks. Default 0.002.distance (int) – Minimum number of bins between two peaks. Default 3.
csl (dict or None) – Mapping
{label: angle_degrees}. If None, usesCUBIC_CSL.csl_tol (float) – Tolerance in degrees for declaring a peak “near” a CSL. Default 2.0.
bw_method (str or float) – Bandwidth method passed to
scipy.stats.gaussian_kde. Default'scott'.n_kde (int) – Number of points in the KDE evaluation grid. Default 500.
angle_max (float) – Upper end of the KDE evaluation grid (degrees). Default 65.
- Returns:
result –
'peak_indices'– ndarray of bin indices of detected peaks'peak_angles'– list of float, angle of each detected peak'peak_labels'– list of human-readable label strings'csl_nearest'– list of (nearest_label, delta) tuples per peak'kde'– fittedgaussian_kdeobject'kde_vals'– ndarray (n_kde,) of KDE density values'theta_fine'– ndarray (n_kde,) evaluation grid (degrees)'csl'– the CSL dict used'csl_tol'– the tolerance used- Return type:
dict with keys
- upxo.xtalphy.crystal_orientation.segregate_csl_pairs(mdf: dict, selected_peaks: dict, csl: dict[str, float] | None = None, csl_tol: float = 2.0) dict[source]
Segregate grain-boundary pairs into CSL categories based on user-selected MDF peaks.
For every selected peak, the nearest CSL reference angle is found; pairs whose disorientation lies within csl_tol of that reference are collected under that CSL label.
- Parameters:
mdf (dict) – Output of
compute_mdf_from_quats(). Must contain'pairs'(N,2) and'miso_deg'(N,).selected_peaks (dict) – Output of
mdf_peak_selector(). Keys:'angles'(list[float]) and'indices'(list[int]).csl (dict or None) –
{label: reference_angle_degrees}. Defaults toCUBIC_CSL.csl_tol (float) – Tolerance in degrees. Pairs with
|Δθ| ≤ csl_tolare included. Default 2.0.
- Returns:
result – Keyed by CSL label. Each value is a dict with keys:
'csl_angle'(float),'pairs'(ndarray M×2),'miso_deg'(ndarray M),'grains_A','grains_B','grains_all'(ndarrays of unique grain IDs).- Return type:
Notes
A selected peak that maps to the same CSL label as another peak (within csl_tol) produces only one key in the output. Pairs can match more than one CSL category if tolerances overlap.
- upxo.xtalphy.crystal_orientation.csl_volume_fractions(lfi: numpy.ndarray, csl_grains: dict) dict[source]
Compute the area (volume) fraction of the indexed map occupied by grains that participate in each CSL boundary type.
A grain is counted if it appears in any boundary of that CSL type (i.e. it belongs to
csl_grains[label]['grains_all']).Grains can contribute to multiple CSL categories simultaneously, so fractions do not necessarily sum to 1.
- Parameters:
- Returns:
result –
'n_pixels'– int, total pixels occupied by CSL grains'vf_indexed'– float, fraction of indexed pixels'vf_total'– float, fraction of all pixels (including unindexed)'csl_angle'– float, reference CSL angle (degrees)'n_grains'– int, number of grains in this CSL type- Return type:
dict keyed by CSL label, each value a dict with
- upxo.xtalphy.crystal_orientation.identify_parent_grains(csl_grains: dict, prop: dict) dict[source]
For each CSL boundary pair, identify the parent (larger) grain and the twin / child (smaller) grain using grain area as the discriminator.
A grain can play both roles across different pairs (twin chain: A→B→C where B is twin of A but parent of C). Grains are therefore classified per their net role:
pure_parents– appear as the larger grain in every one of their pairspure_twins– appear as the smaller grain in every one of their pairsintermediates– appear as parent in some pairs and twin in others
- Parameters:
- Returns:
result –
'pairs_labeled'– list of(parent_gid, twin_gid)tuples'all_parents'– ndarray, grains that are parent in ≥1 pair'all_twins'– ndarray, grains that are twin in ≥1 pair'pure_parents'– ndarray, grains that are only ever a parent'pure_twins'– ndarray, grains that are only ever a twin'intermediates'– ndarray, grains in both roles (twin chains)'n_pure_parents'– int'n_pure_twins'– int'n_intermediates'– int'csl_angle'– float- Return type:
dict keyed by CSL label, each value a dict with
- upxo.xtalphy.crystal_orientation.classify_grain_roles_extended(parent_info: dict) dict[source]
Extend the EBSD parent_info dict with
'primary_twins'and'secondary_twins'keys, derived from the existing'pairs_labeled'and'intermediates'arrays.Operates entirely on EBSD grain IDs — no MC data involved.
A primary twin is a
pure_twinwhose direct parent (the larger grain in its CSL pair) is itself apure_parent. It is a first-generation twin with no twins of its own.A secondary twin is a
pure_twinwhose direct parent is anintermediategrain (a grain that is both someone’s twin and someone’s parent). It is a twin-of-a-twin — second generation or deeper.- Parameters:
parent_info (dict) – Output of
identify_parent_grains(). Each value must contain'pairs_labeled','pure_twins', and'intermediates'.- Returns:
Same structure as
parent_infowith two extra keys per CSL label:'primary_twins'— ndarray of first-generation twin grain IDs'secondary_twins'— ndarray of second-generation twin grain IDs'n_primary_twins'— int'n_secondary_twins'— int- Return type:
Notes
pure_twins ≡ primary_twins ∪ secondary_twins(the sets are disjoint and their union equals the existingpure_twinsarray).
- upxo.xtalphy.crystal_orientation.compute_grain_role_ratios(lfi: numpy.ndarray, parent_info: dict) dict[source]
Compute the ratio of each grain role (pure parent, pure twin, intermediate, non-role) to the total number of indexed grains, using the same priority rules as
plot_grain_role_property_stats(intermediate > parent > twin).- Parameters:
lfi (ndarray, shape (ny, nx)) – Integer grain label field (positive = grain ID).
parent_info (dict) – Output of
identify_parent_grains().
- Returns:
Keys:
'total','pure_parents','pure_twins','intermediates','non_role','all_role'. Each value is a sub-dict with'count'and'ratio'.- Return type:
- upxo.xtalphy.crystal_orientation.assign_grain_roles_by_ratio(lfi: numpy.ndarray, grain_areas: dict, target_ratios: dict) dict[source]
Assign pure-parent / pure-twin / intermediate / non-role labels to grains in a grain structure so that the resulting role fractions match the target_ratios (e.g. derived from an EBSD dataset via
compute_grain_role_ratios()).Grains are sorted by area (descending). The largest grains receive the pure parent label, the next tier pure twin, the next intermediate, and the remainder non-role. This reflects the physical expectation that parent grains are typically larger than their twins.
- Parameters:
lfi (ndarray, shape (ny, nx)) – Integer grain label field (positive = grain ID).
grain_areas (dict) –
{grain_id: area}mapping. Can be built from an EBSDprop_ebsddict or from amcgs2_grain_structure.propDataFrame column.target_ratios (dict) – Output of
compute_grain_role_ratios(). The'ratio'field of'pure_parents','pure_twins','intermediates', and'non_role'entries is used.
- Returns:
Same structure as
compute_grain_role_ratios(): keys'total','pure_parents','pure_twins','intermediates','non_role','all_role'; each value has'count','ratio','grain_ids'.- Return type:
- upxo.xtalphy.crystal_orientation.introduce_twins_by_csl(lfi: numpy.ndarray, parent_grain_ids: set | list, csl_label: str, twin_half_width: float = 2.0, twin_angle_deg: float | None = None, n_twins_per_parent: int = 1, angle_perturb_deg: float = 0.0, width_perturb: float = 0.0, rng_seed: int | None = None) dict[source]
Introduce straight twin lamellae into selected parent grains on a pixel grid using
Sline2d.For each parent grain a base angle is chosen; all lamellae within that grain are parallel (same angle) but can receive small independent perturbations to their angle and half-width to model realistic scatter.
- Parameters:
lfi (ndarray, shape (ny, nx)) – Integer grain label field. Modified in-place.
parent_grain_ids (set or list) – Grain IDs of the parent grains to twin.
csl_label (str) – CSL type label (e.g.
'Σ3 (twin)'). Stored in output metadata.twin_half_width (float) – Nominal half-width in pixels of each twin lamella.
twin_angle_deg (float or None) – Fixed base inclination angle (degrees, from the x-axis). If
None, a random angle in [0°, 180°) is drawn independently for each parent.n_twins_per_parent (int) – Number of parallel twin lamellae to insert per parent grain.
angle_perturb_deg (float) – Maximum angular perturbation (degrees) applied independently to each lamella angle around the parent’s base angle. A uniform random draw in
[-angle_perturb_deg, +angle_perturb_deg]is used. Set to 0 for strictly parallel lamellae.width_perturb (float) – Maximum half-width perturbation (pixels) applied independently to each lamella. A uniform random draw in
[-width_perturb, +width_perturb]is added to twin_half_width. The effective half-width is clamped to a minimum of 0.5 px.rng_seed (int or None) – Seed for the random number generator (reproducibility).
- Returns:
A dict with keys:
{'lfi': ndarray, 'twin_lines': dict[parent_gid -> list[Sline2d]], 'new_twin_gids': dict[parent_gid -> list[new_gid]], 'csl_label': str}
- Return type:
- upxo.xtalphy.crystal_orientation.compute_scale_factor_grain_size(lfi_sim: numpy.ndarray, mean_area_ebsd_um2: float, ebsd_step_size_um: float, prop_ebsd: dict | None = None) dict[source]
Compute the linear scale factor (µm per sim-pixel) that makes the mean grain area of the synthetic label field equal to the EBSD mean grain area.
The derivation is:
\[s = \sqrt{\bar{A}_{\text{EBSD}} / \bar{A}_{\text{sim}}}\]where \(s\) is in µm/px, \(\bar{A}_{\text{EBSD}}\) is in µm², and \(\bar{A}_{\text{sim}}\) is in px².
- Parameters:
lfi_sim (ndarray (ny, nx)) – Synthetic label field (integer grain IDs, background ≤ 0).
mean_area_ebsd_um2 (float) – Mean grain area of the EBSD target in µm². Typically
rg.stat_ebsd['area']['mean'].ebsd_step_size_um (float) – EBSD step size in µm (used for reporting only).
prop_ebsd (dict or None) –
{grain_id: {'area': ..., ...}}dict from EBSD characterisation. When provided, per-grain EBSD areas are stored in the result for downstream plotting.
- Returns:
'scale_factor': float — µm per sim-pixel'mean_area_sim_px2': float — mean sim grain area (px²)'mean_area_ebsd_um2': float — target EBSD mean area (µm²)'mean_area_sim_um2': float — sim mean area after scaling (µm²)'ebsd_step_size_um': float'sim_domain_px': tuple (ny, nx)'sim_domain_um': ndarray (ny_um, nx_um) — physical size'gids_sim': ndarray — grain IDs from lfi_sim (> 0)'counts_sim': ndarray — pixel counts per grain'areas_sim_um2': ndarray — per-grain areas after scaling (µm²)'areas_ebsd_um2': ndarray or None — per-grain EBSD areas (µm²)'ratio_pixel_sizes': float — scale_factor / ebsd_step_size_um- Return type:
dict with keys
- upxo.xtalphy.crystal_orientation.compute_twin_thickness_stats(parent_info: dict, prop_ebsd: dict, step_size_um: float, thickness_key: str | None = None) dict[source]
Compute twin lamella thickness statistics from EBSD grain properties.
- Parameters:
parent_info (dict) – Output of
identify_parent_grains. Each value must expose'pure_twins'and'intermediates'arrays of EBSD grain IDs.prop_ebsd (dict) – Per-grain property dict
{grain_id: {'minor_axis_length': ..., ...}}.step_size_um (float) – EBSD step size in µm/pixel used to convert pixel lengths to µm.
thickness_key (str or None) – Key to use as the thickness proxy. If None (default), uses
'minor_axis_length'when present, otherwise'eq_diameter'.
- Returns:
'gids': list[int] — twin grain IDs with data'thick_px': ndarray(N,) — thickness in pixels'thick_um': ndarray(N,) — thickness in µm'col': str — property key used'step_um': float — step size used'mean': float'median': float'std': float'min': float'max': float'pct10': float (10th percentile)'pct90': float (90th percentile)- Return type:
dict with keys
- upxo.xtalphy.crystal_orientation.compute_s3_lamella_angle_2d(q: numpy.ndarray) float[source]
Compute the 2D S3 twin lamella trace angle in the sample XY plane.
The active {111} plane variant is the one whose sample-frame normal has the largest XY-plane projected norm. The lamella trace direction is perpendicular to that projected normal.
- upxo.xtalphy.crystal_orientation.assign_orientations_mdf_matched(lfi_after: numpy.ndarray, gs_roles: dict, twin_result: dict, ebsd_lfi: numpy.ndarray, ebsd_quat: numpy.ndarray, ebsd_parent_info: dict, rng_seed=None) dict[source]
Assign grain-averaged quaternion orientations to every domain in the twinned simulated label field lfi_after so that the grain-boundary MDF of the simulation approximates the EBSD target.
Assignment strategy
- Pure-parent sim grains
Sampled randomly from the pool of EBSD pure-parent grain-averaged quaternions.
- Geometrically-introduced twin grains (new IDs in twin_result)
Derived from the parent’s assigned quaternion via the Σ3 twinning rotation: q_twin = Σ3_Q ⊗ q_parent
- Pre-labelled pure-twin sim grains (from Section 15, not in twin_result)
Sampled from the EBSD pure-twin pool.
- Intermediate sim grains
Sampled from the EBSD intermediate pool.
- Non-role sim grains
Sampled from the EBSD non-role pool.
Pools are sampled with replacement when the pool is smaller than the number of grains needing assignment. The neighbour graph of lfi_after (4-connected pixel contacts) is returned so the caller can immediately compute the simulated MDF and compare it with the EBSD reference.
- param lfi_after:
Label field after twin introduction (integer grain IDs, 0 = background).
- type lfi_after:
ndarray (ny, nx)
- param gs_roles:
Output of
assign_grain_roles_by_ratio. Must contain keys'pure_parents','pure_twins','intermediates','non_role'each with a'grain_ids'entry that is a set of integer grain IDs.- type gs_roles:
dict
- param twin_result:
Output of
introduce_twins_by_csl. Must contain key'new_twin_gids': {parent_gid : [twin_gid, …]}.- type twin_result:
dict
- param ebsd_lfi:
EBSD label field (grain IDs, 0 = boundary / unindexed).
- type ebsd_lfi:
ndarray (ny_e, nx_e)
- param ebsd_quat:
Per-pixel quaternions from EBSD in (w, x, y, z) convention.
- type ebsd_quat:
ndarray (ny_e, nx_e, 4)
- param ebsd_parent_info:
Output of
find_csl_grains(keyed by CSL label). Each value must expose'pure_parents','pure_twins', and'intermediates'arrays of EBSD grain IDs.- type ebsd_parent_info:
dict
- param rng_seed:
Seed for the random-number generator (reproducibility).
- type rng_seed:
int or None
- returns:
'grain_quats'{grain_idndarray(4,)}Grain-averaged quaternion for every non-background grain in lfi_after.
'quat_pixel'ndarray(ny, nx, 4)Per-pixel quaternion map built by flooding each grain with its assigned quaternion.
'neigh_gid_sim'{grain_idlist[int]}4-connected contact neighbour graph of lfi_after.
- rtype:
dict with keys
- upxo.xtalphy.crystal_orientation.assign_parent_orientations(host_gids: numpy.ndarray, sim_lfi: numpy.ndarray, ebsd_parent_gids: numpy.ndarray, ebsd_lfi: numpy.ndarray, ebsd_quat: numpy.ndarray, rng_seed=None, max_retries: int = 50) dict[source]
Assign EBSD pure-parent quaternions to MC host grains with a neighbour-conflict-free constraint: no two directly adjacent host grains receive the same quaternion (which would imply zero misorientation between them, contradicting the existence of a grain boundary).
When the EBSD parent pool is exhausted (all remaining pool entries still conflict with every neighbour after max_retries redraws), the function falls back to FCC-texture quaternions generated by
tops.synth_fcc_quats.- Parameters:
host_gids (array-like of int) – MC grain IDs that will host twins.
ebsd_parent_gids (array-like of int) – EBSD pure-parent grain IDs (typically
parent_info[csl_label]['pure_parents']).ebsd_lfi (ndarray (ny_e, nx_e)) – EBSD grain label field (
rg.lfi_ebsd).ebsd_quat (ndarray (ny_e, nx_e, 4)) – Per-pixel EBSD quaternions in [w, x, y, z] convention (
rg.quat_ebsd).rng_seed (int or None) – Seed for reproducibility.
max_retries (int) – Maximum redraws from the EBSD pool before switching to FCC fallback for a given grain.
- Returns:
'host_quats': {gid: ndarray(4,)} — assigned quaternion per host'pool_size': int — unique EBSD parent orientations available'n_hosts': int'n_fallback': int — grains that needed the synth-FCC fallback- Return type:
dict with keys
- upxo.xtalphy.crystal_orientation.get_ks_rotations() numpy.ndarray[source]
Return the 24 Kurdjumov-Sachs (K-S) orientation relationship operators as (24, 3, 3) rotation matrices.
The K-S OR is defined by the relationship between FCC austenite and BCC martensite/ferrite:
{111}γ ∥ {110}α, <110>γ ∥ <111>α.- Returns:
ks_variants
- Return type:
ndarray, shape (24, 3, 3)
- upxo.xtalphy.crystal_orientation.exmp_cubic_misorientation()[source]
Demonstrate cubic_misorientation() for a single grain-boundary pair.
Computes the fundamental misorientation angle (degrees), the rotation axis, and the three smallest unique equivalent angles between two grains whose orientations are given as Bunge Euler angles.
- upxo.xtalphy.crystal_orientation.exmp_grain_boundary_misorientation_distribution()[source]
Demonstrate grain_boundary_misorientation_distribution() with explicit grain-boundary pairs and random per-grain Euler angles.
Prints histogram statistics and plots the GBMD bar chart.
- upxo.xtalphy.crystal_orientation.exmp_gbmd_from_lfi()[source]
Demonstrate gbmd_from_lfi() on a small synthetic label-field image.
A 20×20 pixel domain is partitioned into a regular 4×4 grid of grains (16 grains total) each given a random Bunge Euler angle. Grain boundaries are detected automatically from the label field.
- upxo.xtalphy.crystal_orientation.exmp_euler_bunge_to_matrix()[source]
Demonstrate euler_bunge_to_matrix() for well-known FCC texture components and verify R is orthogonal and det(R)=+1.
- upxo.xtalphy.crystal_orientation.exmp_normalize_euler_bunge()[source]
Demonstrate normalize_euler_bunge() on edge-case inputs including negative Phi, Phi > 180, and out-of-range phi1/phi2.
- upxo.xtalphy.crystal_orientation.exmp_fcc_symmetrise_ori()[source]
Demonstrate fcc_symmetrise_ori() for the Copper texture component. Prints the number of distinct symmetrically equivalent orientations and the first few triplets.
- upxo.xtalphy.crystal_orientation.exmp_ipf_color()[source]
Demonstrate ipf_color() for standard FCC texture components along [001].