upxo.pxtal.twinned_simple_3d.twin_generator_3d module

twin_generator_3d.py

Primary and secondary Sigma3 twin lamella introduction for the twinned simple 3D pipeline.

Path A note: this module is hardcoded for Sigma3 (FCC annealing twin) regardless of the CSL label selected in Part A. A warning is emitted when the selected CSL label is not ‘S3 (twin)’. Generalisation to other CSL types (Path B / csl-registry) is deferred – see project memory ‘csl-registry-path-b’.

class upxo.pxtal.twinned_simple_3d.twin_generator_3d.TwinGenerator3D(base, n_lamellae_per_host: int = 2, twin_nucleation_site: str = 'random_gb', tvf_tolerance: float = 0.05, twin_orient_scatter_deg: float = 1.5, meshing_route: str = 'conformal', min_lamella_thickness_conformal: int = 2, min_lamella_thickness_non_conformal: int = 1, min_host_vox_for_lamella: int = 8, prob_lamella_separated: float = 0.5, prob_lamella_contacting: float = 0.5, prob_secondary_outward_twinNucleation: float = 0.3, prob_secondary_inward_twinNucleation: float = 0.7, twin_thick_scale_factor: float = 1.0, tvf_2d_to_3d_scale_factor: float = 1.0, max_lamella_thickness_um: float | None = None, max_lamella_vf_per_host: float = 0.4, schmid_loading_direction: tuple = (0.0, 0.0, 1.0), host_schmid_weight: float = 0.0, use_schmid_for_variant_selection: bool = True, rng_seed: int | None = None)[source]

Bases: object

Introduces Sigma3 twin lamellae into a 3D grain structure.

Primary lamellae

Each host grain receives up to n_lamellae_per_host lamella carvings. The first lamella (k=0) always uses twin_nucleation_site to determine its plane origin. Subsequent lamellae (k >= 1) are placed according to prob_lamella_separated:

  • Separated (prob_lamella_separated): independent placement using twin_nucleation_site on the remaining host voxels.

  • Contacting (prob_lamella_contacting): geometric offset from lamella-0’s outer face, guaranteeing face-adjacency and hence Sigma9 misorientation between the two primary lamellae. twin_nucleation_site is overridden for this path.

Different {111} variants are pre-selected without replacement per grain so that contacting lamellae always use different variants, producing Sigma9 (not Sigma1/Sigma3) at the twin-twin boundary.

Secondary lamellae

Secondary twins nucleate from the primary twin’s surface according to prob_secondary_outward_twinNucleation:

  • Outward 2a: carved from the HOST grain at the primary-host interface centroid. Creates a direct planar Sigma9 boundary between secondary and host. twin_nucleation_site is overridden.

  • Inward 2b: carved from INSIDE the primary twin (nested). Creates a Sigma3 boundary with the primary and a Sigma9 boundary with the host if the lamella spans the full primary width. Uses twin_nucleation_site on the primary twin voxel set.

Volume-fraction control

cumulative_twin_vox is tracked across BOTH primary and secondary introduction. Introduction stops when the running twin VF exceeds tvf['overall_twin_frac'] * (1 + tvf_tolerance).

TWIN_TYPE_LABEL = 'Sigma3 -- FCC Annealing Twin (60 deg / <111>)'
DIAG_SHORTFALL_PCT_THRESHOLD = 70.0
DIAG_ABRUPT_FRAC_THRESHOLD = 0.5
DIAG_CLAMPED_FRAC_THRESHOLD = 0.3
DIAG_HOST_SATURATION_THRESHOLD = 0.95
DIAG_ANISOTROPY_RATIO_THRESHOLD = 1.5
base
n_lamellae_per_host
twin_nucleation_site
tvf_tolerance
twin_orient_scatter_deg
meshing_route
min_lamella_thickness_conformal
min_lamella_thickness_non_conformal
min_host_vox_for_lamella
prob_lamella_separated
prob_lamella_contacting
prob_secondary_outward_twinNucleation
prob_secondary_inward_twinNucleation
lgi_twinned: numpy.ndarray | None
primary_twin_quats: Dict
secondary_twin_quats: Dict
all_quats: Dict
twin_role: Dict
twin_parent_of: Dict
n_clamped: int
n_abrupt_primary: int
n_hosts_with_primary_twin: int
vf_stopped_early: bool
cumulative_twin_vox: int
twin_halfwidths_vox: Dict
twin_thick_scale_factor: float
tvf_2d_to_3d_scale_factor: float
max_lamella_thickness_um: float | None
max_lamella_vf_per_host: float
tvf_2d_ebsd: float
tvf_target_3d: float
schmid_loading_direction: tuple
host_schmid_weight: float
use_schmid_for_variant_selection: bool
introduce_primary_twins(host_orientations: Dict[int, numpy.ndarray], twin_thickness: Dict, tvf: Dict | None = None, tvf_stage1: float | None = None)[source]

Introduce primary Sigma3 twin lamellae into all designated host grains.

Parameters:
  • host_orientations (dict {gid: ndarray(4,)})

  • twin_thickness (dict) – Output of rg.compute_mc_twin_thickness(parent_info).

  • tvf (dict or None) – Output of rg.compute_ebsd_tvf(parent_info). Provides overall_twin_frac (VF stopping target) and csl_label (for Path A CSL warning).

introduce_secondary_twins(tvf: Dict, twin_thickness: Dict, tvf_2a: float | None = None, tvf_2b: float | None = None)[source]

Introduce secondary Sigma3 twins nucleated from the primary twin surface.

Nucleation mode (2a vs 2b) is chosen per event using prob_secondary_outward_twinNucleation.

2a (outward): carved from HOST grain at primary-host interface;

twin_nucleation_site overridden. Creates planar Sigma9 boundary between secondary and host.

2b (inward): carved inside the primary twin (nested);

twin_nucleation_site applied to primary twin voxels. Creates Sigma3 with primary and Sigma9 with host if it spans the full primary width.

compute_achieved_2d_tvf(n_slices_per_axis: int = 10, axes=('x', 'y', 'z'), return_raw: bool = False) → Dict[source]

Cross-sectional (2D) measurement of the achieved twin area fraction in lgi_twinned, for direct comparability with the EBSD 2D twin area fraction – EBSD data is inherently 2D, but tvf_achieved_3d is measured over the whole 3D structure, so neither is directly comparable to what a real 2D EBSD cross-section of the synthetic structure would show.

For each of n_slices_per_axis evenly-spaced 2D cross-sections along each axis in axes, computes twin pixel count / indexed pixel count (role in {'primary_twin', 'secondary_twin'} vs. any assigned role), then averages (mean and std) across every sampled slice – matching TwinnedSimple3DBase.assess_hosting_representativeness_2d’s convention of not filtering down to whichever slices happen to already be close to target. No cc3d re-labelling is needed here (unlike that method) since classification is by pixel value against twin_role, not by reassociating disconnected 2D regions back to an original 3D grain ID.

Must be called after introduce_primary_twins() (reads lgi_twinned / twin_role).

Parameters:
  • n_slices_per_axis (int) – Evenly-spaced 2D cross-sections sampled per axis.

  • axes (iterable of str) – Subset of ('x', 'y', 'z') (array axes 0/1/2 respectively – same convention as TwinnedSimple3DBase).

Returns:

  • dict with keys mean, std, n_slices_used, and (only when

  • return_raw=True) ratios – the raw per-slice twin area

  • fraction values, for callers needing the full distribution rather

  • than its mean/std (e.g. Summary Report’s EBSD-vs-MC comparison).

twin_volume_fraction() → float[source]

Return current twin VF in lgi_twinned.

summary() → Dict[source]

Return a rich summary dict of twin introduction results.

diagnose(tvf: Dict | None = None) → List[Dict][source]

Return advisory notes explaining surprising values in summary(), in a suggestive (not asserted) tone – these are heuristic hypotheses about why a number looks the way it does, not a pass/fail verdict. They never gate progression to the next GUI page.

Parameters:

tvf (dict or None) – The tvf dict passed to introduce_primary_twins() (output of rg.compute_ebsd_tvf). When provided, enables the CSL-label mismatch check. Not stored on the instance, so callers must pass it again here (kept out of __slots__ deliberately – it’s EBSD-side provenance, not twin-generation state).

Returns:

Each entry: {'anchor_fields': [...], 'message': str}. anchor_fields names the summary() keys the note explains (a GUI can attach “(Ref N)” to those fields, N = this note’s 1-based position in the returned list).

Return type:

list of dict

print_summary() → None[source]

Print a formatted, detailed summary of twin introduction results.

upxo.pxtal.twinned_simple_3d.twin_generator_3d.compute_twin_thickness_comparison(tg, cleaner, twin_thickness, validator)[source]

Three-way twin-thickness population comparison: EBSD target, actual 3D thickness as introduced, and apparent 2D thickness measured on representative slices of the cleaned structure.

Parameters:
  • tg (TwinGenerator3D) – Provides base.voxel_size and twin_halfwidths_vox (the actual per-twin half-width, in voxels, used during introduction).

  • cleaner – Post-cleaning structure (lgi_clean/twin_role_clean – duck-typed, matching StructureCleaner3D and its subset variants).

  • twin_thickness (dict) – EBSD twin thickness statistics, as produced by the EBSD Twin Thickness Statistics computation – must contain 'thick_um' (array-like) and 'mean'.

  • validator – A representativeness validator exposing slice_results: dict of axis name (‘X’/’Y’/’Z’) -> list of dicts each containing 'slice_idx', the representative slice positions to sample.

Returns:

‘ebsd_um’, ‘actual_3d_um’ndarray

Per-lamella thickness populations (microns).

’ebsd_mean_um’, ‘actual_3d_mean_um’ : float ‘per_axis_um’ : dict {axis_name: ndarray}

Apparent 2D thickness (minor-axis length of each twin’s cross-section) measured on the validator’s own representative slices, per axis.

’per_axis_means_um’ : dict {axis_name: float}

Return type:

dict with