upxo.pxtal.fm_steel_3d.base_3d module
FMSteel3DBase Implementation: Grain detection, neighbor graphs, PAG clustering.
Phase 1 working implementation of the base grain structure class. Entry point for the microstructure generation pipeline.
- class upxo.pxtal.fm_steel_3d.base_3d.FMSteel3DBase(lgi: numpy.ndarray, physical_dimensions: PhysicalDimensions, voxel_size: float, units: str = 'microns', connectivity: int = 6, grain_locs: Dict[int, numpy.ndarray] | None = None, n_grains: int = 0, neigh_gid: Dict[int, List[int]] | None = None, min_grain_nvoxels: int = -1, random_seed: int | None = None, verbosity: int = 0, log_sink=None)[source]
Bases:
objectFoundation FM steel grain structure from labeled grain image (LFI).
Entry point to the pipeline. Detects grains, builds neighbor graph, optionally cleans small grains, then enables PAG clustering.
- lgi
- physical_dimensions
- voxel_size
- units
- connectivity
- grain_locs
- n_grains
- neigh_gid
- min_grain_nvoxels
- classmethod from_lfi(lfi: numpy.ndarray, physical_dimensions: Tuple[float, float, float], voxel_size: float = 1.0, units: str = 'microns', connectivity: int = 6, min_grain_nvoxels: int = -1, grain_cleanup_max_passes: int = 100, random_seed: int | None = None, verbosity: int = 0, log_sink=None) FMSteel3DBase[source]
Create FMSteel3DBase instance from a labeled grain image.
This is the primary factory method. It initializes grain locations, computes neighbor relationships, and optionally performs cleanup (dissolution of small grains) if min_grain_nvoxels >= 0.
- Parameters:
lfi (np.ndarray) – 3D labeled grain image. Shape (nx, ny, nz). Values are grain IDs (1, 2, …, n_grains).
physical_dimensions (tuple[float, float, float]) – Physical domain size as (Lx, Ly, Lz).
voxel_size (float, optional) – Size of each voxel. Default 1.0. Must be positive.
connectivity (int, optional) – Grain connectivity (6, 18, or 26). Default 6 (face connectivity only). Values: 6 = face, 18 = face+edge, 26 = face+edge+corner.
min_grain_nvoxels (int, optional) – Minimum grain size in voxels. If >= 0, triggers cleanup via clean_gs_GMD_by_source_erosion_v1. Grains below threshold are dissolved into neighboring larger grains. Default -1 (no cleanup).
random_seed (int, optional) – Random seed for reproducibility. If provided, numpy random state is set at initialization.
- Returns:
Fully initialized grain structure instance.
- Return type:
- Raises:
ValueError – If lfi is not 3D, connectivity is invalid, or physical_dimensions are invalid.
Examples
>>> lfi = np.random.randint(1, 100, size=(50, 50, 50)) >>> fm = FMSteel3DBase.from_lfi(lfi, physical_dimensions=(100.0, 100.0, 100.0)) >>> print(fm.n_grains)
- compute_grain_locations() Dict[int, numpy.ndarray][source]
Compute voxel coordinates for each grain from LGI.
Single sort-and-split pass over all non-background voxels — O(N log N) in voxel count, independent of grain count. Replaces an earlier per-grain np.argwhere(lgi == gid) loop that rescanned the full array once per grain (O(n_grains * N)); on a 100^3 domain with ~3000 grains that loop took ~7s vs ~0.08s here (verified equivalent output on randomized structures before replacing).
- Returns:
Maps grain_id → (n_voxels, 3) array of voxel coordinates.
- Return type:
Notes
Results are cached in self.grain_locs after first call.
- compute_neighbor_network(connectivity: int | None = None) Dict[int, List[int]][source]
Compute grain neighbor relationships using cc3d.region_graph.
- Parameters:
connectivity (int, optional) – Connectivity type (6, 18, 26). If None, uses self.connectivity.
- Returns:
Maps grain_id → list of neighbor grain_ids.
- Return type:
Notes
Results are cached in self.neigh_gid after first call. Uses cc3d.region_graph for efficient neighbor detection.
- clean_small_grains(threshold: int, parameter_metric: str = 'mean', max_passes: int = 100) FMSteel3DBase[source]
Dissolve grains smaller than threshold into larger neighbors.
Reimplements clean_gs_GMD_by_source_erosion_v1 from mcgs3_temporal_slice.py to work independently. Iteratively merges small grains with their largest neighboring grains until all remaining grains are >= threshold voxels.
Does NOT modify self – returns a new FMSteel3DBase instance with the cleaned grain structure. Callers must use the return value, e.g.
fm = fm.clean_small_grains(threshold=10).- Parameters:
threshold (int) – Minimum grain size (voxels). Grains with fewer voxels are dissolved.
parameter_metric (str, optional) – Reserved for future sink-selection strategies (‘mean’, ‘max’, etc., matching the original clean_gs_GMD_by_source_erosion_v1 API). Currently unused: this reimplementation always merges into the largest neighboring grain regardless of the value passed.
- Returns:
New instance with the cleaned grain structure.
- Return type:
Notes
This is called automatically by from_lfi if min_grain_nvoxels >= 0.
Each pass is fully vectorized: grain sizes come from a single np.bincount, and neighbour-label detection for ALL small grains in the pass is done via whole-array shifted-slice comparisons (one pair of slices per connectivity offset, not per grain/voxel), followed by a single array-wide relabel. This replaced an earlier per-grain, per-voxel, per-offset Python loop; verified to produce identical grain-size distributions on randomized test structures (including a deliberately chained small-into-small-into-large merge case), while running roughly 500-650x faster on a 100^3 / ~3000-grain domain. A pass’s merge decisions are all computed from that pass’s start-of- pass snapshot and applied together, rather than the previous implementation’s incidental order-dependence (where a grain processed later in the same pass could see an earlier grain’s already-applied merge) — final converged results are equivalent, occasionally reaching convergence in a different number of passes.
- remove_spikes()[source]
Remove spike voxels (face-connected same-grain neighbour count <= 1).
Each spike voxel is reassigned to whichever neighbouring grain has the most face-touching voxels. Single vectorised detection pass, small Python loop only over the (typically very few) spike voxels. Adapted from StructureCleaner3D._fast_clean_spikes in the twinned_simple_3d pipeline.
- Returns:
(new_instance, spike_count)
- Return type:
- get_grain_statistics() Dict[str, any][source]
Compute basic statistics on grain structure.
- Returns:
Keys: ‘n_grains’, ‘min_voxels’, ‘max_voxels’, ‘mean_voxels’, ‘median_voxels’, ‘total_voxels’, ‘domain_voxels’.
- Return type:
- generate_pag_clusters(pag_size_distribution: Dict, pag_grain_fraction: float = 1.0, use_non_neigh_pag: bool = False, isolated_grain_strategy: str = 'auto', isolated_size_tol: float = 0.25, random_seed: int | None = None) FMSteel3DWithPAGs[source]
Partition grains into PAGs (Prior Austenite Grains) via neighbour clustering.
Uses stochastic breadth-first-search on the grain neighbour graph to form PAGs. Returns a new FMSteel3DWithPAGs instance.
- Parameters:
pag_size_distribution (dict) –
Dict with keys ‘sizes’ (list of target cluster sizes) and ‘probs’ (list of corresponding probabilities, must sum to ~1.0).
- Example: {‘sizes’: [3, 4, 5, 6, 7],
’probs’: [0.10, 0.30, 0.40, 0.15, 0.05]}
pag_grain_fraction (float, optional) – Volume fraction of the grain structure that participates in PAG clustering (0.0–1.0, measured in voxels). The remaining volume becomes isolated grains. Default 1.0 (all grains participate).
use_non_neigh_pag (bool, optional) – If True, each new PAG seed is chosen from grains that do not yet neighbour any formed PAG, keeping PAGs spatially separated. As pag_grain_fraction increases this becomes impossible; once no non-neighbour candidates remain the algorithm falls back to a random seed from any remaining unclustered grain. Default False (original random-seed behaviour).
isolated_grain_strategy (str, optional) –
Strategy for selecting which grains become isolated. Ignored when pag_grain_fraction >= 1.0. Options:
- ’auto’ — BFS runs until the volume target is met; remaining
grains become isolated (original behaviour).
’random’ — random independent set (no two isolated grains touch). ‘smallest’ — smallest-first independent set. ‘largest’ — largest-first independent set. ‘boundary’ — domain-boundary grains preferred in the independent set. ‘near_mean’ — grains closest to mean grain size preferred (primary:
within isolated_size_tol of mean; fallback: by distance).
Default ‘auto’.
isolated_size_tol (float, optional) – Fractional tolerance around the mean grain size for the ‘near_mean’ strategy. E.g. 0.25 accepts grains within ±25% of the mean voxel count as primary candidates before falling back. Default 0.25.
random_seed (int, optional) – Random seed for reproducibility.
- Returns:
New instance with computed PAG hierarchy.
- Return type:
- class upxo.pxtal.fm_steel_3d.base_3d.PhysicalDimensions(Lx: float, Ly: float, Lz: float)[source]
Bases:
objectContainer for physical domain dimensions (Lx, Ly, Lz).
- classmethod from_tuple(dims_tuple: Tuple[float, float, float])[source]
Create from (Lx, Ly, Lz) tuple.
- as_array() numpy.ndarray[source]
Return as (Lx, Ly, Lz) array.