graintrace.fragmentation#

Grain fragmentation and reorientation analysis.

FragmentationAnalyzer groups the reorientation and fragmentation capabilities that share the same crystal symmetry and column conventions:

  • reorientation (misorientation-from-a-reference-step) as a rare-event scalar;

  • orientation-based point-set segmentation (graph/Leiden) and its reduction to a per-grain table, shared by the simulation and experiment paths;

  • fragmentation detection: simulation intragranular sub-grains (FragmentationAnalyzer.grain_fragments()) and experiment grain splits across load steps (FragmentationAnalyzer.detect_splits()).

The segmenter uses a fixed broad RBF sigma (sigma_auto collapses on near-identical intra-grain edges and shatters grains), a hard misorientation edge cutoff, gamma chosen by percolation, then fragment absorption and an adjacency-only re-merge.

Heavy dependencies (torch, neml2, scipy, networkit via GraphSpatialCluster) are imported lazily inside the methods.

class graintrace.fragmentation.FragmentationAnalyzer(symmetry='432', ori_cols=('ori_rodrigues_x', 'ori_rodrigues_y', 'ori_rodrigues_z'), coord_cols=('x', 'y', 'z'))[source]#

Bases: object

Reorientation and fragmentation analysis for simulation and experiment data.

Holds the crystal symmetry and the simulation orientation/coordinate column conventions shared by the methods. The experiment split detector (detect_splits()) takes its own column params because grain tables use a different schema (Eul0/1/2, X/Y/Z).

Parameters:
  • symmetry (str) – crystal symmetry in orbifold notation (default "432").

  • ori_cols (Sequence[str]) – per-element neml2 MRP orientation columns in the simulation field CSVs (default ori_rodrigues_x/y/z).

  • coord_cols (Sequence[str]) – per-element coordinate columns in the simulation field CSVs (default x/y/z).

reorientation_from_reference(results, step, ref_step=None)[source]#

Per-element misorientation angle (deg) from a reference load step.

Joins the per-element field CSVs at ref_step and step on id and returns the symmetry-reduced misorientation between the two orientations at each shared element. The ori_cols hold neml2 v3 MRP orientations.

Parameters:
  • results – a SimulationResults whose field_dir holds per-element CSVs.

  • step (int) – field-step index to measure.

  • ref_step (int | None) – reference field-step index (defaults to the first field step).

Returns:

element ids (N,) and misorientation angles in degrees (N,), aligned to the ids common to both steps.

Return type:

(ids, deg)

write_reorientation_field(results, step, out_csv, ref_step=None, extra_cols=())[source]#

Write id,x,y,z,reorientation_deg (+ optional passthrough) for REI.

The output is consumed unchanged by IdentifyRareClusters. Passing extra_cols (e.g. nye_tensor_* or stress columns) carries those features through so the same CSV can drive a combined rare-event criterion.

Parameters:
  • results – a SimulationResults.

  • step (int) – field-step index to measure.

  • out_csv (str) – output CSV path.

  • ref_step (int | None) – reference field-step index (defaults to the first field step).

  • extra_cols (Sequence[str]) – additional feature columns to copy through for combined REI.

Returns:

The output CSV path.

Return type:

str

static mean_orientation_mrp(mrp_arr, symmetry='1')[source]#

Symmetry-aware quaternion (eigenvector) mean of neml2 MRP orientations.

A proper orientation mean (NOT an arithmetic mean of the MRP components). When symmetry names a crystal point group, every orientation is first folded to the symmetry-equivalent variant closest to the first (reference) orientation, so a set whose members are reported in different symmetry variants (which the plain quaternion mean would average into a meaningless result) still yields the correct mean. With symmetry="1" (the default) this reduces to the plain quaternion eigenvector (Markley) mean.

Parameters:
  • mrp_arr (ndarray) – (N, 3) neml2 MRP orientations.

  • symmetry (str) – crystal symmetry (orbifold notation) to fold variants under; "1" disables folding.

Returns:

(3,) mean neml2 MRP orientation.

Return type:

ndarray

static merge_fragments_by_misorientation(mrp, labels, tol_deg, symmetry='432')[source]#

Merge sub-grain fragments whose mean orientations are within tol_deg.

Agglomerative, orientation-only merge for intragranular sub-grains: the two fragments with the smallest (symmetry-aware) mean misorientation are merged first, the merged mean is recomputed, and the process repeats until the closest remaining pair is at least tol_deg apart. Recomputing the mean each step avoids the single-linkage chaining that a fixed-mean union-find would suffer. Use it to impose a minimum sub-grain misorientation so a smooth intragranular gradient is not reported as several near-identical fragments.

Parameters:
  • mrp (ndarray) – (N, 3) neml2 MRP orientations aligned to labels.

  • labels (ndarray) – (N,) integer fragment labels.

  • tol_deg (float) – minimum mean misorientation (degrees) to keep two fragments distinct; closer pairs merge.

  • symmetry (str) – crystal symmetry (orbifold notation).

Returns:

(N,) contiguous 0-based labels after merging.

Return type:

ndarray

static absorb_fragments(coords, labels, min_size, k_neighbors=8, max_passes=25)[source]#

Absorb sub-min_size clusters into their largest-contact neighbour.

Uses kNN contact counts for adjacency, so it works for any point set (voxel grids or scattered mesh centroids). Iterates until no sub-min_size cluster remains (or no further merge is possible), then relabels contiguously from 0.

Parameters:
  • coords (ndarray) – (N, 3) point coordinates.

  • labels (ndarray) – (N,) integer cluster labels.

  • min_size (int) – clusters with fewer points are absorbed.

  • k_neighbors (int) – neighbours per point used to measure cluster contact.

  • max_passes (int) – safety cap on absorption passes.

Returns:

(N,) contiguous 0-based labels.

Return type:

ndarray

static adjacency_remerge(coords, mrp, labels, tol_deg, symmetry='432', k_neighbors=8, percolation_max_frac=0.03)[source]#

Merge spatially adjacent clusters within tol_deg misorientation.

Tightest (lowest-misorientation) adjacent pairs merge first, and any union that would exceed percolation_max_frac of the point count is skipped so a chain of low-angle fragments cannot re-percolate into one giant grain (the failure mode a global orientation merge or naive single-linkage produces). Adjacency is kNN contact, so it works for both voxel grids and scattered mesh element centroids.

Intended for large, many-cluster segmentations (NF/EBSD grids): the size cap is a fraction of the whole point set, so on a small single-grain point set (e.g. one grain’s ~100 elements in grain_fragments()) it is smaller than any fragment and blocks every merge (inert). Raising the cap there instead lets union-find chain low-angle fragments (the means are computed once), which over-merges. So sub-grain segmentation disables re-merge and relies on gamma.

Parameters:
  • coords (ndarray) – (N, 3) point coordinates.

  • mrp (ndarray) – (N, 3) neml2 MRP orientations.

  • labels (ndarray) – (N,) integer cluster labels.

  • tol_deg (float) – merge adjacent clusters whose mean orientations are within this.

  • symmetry (str) – crystal symmetry (orbifold notation).

  • k_neighbors (int) – neighbours per point used to measure cluster contact.

  • percolation_max_frac (float) – hard cap on any merged cluster’s size fraction.

Returns:

(N,) contiguous 0-based labels.

Return type:

ndarray

static gamma_sweep_leiden(csv_path, ckpt_base, gammas, n_points, spec, weight_cfg, max_edge_distance, graph_mode='grid', manhattan_radius=2, k=16, reduce_topk=12, n_jobs=1, n_threads=1, seed=42)[source]#

Build the misorientation graph once, then run Leiden per gamma.

The edges/weights depend only on the graph + misorientation cutoff (not gamma), so they are checkpointed and reused across the sweep.

Returns:

list of dicts with keys gamma, n_grains, max_frac, labels.

Parameters:
Return type:

List[dict]

static pick_gamma_by_percolation(sweep, percolation_max_frac=0.03)[source]#

Pick the smallest gamma whose biggest cluster is below the percolation cap.

The raw Leiden count is near gamma-invariant here (all gammas over-segment); gamma controls only the biggest (“percolating”) cluster, and the final grain count is set by absorption. So choose the least over-resolved, still de-percolated gamma; if none qualifies, the overall minimum max_frac.

Parameters:
Return type:

dict

segment(coords, mrp, misori_tol_deg=5.0, graph_mode='grid', gamma=None, gamma_sweep=(0.5, 1.0, 2.0, 4.0, 8.0), rbf_sigma_deg=None, manhattan_radius=2, k=16, reduce_topk=12, grain_threshold_final=30, remerge_miso_deg=3.0, percolation_max_frac=0.03, n_jobs=1, n_threads=1, seed=42, workdir=None)[source]#

Segment a point set by crystal orientation (graph / Leiden).

Preferred over flood fill, which over-merges into percolating grains. Wraps GraphSpatialCluster with the symmetry-aware misorientation metric on neml2 MRP orientations and the tuned recipe: fixed broad RBF sigma, hard misorientation cutoff, gamma-by-percolation, then absorb + adjacency re-merge.

Parameters:
  • coords (ndarray) – (N, 3) point coordinates (voxel centres or element centroids).

  • mrp (ndarray) – (N, 3) neml2 MRP orientations.

  • misori_tol_deg (float) – hard misorientation edge cutoff (degrees).

  • graph_mode (str) – "grid" for regular voxel grids (NF/EBSD), "knn" for scattered mesh element centroids.

  • gamma (float | None) – fixed Leiden resolution; if None, a percolation-picked sweep.

  • gamma_sweep (Sequence[float]) – resolutions to sweep when gamma is None.

  • rbf_sigma_deg (float | None) – fixed RBF sigma (degrees); defaults to half misori_tol_deg.

  • manhattan_radius (int) – grid connectivity radius (graph_mode="grid").

  • k (int) – neighbours per node (graph_mode="knn").

  • reduce_topk (int | None) – keep top-k edges per node by weight before clustering.

  • grain_threshold_final (int) – minimum cluster size; smaller ones are absorbed.

  • remerge_miso_deg (float) – adjacency re-merge misorientation threshold (degrees).

  • percolation_max_frac (float) – percolation cap for gamma pick and re-merge.

  • n_jobs (int) – parallel workers for edge-distance build.

  • n_threads (int) – threads for Leiden.

  • seed (int) – RNG seed.

  • workdir (str | None) – scratch directory for the graph input CSV + checkpoint (a temp directory is used and cleaned up if None).

Returns:

(N,) contiguous 0-based cluster labels aligned to coords rows.

Return type:

ndarray

seg_to_grain_table(labels, coords, mrp=None, euler=None, angle_convention='bunge', angle_type='radians', out_csv=None, background_label=-1)[source]#

Reduce a labelled point set to a per-grain table (FF/NF/EBSD connector).

Produces the grain_id, X, Y, Z, GrainRadius, Eul0, Eul1, Eul2 schema shared by the FF reconstruction and synthetic-load experiment CSVs, so segmented NF/EBSD voxel grids and FF grain tables converge on one grain table that detect_splits() consumes.

Orientation is a proper quaternion mean per grain (not an arithmetic mean of Euler angles). Provide orientations as either mrp (neml2 MRP) or euler (with angle_convention/angle_type).

Parameters:
  • labels (ndarray) – (N,) integer grain labels.

  • coords (ndarray) – (N, 3) point coordinates.

  • mrp (ndarray | None) – (N, 3) neml2 MRP orientations (mutually exclusive with euler).

  • euler (ndarray | None) – (N, 3) Euler angles (mutually exclusive with mrp).

  • angle_convention (str) – Euler convention (used when euler is given).

  • angle_type (str) – "degrees" or "radians" for the input euler.

  • out_csv (str | None) – if given, also write the table to this CSV path.

  • background_label (int) – the single label treated as void and skipped.

Return type:

object

Warning

background_label defaults to -1, but a segmented NF or EBSD grid marks void as 0 and uses -1 for material that was not segmented. Left at the default on such a grid, the whole void region is emitted as one enormous “grain” – with a centroid near the sample centre and a meaningless mean orientation – which then matches against the next step in detect_splits(). Pass background_label=0 for a grid produced by the NF/EBSD path, and mask or absorb any remaining -1 voxels first. Only one label can be excluded per call.

Returns:

A pandas.DataFrame with the grain-table schema (Euler in degrees, Bunge convention).

Parameters:
Return type:

object

grain_fragments(results, step, tol_deg=5.0, block_id_col='block_id', grain_ids=None, min_fragment_elems=5, min_subgrain_miso_deg=1.0, seg_kwargs=None)[source]#

Re-segment each original grain’s elements by orientation at a loaded step.

Groups the per-element field at step by block_id (the original grain subdomain), runs segment() (graph_mode="knn" for scattered mesh elements) within each grain, and composes a fragment_label of the form "{grain}.{k}" (k the 0-based sub-grain index). This measures intragranular fragmentation – how a single grain breaks into orientation sub-domains under load – keeping the parent grain id.

Parameters:
  • results – a SimulationResults.

  • step (int) – field-step index to analyse.

  • tol_deg (float) – misorientation tolerance (degrees) for the sub-grain segmenter.

  • block_id_col (str) – the grain/block-id column in the field CSV.

  • grain_ids (Sequence[int] | None) – grains (block ids) to analyse; defaults to all blocks present.

  • min_fragment_elems (int) – grains with fewer elements than this are left whole (one fragment); also passed as the segmenter absorb threshold.

  • min_subgrain_miso_deg (float) – minimum misorientation (degrees) between two sub-grains for them to count as distinct fragments. After Leiden, fragments whose mean orientations are closer than this are merged (agglomeratively, closest-pair first, means recomputed each step), so a smooth intragranular gradient chopped into near-identical Leiden communities collapses back to one fragment while genuine sub-grains (a real low-angle sub-boundary) survive. 0 disables the merge.

  • seg_kwargs (Dict | None) – extra keyword overrides forwarded to segment().

Returns:

(fragments_df, per_element_labels) where fragments_df has one row per grain (grain_id, n_fragments, fragment_sizes, n_elements) and per_element_labels maps each element id to its "{grain}.{k}" fragment label.

Return type:

Tuple[object, Dict[int, str]]

detect_splits(nodes_a, nodes_b, d_tol, theta_tol_deg, angle_convention='bunge', angle_type='degrees', ori_cols=('Eul0', 'Eul1', 'Eul2'), coord_cols=('X', 'Y', 'Z'), id_col='grain_id', top_k=8, adjacency_b=None, out_csv=None)[source]#

Detect grain splits/merges/births/deaths between two load steps.

Goes beyond the strictly one-to-one grain matcher: candidate cross edges from a centroid cKDTree (top-k) are kept when the centroid distance is within d_tol and the misorientation within theta_tol_deg; connected components of the step-A/step-B union graph are grain lineages, classified by their (nA, nB) shape. FF supplies the node tables directly; NF/EBSD reach the same detector via segment() + seg_to_grain_table().

The grain tables use their own ori_cols/coord_cols (Eul0/1/2 and X/Y/Z by default), independent of the simulation column conventions on the instance.

Parameters:
  • nodes_a – per-grain node tables (DataFrame or CSV path) for the two steps, with an id column, three coordinate columns, and three orientation columns.

  • nodes_b – per-grain node tables (DataFrame or CSV path) for the two steps, with an id column, three coordinate columns, and three orientation columns.

  • d_tol (float) – maximum centroid distance for a candidate cross edge.

  • theta_tol_deg (float) – maximum misorientation (degrees) for a candidate cross edge.

  • angle_convention (str) – "bunge"/"kocks"/"roe" Euler, or "mrp".

  • angle_type (str) – "degrees" or "radians" (Euler conventions only).

  • ori_cols (Sequence[str]) – the three orientation columns.

  • coord_cols (Sequence[str]) – the three coordinate columns.

  • id_col (str) – the grain-id column.

  • top_k (int) – nearest B centroids probed per A grain. This truncation happens before the d_tol gate, not after it: the KD-tree returns the top_k nearest B grains and only those are then tested against d_tol and theta_tol_deg. So top_k, not d_tol, is the binding limit on how many children a split can have whenever more than top_k B grains sit within d_tol. A parent that fragments into many pieces, or a dense fine-grained step B, needs top_k raised as well as d_tol; raising d_tol alone will not surface the extra children.

  • adjacency_b (Iterable[Tuple[int, int]] | None) – optional iterable of (grain_id, grain_id) pairs giving intra-step-B grain adjacency; used to reject a spurious split child that is not adjacent to any other child of the same parent. If no two of a parent’s candidates are mutually adjacent there is no split to reject, so the single best candidate (lowest misorientation, then shortest centroid distance) is kept and the parent matches it rather than being dropped entirely.

  • out_csv (str | None) – if given, write the split correspondences to this CSV path.

Returns:

dict with components (list of {a_ids, b_ids, kind}), split_correspondences (DataFrame), and the counts n_splits, n_merges, n_matches, n_births, n_deaths.

Return type:

Dict[str, Any]