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 :class:`~graintrace.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 :doc:`ff-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: .. list-table:: :header-rows: 1 :widths: 14 24 24 38 * - ``type`` - Required keys - ``-morpho`` string - Grain count * - ``"gg"`` - ``mean`` - ``gg(mean)`` - ``from_morpho`` * - ``"diameq"`` - ``distribution``, ``params`` - ``diameq:(p...)`` - ``from_morpho`` * - ``"size"`` - ``distribution``, ``params`` - ``size:(p...)`` - ``from_morpho`` * - ``"lamellar"`` - ``n``, ``v`` - ``lamellar(n=...,v=...)`` - ``from_morpho`` (``n`` goes in the string) * - ``"columnar"`` - ``n``, ``v`` - ``columnar(v)`` - ``-n `` (``n`` is the cell count) * - ``"bamboo"`` - ``n``, ``v`` - ``bamboo(v)`` - ``-n `` (``n`` is the cell count) * - ``"raw"`` - ``morpho_str`` (``n`` optional) - verbatim - ``-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** :math:`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: .. math:: :label: msgen-diameq d_{\mathrm{eq}} = \left( \frac{6 v}{\pi} \right)^{1/3} . A distribution that is lognormal in :math:`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: .. list-table:: :header-rows: 1 :widths: 38 62 * - ``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: .. code-block:: python 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 :doc:`ff-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 ``, ``-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 :doc:`/configuration` 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 -------- - Tutorial: :doc:`/tutorials/microstructure-generation` - API: :class:`~graintrace.CrystalGenerator`