Microstructure generation#

Overview#

Synthesis of a random polycrystalline microstructure with a prescribed grain-size and grain-shape distribution, for use as synthetic test data or as a CPFE input. The stage is driven by CrystalGenerator, which drives NEPER to build a Voronoi tessellation from a morphology specification and can derive HEDM-like scan CSVs from it.

Method#

A microstructure is generated by tessellating a domain to a NEPER morphology (-morpho) specification. Unlike FF Voronoi/CVT tessellation, there is no measured data: the seeds are not given, and NEPER’s morphology optimiser places and shapes cells until the realised population matches a requested distribution. The result is a statistically specified microstructure, reproducible from a seed, with exact ground truth – which is what makes it the right input for testing every downstream stage.

Morphology types#

morpho_args is a dict whose "type" selects one of seven forms. The type also decides how the grain count is set, which is the part most easily got wrong:

type

Required keys

-morpho string

Grain count

"gg"

mean

gg(mean)

from_morpho

"diameq"

distribution, params

diameq:<dist>(p...)

from_morpho

"size"

distribution, params

size:<dist>(p...)

from_morpho

"lamellar"

n, v

lamellar(n=...,v=...)

from_morpho (n goes in the string)

"columnar"

n, v

columnar(v)

-n <n> (n is the cell count)

"bamboo"

n, v

bamboo(v)

-n <n> (n is the cell count)

"raw"

morpho_str (n optional)

verbatim

-n <n> if given, else from_morpho

from_morpho means NEPER derives the number of cells from the requested size distribution and the domain volume. That is the behaviour you want for a size-specified microstructure – asking for both a mean diameter and a cell count over-determines the domain – but it means the grain count is an output, not an input on those paths. If you need an exact count, use "columnar"/"bamboo", or "raw" with an explicit n.

Note n means two different things in the table: for "lamellar" it is the number of lamellae within the morphology string, for "columnar"/"bamboo" it is the total number of cells.

Size distributions#

"diameq" constrains the equivalent diameter \(d_{\mathrm{eq}}\), the diameter of the sphere of equal volume; "size" constrains the cell volume directly. They are not interchangeable, and the difference is a cube:

(19)#\[d_{\mathrm{eq}} = \left( \frac{6 v}{\pi} \right)^{1/3} .\]

A distribution that is lognormal in \(d_{\mathrm{eq}}\) is therefore not lognormal in volume with the same parameters, and the spread looks very different between the two. Prefer "diameq": it is the quantity HEDM reports as GrainRadius, so a generated structure and a measured one are then described in the same variable.

params is a positional tuple whose length and meaning follow the distribution:

distribution

params order

dirac

(mean,)

normal, lognormal, lorentzian, studentst

(mean, sigma)

weibull

(k, sigma)

beta

(x, y)

breitwigner, expnormal, moffat, pearson7, pseudovoigt, skewnormal

(mean, sigma, gamma)

The tuple is validated for length and then emitted comma-joined into the NEPER string verbatim; the parameters’ precise meaning is NEPER’s, so check its -morpho documentation rather than assuming a convention (lognormal, in particular, takes the mean and standard deviation of the variable itself, not of its logarithm).

Shape, and the raw escape hatch#

The six structured types cover size and the layered/columnar families. Everything else – grain elongation, bimodal mixtures, any multi-term morphology – goes through "raw", which passes its morpho_str to NEPER untouched. Elongation in particular has no structured type:

morpho_args = {
    "type": "raw",
    "morpho_str": "diameq:lognormal(130,5),aspratio(1,1,3)",
}

aspratio(a,b,c) gives cells elongated 3:1 along the third axis here. That is the generator behind the anisotropic-stitching benchmark, where the point is precisely that FF observables cannot recover such a shape (see FF Voronoi/CVT tessellation).

NEPER performs the tessellation and the morphology optimisation; graintrace validates the morphology dict, assembles the string and domain, and parses the results.

Algorithm#

  1. Validate the requested morphology type and its distribution parameters against the schema.

  2. Assemble the NEPER -morpho string, the domain, and the grain-count argument (-n from_morpho or an explicit count, per the table above).

  3. Invoke neper -T with -id <seed>, -oridescriptor euler-bunge, and -statcell id,vol,w,x,y,z,radeq, stopping the optimiser at iterations.

  4. Join the per-cell statistics and the orientation file into voronoi.csv; optionally derive HEDM-like z-scan CSVs for stitching/reconstruction studies.

  5. Recommended check: compare the realized grain-size/shape distribution against the request.

Parameters that matter#

See Configuration reference for the full list.

  • bounding_box and dim: domain extent and dimensionality (2 or 3). The domain volume together with the size distribution is what fixes the grain count on the from_morpho paths.

  • morpho_args: the type and its keys, per the tables above.

  • iterations: the morphology optimiser’s budget (-morphooptistop iter=). The default is 10, which is a fast-and-loose setting – fine for a smoke test, too few for a realised distribution that actually matches the request. The FF reconstruction path uses 1000 by comparison. Raise it and re-check the realised statistics before trusting a generated structure as a benchmark.

  • seed: passed to NEPER as -id. None (default) draws a fresh random seed each run and prints it, so an interesting run can be reproduced after the fact; pass an int up front for a deterministic one. Verified: the same seed gives a byte-identical tessellation, a different seed gives different grains.

  • extra_neper_args: appended to the command verbatim, for NEPER flags graintrace does not wrap.

Further details#

For the full morphology-generation and tessellation-optimization algorithm, see the NEPER documentation: https://neper.info/doc/ (-morpho options).

See also#