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: object

Foundation 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:

FMSteel3DBase

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:

dict[int, np.ndarray]

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:

dict[int, list[int]]

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:

FMSteel3DBase

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:

tuple[FMSteel3DBase, int]

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:

dict

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:

FMSteel3DWithPAGs

class upxo.pxtal.fm_steel_3d.base_3d.PhysicalDimensions(Lx: float, Ly: float, Lz: float)[source]

Bases: object

Container for physical domain dimensions (Lx, Ly, Lz).

Lx: float
Ly: float
Lz: float
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.