upxo.pxtal.twinned_simple_3d package
Subpackages
- upxo.pxtal.twinned_simple_3d.steps package
- Submodules
- upxo.pxtal.twinned_simple_3d.steps.steps_base_grain_structure_mc module
- upxo.pxtal.twinned_simple_3d.steps.steps_distribution_viewer module
- upxo.pxtal.twinned_simple_3d.steps.steps_ebsd_analysis_1 module
- upxo.pxtal.twinned_simple_3d.steps.steps_ebsd_analysis_2 module
- upxo.pxtal.twinned_simple_3d.steps.steps_host_allocation module
- upxo.pxtal.twinned_simple_3d.steps.steps_orientation_assignment module
- upxo.pxtal.twinned_simple_3d.steps.steps_post_twin_validation module
- upxo.pxtal.twinned_simple_3d.steps.steps_pre_twin_validation module
- upxo.pxtal.twinned_simple_3d.steps.steps_repr_assessment_qualification module
- upxo.pxtal.twinned_simple_3d.steps.steps_self_repr_1 module
- upxo.pxtal.twinned_simple_3d.steps.steps_start module
- upxo.pxtal.twinned_simple_3d.steps.steps_subsetting module
- upxo.pxtal.twinned_simple_3d.steps.steps_temporal_slice_sweep module
- upxo.pxtal.twinned_simple_3d.steps.steps_transformations module
- upxo.pxtal.twinned_simple_3d.steps.steps_twin_generation module
- upxo.pxtal.twinned_simple_3d.steps.steps_visualization_export module
- Module contents
- Submodules
Submodules
- upxo.pxtal.twinned_simple_3d.abaqus_exporter_3d module
- upxo.pxtal.twinned_simple_3d.base_3d module
- base_3d.py
TwinnedSimple3DBaseTwinnedSimple3DBase.lgiTwinnedSimple3DBase.voxel_sizeTwinnedSimple3DBase.unitsTwinnedSimple3DBase.n_grainsTwinnedSimple3DBase.grain_idsTwinnedSimple3DBase.mpropTwinnedSimple3DBase.host_grain_idsTwinnedSimple3DBase.non_host_grain_idsTwinnedSimple3DBase.eligible_grain_idsTwinnedSimple3DBase.target_hosting_fractionTwinnedSimple3DBase.actual_hosting_fractionTwinnedSimple3DBase.host_fraction_2d_to_3d_scale_factorTwinnedSimple3DBase.host_ranking_volume_weightTwinnedSimple3DBase.n_pool_aTwinnedSimple3DBase.n_pool_bTwinnedSimple3DBase.allocation_summaryTwinnedSimple3DBase.from_mcgs()TwinnedSimple3DBase.char_morphology()TwinnedSimple3DBase.compute_aspect_ratio_bbox()TwinnedSimple3DBase.compute_2d_slice_properties_by_axis()TwinnedSimple3DBase.compute_2d_slice_properties()TwinnedSimple3DBase.allocate_twin_hosts()TwinnedSimple3DBase.allocate_twin_hosts_spatial()TwinnedSimple3DBase.assess_hosting_representativeness_2d()TwinnedSimple3DBase.domain_shape()TwinnedSimple3DBase.physical_size()TwinnedSimple3DBase.rank_temporal_slices_by_n()TwinnedSimple3DBase.compute_slice_grain_properties()TwinnedSimple3DBase.percentile_trim()TwinnedSimple3DBase.compare_property_distributions()TwinnedSimple3DBase.rescale_property_values()TwinnedSimple3DBase.calibrate_scale_factor()TwinnedSimple3DBase.plot_temporal_slice_3d()TwinnedSimple3DBase.calculate_lfi()TwinnedSimple3DBase.clean_temporal_slices()TwinnedSimple3DBase.clean_temporal_slices_recursive()TwinnedSimple3DBase.select_temporal_slice()
compute_dropped_features()
- upxo.pxtal.twinned_simple_3d.cleaning_3d module
- cleaning_3d.py
StructureCleaner3DStructureCleaner3D.upscale_fallbackStructureCleaner3D.split_jitter_degStructureCleaner3D.do_spike_removalStructureCleaner3D.do_lobe_splitStructureCleaner3D.min_clean_voxelsStructureCleaner3D.lgi_cleanStructureCleaner3D.all_quats_cleanStructureCleaner3D.twin_role_cleanStructureCleaner3D.twin_parent_of_cleanStructureCleaner3D.split_eventsStructureCleaner3D.spike_countStructureCleaner3D.n_splitsStructureCleaner3D.remaining_spikesStructureCleaner3D.remaining_multiStructureCleaner3D.upscale_appliedStructureCleaner3D.clean()StructureCleaner3D.check_remaining_defects()StructureCleaner3D.apply_upscale()StructureCleaner3D.split_events_report()StructureCleaner3D.jitter_report()StructureCleaner3D.clean_recursive()
- upxo.pxtal.twinned_simple_3d.feature_props_3d module
- feature_props_3d.py
compute_grain_volumes()compute_twin_volume_fraction()compute_equivalent_diameters()grain_role_statistics()compute_ebsd_mc_comparison_stats()print_ebsd_mc_comparison()assemble_ebsd_sgs_comparison_data()compute_sgs_role_property_distributions()format_size()build_summary_markdown_lines()
- upxo.pxtal.twinned_simple_3d.mc_qualification module
- upxo.pxtal.twinned_simple_3d.orientation_3d module
- orientation_3d.py
OrientationAssigner3DOrientationAssigner3D.baseOrientationAssigner3D.orientation_assignment_modeOrientationAssigner3D.pair_similarity_degOrientationAssigner3D.max_retriesOrientationAssigner3D.neigh_graphOrientationAssigner3D.parent_poolOrientationAssigner3D.full_poolOrientationAssigner3D.fallback_quatsOrientationAssigner3D.all_grain_orientationsOrientationAssigner3D.n_fallback_hostOrientationAssigner3D.n_fallback_nonhostOrientationAssigner3D.n_conflictsOrientationAssigner3D.mrf_max_sweepsOrientationAssigner3D.mrf_eps_convergenceOrientationAssigner3D.mrf_eps_qualityOrientationAssigner3D.mrf_init_modeOrientationAssigner3D.mrf_kde_thresholdOrientationAssigner3D.mrf_bilateral_symmetryOrientationAssigner3D.mrf_sa_t_startOrientationAssigner3D.mrf_sa_t_endOrientationAssigner3D.mrf_historyOrientationAssigner3D.cancel_eventOrientationAssigner3D.build_neighbour_graph()OrientationAssigner3D.build_ebsd_pools()OrientationAssigner3D.build_adjacency_model()OrientationAssigner3D.assign_host_orientations()OrientationAssigner3D.assign_nonhost_orientations()OrientationAssigner3D.check_conflicts()OrientationAssigner3D.compute_mdf()
compute_ebsd_pure_parents_for_csl()build_ebsd_merged_mdf()run_orientation_assignment()score_assigner_pole_figure()run_optimize_sweep()
- upxo.pxtal.twinned_simple_3d.repr_validator_3d module
- repr_validator_3d.py
RepresentativenessValidator3DRepresentativenessValidator3D.n_slices_xRepresentativenessValidator3D.n_slices_yRepresentativenessValidator3D.n_slices_zRepresentativenessValidator3D.test_along_xRepresentativenessValidator3D.test_along_yRepresentativenessValidator3D.test_along_zRepresentativenessValidator3D.p_percentageRepresentativenessValidator3D.p_percentage_xRepresentativenessValidator3D.p_percentage_yRepresentativenessValidator3D.p_percentage_zRepresentativenessValidator3D.wasserstein_thresholdRepresentativenessValidator3D.slice_resultsRepresentativenessValidator3D.axis_acceptanceRepresentativenessValidator3D.overall_acceptedRepresentativenessValidator3D.validate_morphological()RepresentativenessValidator3D.validate_crystallographic()RepresentativenessValidator3D.aggregate()RepresentativenessValidator3D.report()
compute_representative_slice_mdfs()
- upxo.pxtal.twinned_simple_3d.representativeness_metrics module
- upxo.pxtal.twinned_simple_3d.selfrepr_morphology module
- upxo.pxtal.twinned_simple_3d.selfrepr_qualification module
- upxo.pxtal.twinned_simple_3d.stride_study module
- upxo.pxtal.twinned_simple_3d.subsetting module
- upxo.pxtal.twinned_simple_3d.subsetting_2d module
- upxo.pxtal.twinned_simple_3d.twin_generator_3d module
- twin_generator_3d.py
TwinGenerator3DTwinGenerator3D.TWIN_TYPE_LABELTwinGenerator3D.DIAG_SHORTFALL_PCT_THRESHOLDTwinGenerator3D.DIAG_ABRUPT_FRAC_THRESHOLDTwinGenerator3D.DIAG_CLAMPED_FRAC_THRESHOLDTwinGenerator3D.DIAG_HOST_SATURATION_THRESHOLDTwinGenerator3D.DIAG_ANISOTROPY_RATIO_THRESHOLDTwinGenerator3D.baseTwinGenerator3D.n_lamellae_per_hostTwinGenerator3D.twin_nucleation_siteTwinGenerator3D.tvf_toleranceTwinGenerator3D.twin_orient_scatter_degTwinGenerator3D.meshing_routeTwinGenerator3D.min_lamella_thickness_conformalTwinGenerator3D.min_lamella_thickness_non_conformalTwinGenerator3D.min_host_vox_for_lamellaTwinGenerator3D.prob_lamella_separatedTwinGenerator3D.prob_lamella_contactingTwinGenerator3D.prob_secondary_outward_twinNucleationTwinGenerator3D.prob_secondary_inward_twinNucleationTwinGenerator3D.lgi_twinnedTwinGenerator3D.primary_twin_quatsTwinGenerator3D.secondary_twin_quatsTwinGenerator3D.all_quatsTwinGenerator3D.twin_roleTwinGenerator3D.twin_parent_ofTwinGenerator3D.n_clampedTwinGenerator3D.n_abrupt_primaryTwinGenerator3D.n_hosts_with_primary_twinTwinGenerator3D.vf_stopped_earlyTwinGenerator3D.cumulative_twin_voxTwinGenerator3D.twin_halfwidths_voxTwinGenerator3D.twin_thick_scale_factorTwinGenerator3D.tvf_2d_to_3d_scale_factorTwinGenerator3D.max_lamella_thickness_umTwinGenerator3D.max_lamella_vf_per_hostTwinGenerator3D.tvf_2d_ebsdTwinGenerator3D.tvf_target_3dTwinGenerator3D.schmid_loading_directionTwinGenerator3D.host_schmid_weightTwinGenerator3D.use_schmid_for_variant_selectionTwinGenerator3D.introduce_primary_twins()TwinGenerator3D.introduce_secondary_twins()TwinGenerator3D.compute_achieved_2d_tvf()TwinGenerator3D.twin_volume_fraction()TwinGenerator3D.summary()TwinGenerator3D.diagnose()TwinGenerator3D.print_summary()
compute_twin_thickness_comparison()
- upxo.pxtal.twinned_simple_3d.viz_3d module
Module contents
twinned_simple_3d
3D representative grain structure generation for simple twinned FCC polycrystals (e.g. OFHC copper, Cu-Cr-Zr).
Hierarchy: host grain -> primary twin -> secondary twin (2-3 levels).
This is the 3D counterpart of twinned_simple_2d, and the FCC
analogue of pxtal.fm_steel_3d for FM steels.
Public API
TwinnedSimple3DBase – core grain structure, host allocation TwinGenerator3D – primary and secondary twin introduction OrientationAssigner3D – conflict-free EBSD-sampled orientations StructureCleaner3D – voxel spike removal and lobe splitting RepresentativenessValidator3D – multi-axis 2D slice MDF validation
- class upxo.pxtal.twinned_simple_3d.TwinnedSimple3DBase(lgi: numpy.ndarray, voxel_size: float, units: str = 'microns', rng_seed: int | None = None)[source]
Bases:
objectFoundation 3D grain structure for the twinned simple FCC pipeline.
Wraps a 3D labelled grain image (lgi) from an SGC simulation, attaches physical dimensions, characterises grain morphology, and allocates twin host grains driven by the EBSD-measured twin hosting fraction. Entry point to the full pipeline.
- lgi
- voxel_size
- units
- n_grains
- grain_ids
- classmethod from_mcgs(pxt, tslice_key: int, voxel_size: float | None = None, units: str = 'microns', rng_seed: int | None = None)[source]
Construct from an
mcgssimulation object at a chosen time slice.- Parameters:
pxt (mcgs) – UPXO Synthetic Grain Structure (SGC) generator object (after
pxt.simulate()).tslice_key (int) – Key into
pxt.gs(i.e. one of the values inpxt.m).voxel_size (float or None) – Physical voxel edge length. Reads
pxt.vox_sizeif None.units (str) – Physical units, default ‘microns’.
rng_seed (int or None)
- char_morphology(volnv: bool = True, eqdia: bool = True, sanv: bool = False, force_compute: bool = True)[source]
Compute per-grain morphological properties and populate
self.mprop.Properties are stored as
{property_key: {grain_id: value}}.
- compute_aspect_ratio_bbox(force_compute: bool = True)[source]
Per-grain 3D aspect ratio from each grain’s axis-aligned bounding box:
max(extent) / min(extent)across the three voxel-count axes. Populatesself.mprop['aspect_ratio']({grain_id: float}).Bounding-box extents scale identically on every axis for an isotropic (cubic-voxel) structure, so the ratio itself is dimensionless and doesn’t need
voxel_size.This class has no other 3D shape-descriptor beyond volnv/eqdia/ sanv – the only other working 3D aspect-ratio logic anywhere in the codebase lives on the unrelated
mcgs3_grain_structureclass (set_mprop_arbbox); this reimplements the same bounding-box idea directly againstself.lgi, usingscipy.ndimage.find_objectsfor a single vectorised pass over every grain ID at once rather than onenp.whereper grain.- Parameters:
force_compute (bool) – Recompute even if already populated.
- compute_2d_slice_properties_by_axis(selected_props, comparison_axes: list | None = None, n_slices_per_axis: int | dict | None = None, connectivity_2d: int = 4) dict[source]
Same evenly-spaced-cross-section sampling as
compute_2d_slice_properties(), but returns per-axis, per-individual-slice breakdowns instead of one pooled array per property – needed by callers that need the individual slice populations, not just their union (e.g. a min/max KDE envelope band showing slice-to-slice spread, alongside the combined/ pooled distribution).compute_2d_slice_properties()is a thin wrapper around this method that concatenates everything back into flat pooled arrays, so both share exactly the same underlying sampling/ measurement code – see that method’s docstring for the shared parameter semantics.- Returns:
dict {axis – One ndarray per sampled slice position along that axis (in slice-position order), each holding that slice’s per-region values for that property (may be an empty array if no regions were found in that slice). Axes not present in comparison_axes are absent from the returned dict.
- Return type:
{prop_name: [ndarray, ndarray, …]}}
- compute_2d_slice_properties(selected_props, comparison_axes: list | None = None, n_slices_per_axis: int | dict | None = None, connectivity_2d: int = 4) dict[source]
Pool 2D grain-morphology properties from evenly-spaced cross-sections of
self.lgi, independent of any twin-host allocation (contrast withassess_hosting_representativeness_2d(), which readsself.host_grain_ids/self.eligible_grain_idsand requiresallocate_twin_hosts/allocate_twin_hosts_spatialto have already run – this method needs neither).Reuses the same evenly-spaced-cross-section /
cc3drelabelling skeleton asassess_hosting_representativeness_2d(), but poolsskimage.measure.regionprops-derived scalar properties per 2D region directly instead of tallying host/non-host ratios. A thin wrapper aroundcompute_2d_slice_properties_by_axis()that flattens its per-axis/per-slice breakdown back into one pooled array per property.- Parameters:
selected_props (iterable of str) – Subset of
{'area', 'eqdia', 'perimeter', 'aspect_ratio', 'solidity'}.comparison_axes (list of str or None) – Subset of
['x', 'y', 'z'](array axes 2/1/0 respectively – same convention asassess_hosting_representativeness_2d). Defaults to all three.n_slices_per_axis (int or dict or None) – Evenly-spaced 2D cross-sections sampled per axis – same int-or-
{'x': int, 'y': int, 'z': int}convention asassess_hosting_representativeness_2d’sn_comparison_slices. Defaults to 5 per axis.connectivity_2d (int) –
cc3dconnectivity for the 2D re-labelling (4 or 8).
- Returns:
dict {prop_name – Every sampled region’s value for that property, pooled across every sampled slice on every requested axis.
- Return type:
1-D ndarray}
- allocate_twin_hosts(target_hosting_fraction: float, min_host_voxels: int = 4, host_fraction_2d_to_3d_scale_factor: float = 1.0, host_ranking_volume_weight: float = 0.5)[source]
Designate twin host grains and populate
self.host_grain_ids.- Parameters:
target_hosting_fraction (float) – EBSD 2D hosting fraction from
rg.merge_info['twin_hosting_fraction'].min_host_voxels (int) – Grains smaller than this are ineligible to host (but still receive the ‘non_host’ role for visualisation).
host_fraction_2d_to_3d_scale_factor (float) – Stereological correction: the EBSD hosting fraction is a 2D apparent value; set > 1.0 to designate more hosts than the 2D fraction suggests. Effective target is capped at 1.0.
host_ranking_volume_weight (float (0.0 -- 1.0)) – Weight given to grain volume in the composite host-selection rank. 1.0 = pure volume sort (largest first). 0.5 = equal weight between volume and number of face-adjacent neighbours (higher coordination = more grain-boundary area = more twin nucleation sites).
- allocate_twin_hosts_spatial(target_hosting_fraction: float, min_host_voxels: int = 4, host_fraction_2d_to_3d_scale_factor: float = 1.0, host_ranking_volume_weight: float = 0.5, mis_fraction: float = 0.8, pool_b_size_measure: str = 'vol_vox', pool_b_band_method: str = 'std', pool_b_band_shape: str = 'between', pool_b_n_std: float = 1.0, pool_b_percentile_lo: float = 25.0, pool_b_percentile_hi: float = 75.0, neighbour_frac: float = 100.0, mis_runs: int = 20, seed: int = 0)[source]
Spatial-dispersal-constrained twin host selection.
Selects
n_targethost grains in two pools:Pool A — 80% (mis_fraction) from a maximal independent set (MIS): Grains in a MIS are non-adjacent, guaranteeing that no two selected hosts share a face. This prevents twins from different hosts from clustering and touching each other, which would create spurious twin-twin (Sigma9) boundaries and dilute the Sigma3 MDF peak.
maximal_independent_set(networkx) is calledmis_runstimes with different seeds; the largest result is used.Pool B — 20% (1 - mis_fraction) from a size band: The remaining fraction is drawn from non-MIS grains whose size falls in a selected band of the eligible-grain size distribution AND that have at least one neighbour also in that band. This allows physically realistic, limited host-host adjacency between similarly-positioned-in-size grains, confined to whichever part of the size distribution
pool_b_band_shapeselects (by default, the middle – avoiding abnormally large or small grain pairs – but'below'/'above'deliberately target the small/large tails instead, e.g. to stop large grains being systematically under-hosted).- Parameters:
target_hosting_fraction (float) – EBSD 2D hosting fraction.
min_host_voxels (int) – Minimum voxel count for eligibility.
host_fraction_2d_to_3d_scale_factor (float) – Multiplier on the 2D fraction for 3D target.
host_ranking_volume_weight (float) – 0 = rank by adjacency count, 1 = rank by volume, 0.5 = equal.
mis_fraction (float) – Fraction of n_target drawn from the MIS (default 0.80).
pool_b_size_measure (str) –
Size metric used to define the Pool B band. Options:
'vol_vox'– grain volume in voxels (integer count).'vol_um3'– grain volume in μm³ (vol_vox × voxel_size³).'eqdia_um'– sphere-equivalent diameter in μm= (6 × vol_um3 / π)^(1/3).
pool_b_band_method (str) –
How the band’s cutoff(s) are computed from the chosen size measure’s distribution over eligible grains:
'std'– cutoffs aremean ± pool_b_n_std × std.'percentile'– cutoffs arepool_b_percentile_lo/pool_b_percentile_hipercentiles of the distribution.Default
'std'(unchanged prior behaviour).pool_b_band_shape (str) –
- Which side(s) of the distribution the band covers:
'below'–(-inf, hi](near-smaller grains only).'between'–[lo, hi](near-mean; default, unchangedprior behaviour).
'above'–[lo, +inf)(near-larger grains only,uncapped).
lo/hiare whichever of the method’s two cutoffs apply to the selected shape – e.g.'below'only ever uses the upper cutoff,'above'only the lower one.pool_b_n_std (float) – Used when
pool_b_band_method == 'std'. Half-band width (for'between') or single-sided offset (for'below'/'above') in units of the standard deviation of the chosen size measure. Default 1.0.pool_b_percentile_lo (float) – Used when
pool_b_band_method == 'percentile'(0-100).pool_b_percentile_lois the band’s lower cutoff (used by'between'and'above');pool_b_percentile_hiis the upper cutoff (used by'between'and'below'). Defaults 25.0 / 75.0.pool_b_percentile_hi (float) – Used when
pool_b_band_method == 'percentile'(0-100).pool_b_percentile_lois the band’s lower cutoff (used by'between'and'above');pool_b_percentile_hiis the upper cutoff (used by'between'and'below'). Defaults 25.0 / 75.0.neighbour_frac (float) – Percentage (0-100) of Pool B drawn from in-band grains that DO have an in-band neighbour, vs. in-band grains that do NOT. 100 (default) reproduces the original, neighbour-only behaviour exactly. 0 draws entirely from non-neighbouring in-band grains. Any other value mixes the two proportionally. Whichever sub-pool is short of its share falls back to drawing the remainder from the other sub-pool (so the total Pool B count is unaffected by this split, only its do/do-not composition is).
mis_runs (int) – Number of random MIS calls; the largest result is kept.
seed (int) – Base seed for the MIS random runs.
- assess_hosting_representativeness_2d(n_comparison_slices: int | dict = 5, comparison_axes: list | None = None, connectivity_2d: int = 4) dict[source]
Cross-sectional (2D) reassessment of the host/non-host allocation already computed by
allocate_twin_hosts_spatial, for direct comparability with a 2D EBSD-measured hosting fraction – EBSD data is inherently 2D, but host selection above operates on the whole 3D structure, so neitheractual_hosting_fraction(a 3D volume fraction) nor a raw 3D grain-count fraction is directly comparable to it.Must be called AFTER
allocate_twin_hosts_spatial(readsself.host_grain_ids/self.eligible_grain_ids).For each of
n_comparison_slicesevenly-spaced 2D cross-sections ofself.lgialong each axis incomparison_axes:Re-run
cc3d.connected_componentson the 2D slice (connectivity_2d) – a single 3D-connected grain can appear as multiple spatially disconnected pieces within a given cross-section (Monte-Carlo grains can be highly convoluted), so tallying by the ORIGINAL 3D grain ID directly would misrepresent what a real 2D cross-section actually shows. This reassociation runs unconditionally on every slice – the disconnection is invisible from raw ID counts alone, and its extent depends onconnectivity_2d(a mismatch between whatever connectivity producedself.lgiand this 2D re-slicing connectivity can throw the count off).Reassociate each freshly 2D-labelled region back to its underlying original 3D grain ID (a single lookup is safe – a 2D-connected region can never straddle two different original IDs, since
cc3donly ever splits same-ID regions further, never merges differing ones) and thereby to its host / eligible-non-host / ineligible classification.Tally, per slice: the COUNT-based ratio (number of discrete 2D host regions / number of discrete 2D eligible [host + non-host] regions) and the AREA-based ratio (host pixel count / eligible pixel count).
Both ratios are then averaged (mean and std) across every sampled slice – deliberately NOT filtered down to whichever slices happen to already be close to target, which would make the reported “achieved” value circular. The std is itself a useful representativeness diagnostic: a large spread means the host distribution is spatially inhomogeneous, so any single real EBSD cross-section would be a noisy comparison point regardless of how well this mean matches.
- Parameters:
n_comparison_slices (int or dict) – Evenly-spaced 2D cross-sections sampled per axis. Either one int applied uniformly to every axis in
comparison_axes(unchanged from before), or a{'x': int, 'y': int, 'z': int}dict for a different count per axis – axes missing from the dict are not sampled, same as omitting them fromcomparison_axes.comparison_axes (list of str or None) – Subset of
['x', 'y', 'z'](array axes 0/1/2 respectively – same convention asrank_temporal_slices_by_n). Defaults to all three.connectivity_2d (int) –
cc3dconnectivity for the 2D re-labelling (4 or 8).
- Returns:
dict with keys
count_ratio_mean,count_ratio_std,area_ratio_mean,area_ratio_std(Noneif no sliceyielded a usable ratio),
n_slices_used, andper_slice(list of ``{‘axis’, ‘location’, ‘count_ratio’, ‘area_ratio’,
’n_host_2d’, ‘n_eligible_2d’}`` dicts, one per sampled
cross-section, for diagnostic inspection).
- domain_shape()[source]
Return (nx, ny, nz) voxel dimensions of the domain.
self.lgiis stored in the raw MC array’s native (nz, ny, nx) axis order (seeplot_temporal_slice_3d()’s P/R/C convention comment) – reversed here so the RETURNED tuple is genuinely (nx, ny, nz) as documented.
- physical_size()[source]
Return physical domain size (Lx, Ly, Lz) in
self.units.See
domain_shape()–self.lgi.shapeis reversed first so this is genuinely (Lx, Ly, Lz), not (Lz, Ly, Lx).
- classmethod rank_temporal_slices_by_n(pxt, start: int = 0, step: int = 5, ebsd_n_parents: int | None = None, n_comparison_slices: int = 5, comparison_axes: list | None = None, selected_props: list | None = None, voxel_size: float | None = None, verbose: bool = True) list[source]
Rank MC temporal slices by their 2D-equivalent grain count.
The 3D total grain count is not directly comparable to a 2D EBSD cross-section count. Instead, this method extracts
n_comparison_slicesevenly-spaced 2D slices along each axis incomparison_axes, counts grains per slice, and reports the mean across all sampled slices asn_grains_2d_avg. This is the correct quantity to compare against the EBSD parent grain count.Reads the persisted LFI (
pxt.gs[t].lgi, fromcalculate_lfi()) directly rather than re-deriving a fresh connected-component labelling from the raw spin-state array (pxt.gs[t].s) – a fresh relabelling of.sis not guaranteed to reproduce the same grain identity as the persisted, possibly-cleaned LFI once true grain count exceeds the number of Monte-Carlo states (seeclean_temporal_slices()’s docstring). Requirescalculate_lfi()to have already been run for every ranked slice.- Parameters:
pxt (mcgs) – Simulated mcgs object (after
pxt.simulate()).start (int) – Starting positional index into
pxt.m.step (int) – Positional increment – every
step-th slice is sampled.ebsd_n_parents (int or None) – EBSD pure-parent grain count (2D); if provided, a ratio
n_grains_2d_avg / ebsd_n_parentsis computed and the default widget selection is set to the closest match.n_comparison_slices (int) – Number of evenly-spaced 2D slices to extract per axis. Default 5.
comparison_axes (list of str or None) – Axes to extract slices along. Any subset of
['x', 'y', 'z']. Defaults to all three if None.selected_props (list of str or None) – Optional – subset of
upxo.viz.ebsdviz.GRAIN_ROLE_ALL_PROPS('area','aspect_ratio','perimeter','solidity','n_neighbours'). When given, pools these per-grain morphological properties from the SAME already-2D -relabelled slices used for the grain-count ranking above (viaregionprops/adjacency_from_labels), fusing in whatcompute_slice_grain_properties()would otherwise compute from a SECOND, independentcc3d.connected_componentspass over the identical slices – for a caller (Synthetic GS Assessment’s “Rank Temporal Slices”) that always wants both, this halves the per-slice relabelling cost. Each returned row then also carries'prop_stats_raw'. LeaveNone(the default) for a plain ranking with no property pooling.voxel_size (float or None) – Only used when
selected_propsis given – forwarded to the same area/perimeter scalingcompute_slice_grain_properties()applies. Defaults tomean(pxt.vox_size)if None.
- Returns:
tslice_key,n_grains_2d_avg,n_slices_used,ratio(None if no reference), plus the full-3D topology counts derived from the SAMElgi_3dlabelling already computed here (no extra labelling pass):n_grains_3d(total 3D grain count),n_face_grains(grains with at least one voxel on any of the 6 RVE boundary faces),n_interior_grains(n_grains_3d - n_face_grains), andpct_ng_in_3d(percentage ofn_grains_3dthat are completely interior – no voxel on any of the 6 RVE faces –Noneifn_grains_3dis 0). Also, from the SAME per-slicelgi_2drelabelling already done forn_grains_2d_avg(no extra labelling pass):pct_ng_in_2d_mean/pct_ng_in_2d_std– mean/std, across every sampled 2D cross-section, of the percentage of that slice’s grains with no pixel on any of the slice’s 4 edges (the 2D analogue ofpct_ng_in_3d, comparable to a real EBSD map’s own edge-grain fraction – seeebsdviz.compute_pct_interior_grains). Plus, only whenselected_propsis given:prop_stats_raw–{prop_name: ndarray}, identical in definition tocompute_slice_grain_properties()’s return value.- Return type:
- classmethod compute_slice_grain_properties(pxt, tslice_key: int, selected_props: list, comparison_axes: list | None = None, n_comparison_slices: int = 5, voxel_size: float | None = None) dict[source]
Pool per-grain morphological property values across 2D cross-sections of one MC temporal slice, sampled the same way
rank_temporal_slices_by_n()samples slices for grain counting.At this pipeline stage the synthetic structure has no twin/parent role labels yet (those only exist on the EBSD side, post
identify_parent_grains) – every grain found in the sampled cross-sections is pooled, matching the “untwinned whole grain” analogue of an EBSD parent grain.Property definitions mirror
upxo.viz.ebsdviz.compute_grain_role_property_distributions:areaandperimeterare scaled once byvoxel_size(in the corresponding power),aspect_ratioismajor_axis_length / minor_axis_length,solidityis skimage’s ownarea / convex_area, andn_neighbourscomes from face-adjacency on the labelled 2D slice.Reads the persisted LFI (
pxt.gs[t].lgi, fromcalculate_lfi()) directly rather than re-deriving a fresh connected-component labelling frompxt.gs[t].s– seerank_temporal_slices_by_n()’s docstring for why. Requirescalculate_lfi()to have already been run for this slice.- Parameters:
pxt (mcgsV1_1 (or mcgs)) – Simulated grain-growth object (after
pxt.simulate()).tslice_key (int) – Key into
pxt.gs.selected_props (list of str) – Subset of
upxo.viz.ebsdviz.GRAIN_ROLE_ALL_PROPS.comparison_axes (list of str or None) – Axes to sample, subset of
['x', 'y', 'z']. Defaults to all three.n_comparison_slices (int) – Evenly-spaced 2D slices to extract per axis. Default 5.
voxel_size (float or None) – Physical voxel edge length (assumed isotropic in-plane, same assumption the EBSD side makes with a single
step_size). Reads the mean ofpxt.vox_sizeif None.
- Returns:
{prop_name: ndarray of pooled values}, one entry per requested property.- Return type:
- static percentile_trim(arr: numpy.ndarray, low_pct: float = 0.0, high_pct: float = 100.0) numpy.ndarray[source]
Keep only values within [low_pct, high_pct] percentiles of arr’s own range – a direct, user-facing percentile-of-range outlier control, distinct from
_iqr_trim’s fixed 1.5*IQR Tukey fence (that rule has no percentile knob at all; this one exists specifically for UI controls that ask the user for an explicit low/high percentile pair).- Parameters:
low_pct (float) – 0-100. Values below the low_pct percentile or above the high_pct percentile are dropped.
(0, 100)(the default) is a no-op.high_pct (float) – 0-100. Values below the low_pct percentile or above the high_pct percentile are dropped.
(0, 100)(the default) is a no-op.define (Returns arr unchanged if too few points (< 2) to)
meaningfully (percentiles)
drops (or if the resulting mask)
returning (everything (keeps the original array rather than)
empty
behaviour). (matching _iqr_trim's own fallback)
- classmethod compare_property_distributions(ebsd_vals, synth_vals, trim_left: bool = True, trim_right: bool = True, score_function: str = 'exp') dict | None[source]
Compare an EBSD property distribution against a synthetic candidate’s, after removing outliers from each side independently (IQR / Tukey’s rule).
Uses Wasserstein distance and energy distance (both scipy, operating directly on the two 1D sample arrays, in the property’s own units) plus a KS-test-derived similarity score. These three specifically for property-distribution comparison, not (yet) for network-level grain-structure representativeness assessment, which is a separate, later piece of work.
Wasserstein/energy distance are unbounded and expressed in the property’s own units, so they’re neither comparable across properties (Area’s µm² vs Perimeter’s µm) nor usable directly as a [0, 1] “representativeness” score. Both are first made dimensionless by dividing by the EBSD reference’s own trimmed std (its natural scale of variability), then mapped to [0, 1] via
_squash_distance.ks_similarityis already bounded in [0, 1] (1 - KS statistic) and used as-is. The mean of the three isrepresentativeness_score– an absolute (not candidate-set-relative) [0, 1] measure, comparable across properties and stable regardless of what other candidates exist.- Parameters:
ebsd_vals (array-like) – Raw per-grain property values (not pre-aggregated).
synth_vals (array-like) – Raw per-grain property values (not pre-aggregated).
trim_left (bool) – Which Tukey-fence side(s) to trim before comparing – applied identically to both the EBSD and synthetic sides. See
_iqr_trim.trim_right (bool) – Which Tukey-fence side(s) to trim before comparing – applied identically to both the EBSD and synthetic sides. See
_iqr_trim.score_function (str) –
'exp'(default) or'reciprocal'– see_squash_distance.
- Returns:
{'wasserstein': float, 'energy': float, 'ks_similarity': float, 'ratio': float, 'wasserstein_score': float, 'energy_score': float, 'representativeness_score': float}–ratiois (trimmed synthetic mean) / (trimmed EBSD mean), the same quantity the property-column star tolerance check is applied to.Noneif either side has no values left after trimming.- Return type:
dict or None
- static rescale_property_values(prop_name: str, values, scale_factor: float)[source]
Convert one candidate’s raw (voxel-unit) property values into physical units using an already-derived linear scale factor (micrometres per voxel edge length – see
calibrate_scale_factor).Only length-based properties change: area scales as (scale_factor)**2, perimeter as (scale_factor)**1. Aspect ratio, solidity, and n_neighbours are dimensionless/topological and are returned unchanged – rescaling voxel size cannot affect them.
- classmethod calibrate_scale_factor(pxt, tslice_key: int, ebsd_area_vals, synth_area_vals, ebsd_perimeter_vals=None, synth_perimeter_vals=None, trim_left: bool = True, trim_right: bool = True, cross_check_tolerance_pct: float = 5.0) dict | None[source]
Derive the physical voxel edge length (micrometres/voxel) that makes this candidate temporal slice’s mean 2D cross-sectional grain area match the EBSD reference’s mean grain area, then sanity-check it against an independently-derived perimeter-based scale factor.
Method (see the twinned_simple_3d GUI design discussion this implements):
Outlier-trim (IQR/Tukey) both the EBSD and synthetic Area distributions independently, take their means.
Area scales as (length)**2, so
scale_factor = sqrt(ebsd_mean_area / synth_mean_area_voxels)is the implied voxel edge length in the EBSD’s physical units. This is computed independently per candidate – deliberately no relationship is assumed across temporal slices, since MC step number here is used purely as a stochastic generator of candidate grain topologies, not a physical growth timeline.If Perimeter values are also available, repeat with Perimeter (which scales as length**1, no sqrt) as an independent cross-check: if the two scale factors disagree by more than
cross_check_tolerance_pct, the candidate’s grains have a different area-to-perimeter relationship (shape family) than the real EBSD grains, so forcing an area match there would not be physically meaningful –cross_check_okis False.
Caveats callers/UI should surface to the user (see the GUI design notes this implements):
The perimeter cross-check is measured on a voxel/pixel grid, so it is biased by discretisation (“staircase”) error – coarser voxel resolution per grain inflates measured perimeter more, which can fail the cross-check for a purely digitisation reason rather than a genuine shape mismatch.
n_synth_grains/ mean voxels-per-grain should be considered before trusting a failed cross-check as a real shape difference.The scale factor is itself a mean-based estimate – candidates with few grains (e.g. heavily coarsened, late-stage slices) give a statistically noisier estimate.
n_ebsd_grainsandn_synth_grains(the outlier-trimmed sample sizes actually used) are returned so this can be judged.This calibrates size only. It says nothing about whether grain shape/anisotropy is representative – that is judged separately by the property distribution comparisons (
compare_property_distributions).
- Parameters:
pxt (mcgsV1_1 (or mcgs)) – Simulated grain-growth object (after
pxt.simulate()).tslice_key (int) – Key into
pxt.gs– used to read the candidate’s voxel grid shape for the implied RVE size.ebsd_area_vals (array-like) – Raw per-grain Area values (EBSD in physical units; synthetic in voxel units).
synth_area_vals (array-like) – Raw per-grain Area values (EBSD in physical units; synthetic in voxel units).
ebsd_perimeter_vals (array-like or None) – Raw per-grain Perimeter values, for the cross-check. If either is None, the cross-check is skipped (
cross_check_okis None, not False).synth_perimeter_vals (array-like or None) – Raw per-grain Perimeter values, for the cross-check. If either is None, the cross-check is skipped (
cross_check_okis None, not False).trim_left (bool) – Outlier-trim sides, applied identically to area and perimeter (matching whatever the Area/Perimeter columns are configured with elsewhere).
trim_right (bool) – Outlier-trim sides, applied identically to area and perimeter (matching whatever the Area/Perimeter columns are configured with elsewhere).
cross_check_tolerance_pct (float) – Max allowed relative disagreement between the area- and perimeter-derived scale factors for the cross-check to pass.
- Returns:
{'scale_factor': float, 'n_ebsd_grains': int, 'n_synth_grains': int, 'scale_factor_perimeter': float or None, 'cross_check_ok': bool or None, 'cross_check_diff_pct': float or None, 'implied_rve_size_um': (x, y, z)}–Noneif area data is insufficient (empty after trimming) to derive a scale factor at all.- Return type:
dict or None
- classmethod plot_temporal_slice_3d(pxt, tslice_key: int, title: str | None = None)[source]
Render one MC temporal slice’s persisted LFI (
pxt.gs[t].lgi, fromcalculate_lfi()) in a separate PyVista window, each grain shown in a distinct colour via a qualitative colormap.Reads the persisted LFI directly rather than re-deriving a fresh connected-component labelling from
pxt.gs[t].s– seerank_temporal_slices_by_n()’s docstring for why. Requirescalculate_lfi()to have already been run for this slice.
- classmethod calculate_lfi(pxt, tslice_keys: list | None = None, verbose: bool = True) dict[source]
Compute and persist the Labelled Feature Index (LFI –
.lgi, a slight generalisation of “local”/”labelled grain index”) for every saved MC temporal slice at once, viagstslice.char_morphology_of_grains– the same callfrom_mcgs()uses lazily, run here explicitly and in bulk instead.Run this once, right after simulation and before
clean_temporal_slices()– cleaning then operates directly on this persisted LFI rather than re-deriving a fresh label field from.son demand, giving every downstream consumer (cleaning,from_mcgs, and anything else that readsgstslice.lgi) a single, explicitly-computed source of truth instead of a lazily-cached value whose freshness relative to.swas previously only guaranteed by GUI click ordering.- Parameters:
- Returns:
{tslice_key: {'n_grains': int}}- Return type:
- classmethod clean_temporal_slices(pxt, min_grain_size: int = 4, tslice_keys: list | None = None, do_merge_small: bool = True, do_spike_removal: bool = True, verbose: bool = True) dict[source]
Clean every saved MC temporal slice’s persisted LFI (
pxt.gs[t].lgi) in place: merge small grains into their largest neighbour, then remove spike voxels – the same two defect classesupxo.pxtal.twinned_simple_3d.cleaning_3d.StructureCleaner3Dalready handles for post-twin structures, generalised to the base (pre-twin) MC structure here.Requires
calculate_lfi()to have already been run for every targeted slice (raises ifpxt.gs[t].lgiis missing) – cleaning operates directly on the persisted LFI rather than re-deriving a fresh connected-component labelling frompxt.gs[t].son every call, so there is a single, explicit, trusted source of truth for “the current grain labelling” that every downstream consumer (ranking, property stats, GS Plot,from_mcgs) can rely on without guessing whether it reflects the latest cleaning pass.The cleaned result is written back to BOTH
pxt.gs[t].lgi(the persisted LFI, now updated) andpxt.gs[t].s(as STATE values, not label IDs, so.skeeps meaning “MC state” for any code that still reads it directly) – kept in sync so nothing downstream needs to be aware cleaning happened elsewhere.Calling this again on an already-cleaned slice cleans the ALREADY-CLEANED LFI a second time (not fresh raw simulation data) –
.lgiand.sare already in sync after the first pass, so a second pass with the samemin_grain_sizeis normally a no-op; it only does something new ifmin_grain_sizechanged since the first pass. The GUI surfaces this explicitly before re-cleaning.Small-grain removal is a 3D generalisation of
upxo.pxtalops.gssmooth2d._merge_small_grains(2D-only, Shapely +find_neighs2d-based), built instead from the shape-agnostic primitivesgid_ops.find_small_fidsandnetops.neighops.adjacency_from_labels(3D viacc3d.contacts), with every pass’s merge decisions applied via a single vectorised lookup-table relabelling rather than a per-graingid_ops.merge_label_intocall. Spike removal reusesStructureCleaner3D._fast_clean_spikesdirectly.- Parameters:
pxt (mcgsV1_1 (or mcgs)) – Simulated grain-growth object (after
pxt.simulate()andcalculate_lfi()).pxt.gs[t].lgiandpxt.gs[t].sare both overwritten in place for every cleaned slice.min_grain_size (int) – Grains with fewer voxels than this are merged into their largest (by voxel count) neighbour, repeated until no grain remains below the threshold (or it has no neighbours left to merge into).
tslice_keys (list of int or None) – Which
pxt.mentries to clean. Defaults to all of them.do_merge_small (bool) – Run Stage 1 (small-grain merging). Disabling it leaves grains below
min_grain_sizeuntouched.do_spike_removal (bool) – Run Stage 2 (spike-voxel removal). Disabling it leaves spike voxels untouched.
verbose (bool) – Print a per-slice summary.
- Returns:
{tslice_key: {'n_small_merged': int, 'n_spikes_removed': int}}- Return type:
- classmethod clean_temporal_slices_recursive(pxt, min_grain_size: int = 4, tslice_keys: list | None = None, do_merge_small: bool = True, do_spike_removal: bool = True, n_passes: int = 1, verbose: bool = True) Tuple[dict, int][source]
Repeats
clean_temporal_slices()with the same settings up ton_passestimes, stopping early the moment a pass finds nothing left to merge or reassign – a single pass can leave a grain that only drops belowmin_grain_sizeafter that pass’s own spike removal, which a subsequent pass then catches.- Parameters:
n_passes (int) – Maximum number of cleaning passes. 1 reproduces a single
clean_temporal_slices()call exactly.parameters (Other)
- Returns:
(cumulative, n_passes_run) – cumulative :
{tslice_key: {'n_small_merged': int, 'n_spikes_removed': int}}– summed across every pass run. n_passes_run : how many passes actually ran (<= n_passes; less if convergence was reached early).- Return type:
- classmethod select_temporal_slice(rank_info: list, ebsd_n_parents: int | None = None)[source]
Interactive single-select widget for choosing a temporal slice.
Displays RadioButtons with one option per entry in rank_info. The default selection is the slice whose grain count is closest to ebsd_n_parents (if provided), otherwise the first entry.
- Parameters:
rank_info (list of dict) – Output of
rank_temporal_slices_by_n().ebsd_n_parents (int or None) – EBSD pure-parent grain count shown in the header.
- Returns:
Read
.valueafter running the cell to get the chosentslice_key.- Return type:
ipywidgets.RadioButtons
- class upxo.pxtal.twinned_simple_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:
- class upxo.pxtal.twinned_simple_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:
objectConflict-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_orientationsand fallback / conflict counters.- base
- orientation_assignment_mode
- pair_similarity_deg
- max_retries
- parent_pool: numpy.ndarray | None
- full_pool: numpy.ndarray | None
- fallback_quats: numpy.ndarray | None
- 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().
- assign_host_orientations()[source]
Assign orientations to host grains.
Dispatches to the active
orientation_assignment_mode.
- class upxo.pxtal.twinned_simple_3d.StructureCleaner3D(upscale_fallback: bool = False, split_jitter_deg: float = 0.0, rng_seed: int | None = None, min_clean_voxels: int = 0, do_spike_removal: bool = True, do_lobe_split: bool = True)[source]
Bases:
objectPost-twin voxel topology cleaner for the twinned simple 3D pipeline.
Stage 1 – Vertex spike removal (one global atomic pass, vectorised). Stage 2 – Lobe splitting via a single cc3d pass on the full array.
If defects persist and
upscale_fallbackis True the structure is doubled in each spatial dimension (~8x voxel count) and both stages are re-applied.- upscale_fallback
- split_jitter_deg
- lgi_clean: numpy.ndarray | None
- clean(lgi: numpy.ndarray, all_quats: Dict, twin_role: Dict, twin_parent_of: Dict)[source]
Run Stage 1 (spike removal) + Stage 2 (lobe splitting) on the post-twin grain structure using fast vectorised local methods.
Grains with fewer than
self.min_clean_voxelsvoxels are skipped in Stage 2. Set via themin_clean_voxelsconstructor argument.
- apply_upscale(lgi: numpy.ndarray, factor: int = 2) numpy.ndarray[source]
- jitter_report() str[source]
Report focused specifically on the orientation jitter applied to split-off lobes – split_events_report() covers every split event (parent, voxel size, jitter all together in one line); this isolates just the jitter part with summary statistics (min/max/ mean applied jitter), since that’s the piece someone tracking orientation-noise/meshing concerns wants on its own, not mixed in with lobe size/parent bookkeeping.
- classmethod clean_recursive(lgi: numpy.ndarray, all_quats: Dict, twin_role: Dict, twin_parent_of: Dict, n_passes: int = 1, upscale_fallback: bool = False, split_jitter_deg: float = 0.0, rng_seed: int | None = None, min_clean_voxels: int = 0, do_spike_removal: bool = True, do_lobe_split: bool = True, verbose: bool = True) Tuple[StructureCleaner3D, int][source]
Repeats
clean()up ton_passestimes, feeding each pass’s cleaned output (lgi_clean/all_quats_clean/twin_role_clean/ twin_parent_of_clean) into the next pass’s input, stopping early the moment a pass finds nothing left to fix (no spikes removed, no lobes split) – a single pass can occasionally leave a spike that only appears after that pass’s own lobe-splitting step, which a subsequent pass then catches.Constructs a fresh
StructureCleaner3Dfor every pass (same constructor arguments each time). The returned cleaner’sspike_count/n_splits/split_eventsare overwritten with the cumulative totals/merged history across every pass actually run, so callers see the full picture regardless of how many passes it took. Mergingsplit_eventsdicts across passes is safe – each pass’s lobe-splitting numbering starts from that pass’s own already-higher array max (it inherits the previous pass’s new grain IDs baked into the array), so passes never reuse the same new grain ID for a different lobe.- Parameters:
n_passes (int) – Maximum number of cleaning passes. 1 reproduces a single
clean()call exactly.parameters (Other)
- Returns:
(cleaner, n_passes_run) – cleaner : the final pass’s cleaner, with cumulative spike_count/n_splits/split_events across every pass run. n_passes_run : how many passes actually ran (<= n_passes; less if convergence was reached early).
- Return type:
- class upxo.pxtal.twinned_simple_3d.RepresentativenessValidator3D(n_slices_x: int = 10, n_slices_y: int = 10, n_slices_z: int = 10, test_along_x: bool = True, test_along_y: bool = True, test_along_z: bool = True, p_percentage: float = 60.0, p_percentage_x: float | None = None, p_percentage_y: float | None = None, p_percentage_z: float | None = None, wasserstein_threshold: float = 0.5)[source]
Bases:
objectMulti-axis 2D slice representativeness validator.
Two modes: - morphological (pre-twin): Wasserstein distance on normalised
grain-size distributions.
crystallographic (post-twin): Wasserstein distance on misorientation angle distributions.
A 3D structure is accepted when at least
p_percentagepercent of slices pass on every tested axis.- n_slices_x
- n_slices_y
- n_slices_z
- test_along_x
- test_along_y
- test_along_z
- p_percentage
- p_percentage_x
- p_percentage_y
- p_percentage_z
- wasserstein_threshold
- validate_morphological(lgi_3d: numpy.ndarray, ebsd_areas: numpy.ndarray)[source]
Pre-twin morphological validation against EBSD grain-size distribution.
Calls
section_from_3dandget_grain_size_distribution_from_sliceper slice.
- validate_crystallographic(lgi_3d: numpy.ndarray, quat_3d: numpy.ndarray, ebsd_miso_deg: numpy.ndarray)[source]
Post-twin crystallographic validation against EBSD MDF.
Calls
extract_2d_slice_pair,find_neighs2d, andcompute_mdf_from_quatsper slice.