Common pitfalls#
NEPER is bring-your-own#
VoronoiMeshBuilder and CrystalGenerator
resolve a user-installed NEPER via graintrace/neper_env.py. Precedence:
explicit neper_path=/env= → NEPER env var → graintrace_tools.json
"neper" key → neper on PATH. If none resolve, the builder raises a
clear error. On Linux, auto_install=True performs a ~/.local source build.
CrystalGenerator seed is random by default#
CrystalGenerator defaults to seed=None, which draws a fresh
random seed on every instantiation (printed to stdout), so each unseeded run is a
different microstructure. Pass an explicit seed=<int> for a reproducible
tessellation; the printed value reproduces a given run. (NEPER honors -id: different
seed gives different grains, the same seed is byte-identical. The FF reconstruction path
VoronoiMeshBuilder centroid/centroidsize is data-seeded via
-morphooptiini, so its seed is inert — grains come from the measured centroids.)
Derive the cpfe_base path dynamically#
Do not hardcode it:
import graintrace as _gt
from pathlib import Path
_cpfe_base = str(Path(_gt.__file__).parent / "cpfe_base")
FF output orientations are always in degrees#
build_voronoi() always writes
orientations.dat in degrees, regardless of input units. When feeding to
VoxelMeshBuilder afterward, set angle_type="degrees".
orientation_tolerance units must match ori_units#
When ori_units="radians", convert orientation_tolerance before passing it
to RegionBaseStitching:
if ori_units == "radians":
orientation_tolerance = np.deg2rad(orientation_tolerance)
grid_properties bounding box should be inset#
Inset grid_bb by 0.0001 on each face to avoid mesh-boundary issues, while
using the full box for the boundary conditions (see Configuration reference).
CUDA material calibration: the model must be on the device#
TaylorModel(device="cuda") works because taylor.py moves the whole
nonlinear system with nsys.to(device) before wrapping it in the pyzag factory.
Moving only the factory leaves the crystal-geometry buffers on CPU, which
surfaces as a silent loss=inf.
Use the GPU when one is available#
The GPU-accelerated steps are CPFE, material calibration, and pole figures. When
a CUDA GPU is present, use it (device="cuda:0" for CPFE,
TaylorModel(device="cuda") for calibration); CPU is much slower for these
NEML2-dominated workloads.
Resuming a checkpoint validates the parameters that built it#
GraphSpatialCluster.run(resume_from_checkpoint=True) loads the stored edges
and weights and skips graph building, distance filtering, sigma estimation and
top-k pruning. Anything that shaped those arrays is therefore compared against
the checkpoint metadata on resume, and a mismatch raises rather than returning
the old graph as the answer to a new question. That covers the data itself (a
sha256 of the coordinate and feature arrays), the metric, the graph mode and
its connectivity, max_edge_distance, reduce_edges_topweights_k, and the
weighting parameters each weight_cfg.mode actually reads: sigma and
power for rbf, sigma for exp, eps for inverse and
log_inv.
segmenter and the NetworKit options are deliberately not compared. They act
after the graph is loaded, which is what makes reusing one checkpoint across a
gamma sweep worthwhile.
One case needs care. When sigma is None the value is estimated from the
edge distances, and resume skips the step that computes them, so the resolved
sigma is not knowable without rebuilding the graph. Such a request is compared by
its sigma_auto quantile instead, which is exact because everything feeding
those distances is already compared. The consequence is that resuming a
sigma-pinned checkpoint with a sigma=None request is reported as a
mismatch. Pin the resolved sigma (it is recorded in <base>.meta.json as
weight_sigma) if you want to reuse such a checkpoint.
Checkpoints written before a given check existed lack its metadata key; those warn instead of failing.
The multiprocessing guard#
NF reconstruction (graintrace/nf/convert.py) uses multiprocess.Pool, so
scripts that call it must be under an if __name__ == "__main__": guard. The
graph-clustering / REI pipeline does not require the guard, but keeping it is good
practice.
Unsegmented voxels must not reach the mesh writers#
A segmented grid uses 0 for void, >= 1 for a grain and -1 for a voxel
that is material but belongs to no grain. -1 is not only a flood-fill
leftover: graintrace.nf.segment.remove_small_segments() reassigns a
sub-threshold segment to -1 when it has no kept neighbour, so a pipeline that
runs the infill before the removal, as
reconstruct() does, can
still hold orphans at the end.
nf.mesh.write_spn raises ValueError on such a grid rather than meshing it.
Run graintrace.nf.segment.infill_nearest_neighbor() after
remove_small_segments, or mask the orphans to void (0) yourself —
write_spn will not do it for you, because dropping them silently removes
material from the mesh. nf.mesh.write_voxel_exodus skips negative ids the same
way it skips the background id, so an orphan never becomes a block of its own.
Grain orientation averages need the crystal symmetry#
A cubic orientation has 24 equivalent rotation matrices, and a reconstruction may
report voxels of one grain in different variants. Averaging across variants gives
a mean close to no member of the set, so
graintrace.nf.metrics.average_rotations() takes a symmetry argument and
folds every member onto the variant closest to the first before averaging.
The default is symmetry="1" (the plain quaternion mean), which keeps direct
callers of the low-level graintrace.nf.mesh writers unchanged. The
NearFieldMeshBuilder and
VoxelMeshBuilder pass their own
symmetry (default "432") through to every orientation export, so the
builder path is symmetry-aware without any extra configuration. If you call
nf.mesh.write_spn, nf.mesh.mesh_sculpt or nf.mesh.write_voxel_exodus
yourself, pass the crystal point group explicitly.