upxo.pxtal.twinned_simple_3d.cleaning_3d module

cleaning_3d.py

Post-twin voxel topology cleaning for the twinned simple 3D pipeline.

Both cleaning stages use fully vectorised local implementations that are O(1) in grain count (single array pass), replacing the per-grain loop approach in the generic gsdataops.grid_ops versions which scale poorly on large domains.

class upxo.pxtal.twinned_simple_3d.cleaning_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: object

Post-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_fallback is True the structure is doubled in each spatial dimension (~8x voxel count) and both stages are re-applied.

upscale_fallback
split_jitter_deg
do_spike_removal: bool
do_lobe_split: bool
min_clean_voxels: int
lgi_clean: numpy.ndarray | None
all_quats_clean: Dict | None
twin_role_clean: Dict | None
twin_parent_of_clean: Dict | None
split_events: Dict
spike_count: int
n_splits: int
remaining_spikes: int
remaining_multi: int
upscale_applied: bool
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_voxels voxels are skipped in Stage 2. Set via the min_clean_voxels constructor argument.

check_remaining_defects() → Dict[source]
apply_upscale(lgi: numpy.ndarray, factor: int = 2) → numpy.ndarray[source]
split_events_report() → str[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 to n_passes times, 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 StructureCleaner3D for every pass (same constructor arguments each time). The returned cleaner’s spike_count/n_splits/split_events are 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. Merging split_events dicts 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:

(StructureCleaner3D, int)