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:
objectIntroduces Sigma3 twin lamellae into a 3D grain structure.
Primary lamellae
Each host grain receives up to
n_lamellae_per_hostlamella carvings. The first lamella (k=0) always usestwin_nucleation_siteto determine its plane origin. Subsequent lamellae (k >= 1) are placed according toprob_lamella_separated:Separated (prob_lamella_separated): independent placement using
twin_nucleation_siteon 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_siteis 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_siteis 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_siteon the primary twin voxel set.
Volume-fraction control
cumulative_twin_voxis tracked across BOTH primary and secondary introduction. Introduction stops when the running twin VF exceedstvf['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
- 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.
- 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_siteoverridden. Creates planar Sigma9 boundary between secondary and host.- 2b (inward): carved inside the primary twin (nested);
twin_nucleation_siteapplied 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, buttvf_achieved_3dis 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_axisevenly-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 – matchingTwinnedSimple3DBase.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 againsttwin_role, not by reassociating disconnected 2D regions back to an original 3D grain ID.Must be called after
introduce_primary_twins()(readslgi_twinned/twin_role).- Parameters:
- Returns:
dict with keys
mean,std,n_slices_used, and (only whenreturn_raw=True)ratios– the raw per-slice twin areafraction values, for callers needing the full distribution rather
than its mean/std (e.g. Summary Report’s EBSD-vs-MC comparison).
- 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
tvfdict passed tointroduce_primary_twins()(output ofrg.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_fieldsnames thesummary()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:
- 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_sizeandtwin_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