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).

Parameters:
  • phi1 (float) – Bunge Euler angles.

  • Phi (float) – Bunge Euler angles.

  • phi2 (float) – Bunge Euler angles.

  • degrees (bool) – If True, inputs are in degrees. Default True.

Returns:

R

Return type:

ndarray, shape (3, 3)

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.

Parameters:
  • phi1 (array-like, shape (N,)) – Bunge Euler angles.

  • Phi (array-like, shape (N,)) – Bunge Euler angles.

  • phi2 (array-like, shape (N,)) – Bunge Euler angles.

  • degrees (bool) – Default True.

  • dtype (numpy dtype) – Output dtype.

Returns:

R

Return type:

ndarray, shape (N, 3, 3)

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.

Returns:

(phi1, Phi, phi2) – In degrees (default) or radians.

Return type:

tuple of float

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°]

Parameters:
  • ea (array-like, shape (..., 3) or (3,))

  • degrees (bool)

  • eps (float) – Snap tolerance near 0 and 180.

Return type:

Normalized array, same shape as input.

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.

Parameters:
Returns:

R

Return type:

ndarray, shape (3, 3)

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.

Parameters:
  • rstack (ndarray, shape (N, 3, 3))

  • angles (ndarray, shape (N,) in radians)

Returns:

axes

Return type:

ndarray, shape (N, 3)

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.

Parameters:
  • euler_deg (array-like, shape (3,)) – Bunge Euler angles in degrees.

  • sample_direction (array-like, shape (3,)) – Sample reference direction. Default [001].

Returns:

(r, g, b)

Return type:

tuple of float in [0, 1]

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.

  • angle_range ((float, float)) – Default (0, 65).

Returns:

  • Same dict as grain_boundary_misorientation_distribution plus

  • an extra key 'gb_pairs' containing the deduplicated set of

  • adjacent 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:

ndarray, shape (N, 3, 3)

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:
  • lfi (ndarray, shape (ny, nx), dtype int) – Integer grain label field. Positive values are grain IDs; values ≤ 0 are ignored.

  • quat (ndarray, shape (ny, nx, 4)) – Per-pixel unit quaternions in [w, x, y, z] convention.

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 by EBSDReader.characterise() or find_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, uses CUBIC_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' – fitted gaussian_kde object '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 to CUBIC_CSL.

  • csl_tol (float) – Tolerance in degrees. Pairs with |Δθ| csl_tol are 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:

dict

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:
  • lfi (ndarray, shape (ny, nx)) – Integer grain label field. Positive values = grain IDs; ≤0 = unindexed.

  • csl_grains (dict) – Output of segregate_csl_pairs().

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 pairs

  • pure_twins – appear as the smaller grain in every one of their pairs

  • intermediates – appear as parent in some pairs and twin in others

Parameters:
  • csl_grains (dict) – Output of segregate_csl_pairs().

  • prop (dict) – Grain property dict as returned by EBSDReader.characterise()['prop'] or stored in repgen2d.prop_ebsd. Must contain key 'area' for each grain.

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_twin whose direct parent (the larger grain in its CSL pair) is itself a pure_parent. It is a first-generation twin with no twins of its own.

A secondary twin is a pure_twin whose direct parent is an intermediate grain (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_info with 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:

dict

Notes

pure_twins primary_twins secondary_twins (the sets are disjoint and their union equals the existing pure_twins array).

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:
Returns:

Keys: 'total', 'pure_parents', 'pure_twins', 'intermediates', 'non_role', 'all_role'. Each value is a sub-dict with 'count' and 'ratio'.

Return type:

dict

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 EBSD prop_ebsd dict or from a mcgs2_grain_structure.prop DataFrame 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:

dict

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:

dict

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.

Parameters:

q (array-like, shape (4,)) – Host grain quaternion [w, x, y, z].

Returns:

Lamella trace angle in degrees, in [0°, 180°).

Return type:

float

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.

  • sim_lfi (ndarray (ny, nx)) – MC grain label field.

  • 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].

upxo.xtalphy.crystal_orientation.exmp_get_ks_rotations()[source]

Demonstrate get_ks_rotations(). Prints shape, verifies all returned matrices are proper rotations.

upxo.xtalphy.crystal_orientation.exmp_all()[source]

Run every self-sufficient example in sequence. Suitable for a quick sanity-check of the module.