upxo.pxtal.twinned_simple_3d.orientation_3d module

orientation_3d.py

Conflict-free crystallographic orientation assignment for the twinned simple 3D pipeline.

Three progressive orientation assignment modes are available, selectable via orientation_assignment_mode:

Level 0 'conflict_free'

Orientations sampled independently from the EBSD pool with a conflict-free constraint (no two adjacent grains receive the same quaternion). Texture correlations between neighbours are NOT reproduced. Fastest; used as the baseline.

Level 1 'paired_pool'

Orientations sampled from the set of ACTUALLY-OBSERVED adjacent pure-parent orientation pairs in the EBSD. When grain G is being assigned and a neighbour N already has orientation q_N, the algorithm searches the EBSD pair pool for pairs where one member is close to q_N and proposes the other member as q_G. This directly reproduces the neighbour-to-neighbour orientation correlations (texture) from the EBSD, fixing the systematic MDF right-shift of Level 0.

Level 2a 'mdf_conditioned_pairs'

Extends Level 1 by conditioning the pair selection on the EBSD parent-state MDF. A target misorientation angle theta is sampled from the reference MDF, and the search is restricted to the angle bin of the pre-binned EBSD pair pool that corresponds to theta. This reproduces both the orientation-pair identity (Level 1) AND the misorientation angle distribution (Level 2a). Requires build_adjacency_model(mdf_ref=mdf_merged). Fallback chain: target bin -> any bin (Level 1) -> uniform pool. Bin width = 65 / n_bins, inferred automatically from mdf_ref.

Level 2b 'mdf_analytical'

Samples target misorientation angle theta from the EBSD MDF, generates a rotation quaternion of angle theta about a random axis, applies it to the anchor orientation q_N to produce a candidate, then snaps to the nearest EBSD pool member. Covers more of orientation space than pair-pool methods while remaining anchored to physically-observed EBSD orientations. Requires build_adjacency_model(mdf_ref=mdf_merged). Note: cubic-symmetry reduction means the actual crystallographic disorientation will be <= theta; the distribution is biased toward theta and converges with enough realisations.

Level 3a 'mrf_gibbs'

Full Markov Random Field treatment via Gibbs sampling: grain orientation is sampled from P(q_G | {q_nb}) proportional to the product of EBSD P_MDF densities across all assigned neighbours. Sweeps until W1 convergence against the EBSD reference MDF.

Level 3b 'mrf_map'

MRF MAP estimation via Simulated Annealing. Same energy function as Level 3a but uses temperature-scaled argmax (SA) instead of stochastic sampling. Geometric cooling from mrf_sa_t_start to mrf_sa_t_end over mrf_max_sweeps sweeps; converges to pure MAP (argmax) at T→0.

class upxo.pxtal.twinned_simple_3d.orientation_3d.OrientationAssigner3D(base, orientation_assignment_mode: str = 'conflict_free', pair_similarity_deg: float = 10.0, max_retries: int = 50, mrf_max_sweeps: int = 20, mrf_eps_convergence: float = 0.3, mrf_eps_quality: float = 1.5, mrf_init_mode: str = 'mdf_analytical', mrf_kde_threshold: int | None = None, mrf_bilateral_symmetry: bool = False, mrf_sa_t_start: float = 5.0, mrf_sa_t_end: float = 0.05, rng_seed: int | None = None, cancel_event: Event | None = None)[source]

Bases: object

Conflict-aware crystallographic orientation assignment for 3D SGC grains.

Assigns per-grain unit quaternions from EBSD-derived pools, controlled by orientation_assignment_mode (increasing physical fidelity):

  • 'conflict_free' — independent sampling, no adjacent duplicate

  • 'paired_pool' — EBSD-observed neighbour orientation pairs

  • 'mdf_conditioned_pairs' — pairs conditioned on parent MDF bins

  • 'mdf_analytical' — MDF-sampled misorientation + pool snap

  • 'mrf_gibbs' / 'mrf_map' — full MRF (Gibbs / SA MAP)

See the module docstring for full level descriptions. Outputs all_grain_orientations and fallback / conflict counters.

base
orientation_assignment_mode
pair_similarity_deg
max_retries
neigh_graph: Dict | None
parent_pool: numpy.ndarray | None
full_pool: numpy.ndarray | None
fallback_quats: numpy.ndarray | None
all_grain_orientations: Dict
n_fallback_host: int
n_fallback_nonhost: int
n_conflicts: int
mrf_max_sweeps: int
mrf_eps_convergence: float
mrf_eps_quality: float
mrf_init_mode: str
mrf_kde_threshold: int | None
mrf_bilateral_symmetry: bool
mrf_sa_t_start: float
mrf_sa_t_end: float
mrf_history: list
cancel_event
build_neighbour_graph(connectivity: int = 6)[source]

Build the face-connected neighbour graph for the SGC grain structure.

build_ebsd_pools(ebsd_lfi: numpy.ndarray, ebsd_quat: numpy.ndarray, parent_info: Dict, csl_label: str, n_fallback: int = 500, fallback_tc_info: Dict | None = None, fallback_apply_symmetry: Dict[str, bool] | None = None)[source]

Build parent-only and full-EBSD quaternion pools.

fallback_tc_info / fallback_apply_symmetry : optional overrides for the synthetic fallback pool’s texture-component recipe (name -> [volume_fraction, [phi1, Phi, phi2], …] / name -> bool). Both default to None, which reproduces the prior unconditional behaviour unchanged: tops.synth_fcc_quats’ own hardcoded default recipe (Copper/Brass/Goss/Rotated-Cube), fully symmetry-expanded.

build_adjacency_model(rg, parent_info: Dict, csl_label: str, mdf_ref: dict | None = None)[source]

Level 1 – build the EBSD orientation adjacency model.

Extracts every adjacent pure-parent grain pair from the EBSD neighbour graph and records their orientation pair (q_A, q_B). Also builds a full-grain pair pool (all adjacent EBSD grain pairs) for non-host orientation assignment.

Must be called after build_ebsd_pools().

Parameters:
  • rg (repgen2d) – EBSD analysis object (provides lfi_ebsd, quat_ebsd, neigh_gid_ebsd).

  • parent_info (dict) – Output of rg.identify_parent_grains().

  • csl_label (str) – CSL label key into parent_info.

assign_host_orientations()[source]

Assign orientations to host grains.

Dispatches to the active orientation_assignment_mode.

assign_nonhost_orientations()[source]

Assign orientations to non-host grains.

Dispatches to the active orientation_assignment_mode.

check_conflicts() → int[source]

Count adjacent grain pairs sharing identical orientations.

compute_mdf(lgi: numpy.ndarray | None = None, n_bins: int = 65, angle_range: Tuple[float, float] = (0.0, 65.0)) → Dict[source]

Compute MDF for the current grain structure.

upxo.pxtal.twinned_simple_3d.orientation_3d.compute_ebsd_pure_parents_for_csl(rg, parent_info, csl_label)[source]

EBSD orientations of “pure parent” grains for the given CSL label – parent_info[csl_label][‘pure_parents’]: grains that appear as the LARGER member in every CSL pair they’re part of (identify_parent_ grains), i.e. grains that, in the real EBSD data, actually host a twin of this type and never are one themselves.

Returns (ebsd_quats, pure_parent_ids), or (None, None) if csl_label isn’t in parent_info or no pure-parent grain has a computed quaternion.

upxo.pxtal.twinned_simple_3d.orientation_3d.build_ebsd_merged_mdf(rg, parent_info, n_bins, angle_max)[source]

Build (or reuse) the EBSD twin-merged parent-state MDF reference – used both as build_adjacency_model’s mdf_ref (Levels 2a/2b/3a/3b) and as a pre-twin MDF comparison target, since the pre-twin structure has no twins yet and comparing against the still-twinned full EBSD MDF would be an apples-to-oranges comparison.

upxo.pxtal.twinned_simple_3d.orientation_3d.run_orientation_assignment(*, base, rg, parent_info, csl_label, ori_mode, rng_seed, connectivity, n_fallback, fallback_tc_info, fallback_apply_symmetry, mdf_n_bins, mdf_angle_max, pair_similarity_deg, max_retries, mrf_max_sweeps, mrf_eps_convergence, mrf_eps_quality, mrf_init_mode, mrf_kde_threshold, mrf_bilateral_symmetry, mrf_sa_t_start, mrf_sa_t_end, cancel_event=None, prebuilt_pools=None)[source]

Builds and runs one full OrientationAssigner3D for the given mode and seed – the standard build_neighbour_graph -> build_ebsd_pools -> build_adjacency_model -> assign_host_orientations -> assign_nonhost_orientations -> check_conflicts sequence.

prebuilt_poolsoptional (parent_pool, full_pool, fallback_quats)

tuple. build_ebsd_pools() re-derives the EBSD parent/full pools from scratch (pure waste if identical across many calls, e.g. within one Optimize Mapping sweep) AND resamples a fresh, unseeded random fallback pool every time. When given, this skips build_ebsd_pools entirely and assigns the three pools directly, so a whole sweep (and any rerun of one of its candidates) shares one single, already-built set of pools. Omit for a standalone interactive run, where the fallback pool should stay freshly randomized on every call.

upxo.pxtal.twinned_simple_3d.orientation_3d.score_assigner_pole_figure(assigner, base, zi_ebsd, mask, pole_family, grid_points, unit_normalize)[source]

Scores one freshly-built assigner’s host-grain pole figure against the (already unit-normalized, if requested) EBSD reference grid.

upxo.pxtal.twinned_simple_3d.orientation_3d.run_optimize_sweep(*, base, rg, parent_info, csl_label, selected, connectivity, n_fallback, fallback_tc_info, fallback_apply_symmetry, mdf_n_bins, mdf_angle_max, pair_similarity_deg, max_retries, mrf_max_sweeps, mrf_eps_convergence, mrf_eps_quality, mrf_init_mode, mrf_kde_threshold, mrf_bilateral_symmetry, mrf_sa_t_start, mrf_sa_t_end, pole_family, grid_points, unit_normalize, base_seed, cancel_event=None)[source]

Sweeps orientation-assignment mode/seed combinations, scoring each against a real-EBSD reference pole figure, and returns every run’s score plus the assigner objects for the 10 lowest-IQR (best-matching) runs.

Builds the EBSD reference pole figure once, then for every (mode, iteration) pair in selected (an iterable of (mode, n) pairs) runs a full orientation assignment with a fresh seed via run_orientation_assignment, scores it against the reference via score_assigner_pole_figure, ranks all runs by IQR, and re-runs the top 10 to regenerate their full assigner objects for later inspection – cheap relative to the full sweep, and avoids holding every single run’s assigner in memory at once.

Returns (records, top10): records is a list of per-run score dicts (all runs), top10 is a list of {**meta, ‘assigner’: assigner} for the 10 lowest-IQR runs.