Skip to content

Periodic microstructures and bug fixes - #114

Open
rimoli wants to merge 42 commits into
masterfrom
develop
Open

rimoli wants to merge 42 commits into
masterfrom
develop

Conversation

@rimoli

@rimoli rimoli commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

This branch adds periodic microstructures in 2D and 3D, fixes a round of bugs found while going through the package, and gets the CI running again. The full list of changes is in CHANGELOG.rst under "Unreleased".

Periodic microstructures

  • <periodic> in <domain> selects the periodic axes (True, or e.g. xy); the same option is an argument of cli.run, SeedList.position and PolyMesh.from_seeds.
  • Seeds are placed with their periodic images, and the Laguerre tessellation is periodic: cells crossing a face are cut, and their pieces tile the domain.
  • The triangular, tetrahedral and raster meshes have matching nodes on opposite faces. The quality and size settings (mesh_min_angle, mesh_max_volume, per-phase max_volume, mesh_max_edge_length) act as on non-periodic meshes.
  • The Abaqus output has one node set per periodic face, in matching order, ready for periodic boundary conditions. The text files store the pairs of nodes and facets.
  • A new setting, periodic_margin, keeps seeds from leaving thin slivers on the faces. It is off by default, and the examples use auto. edge_opt now also thickens thin pieces of cells at the faces and opens narrow corners there.
  • New examples, each with a page in the docs: periodic_2D.xml, periodic_3D.xml, pbx_2D.xml, pbx_3D.xml, pbx_interface_2D.xml, pbx_interface_3D.xml and periodic_tiling.py.

Bug fixes and changes

The fixes are listed under "Fixed" in the changelog. They include reproducible seeding, cdf size distributions, TetGen volume constraints, the orientation of some ellipsoid breakdowns, cells clipped by circular and elliptical domains, several hangs, and the raster mesh and TriMesh.write outputs.

Some changes affect existing inputs, listed under "Changed":

  • rtol='fit' now uses the coefficients published in CMAME 370 (2020) 113242, Eqs. (14) and (15).
  • mesh_max_edge_length now also refines the grain boundaries of 3D meshes.
  • The facets of triangular and tetrahedral meshes are sorted, so repeated runs give identical files. Triangle and TetGen list edges in an order that varies between runs.

Tests and docs

  • 217 tests, against 13 on master. They include geometric checks of periodic tessellations and meshes against tiled non-periodic references.
  • The documentation builds with Sphinx without warnings, including the new example pages.

CI

CI runs again and passes on Linux, macOS and Windows. The fixes on this branch:

  • The import order (isort) in the files I touched.
  • The test jobs could not start. The tox==3.14.0 pin in requirements.txt forced an old pluggy that the current pytest-cov cannot load. This would also have hit master on its next push.
  • pyvoro-mmalahe on PyPI bundles Voro++ 0.4.6, whose radical tessellation can return a cell uncut. The cells then overlap, and Triangle segfaulted on the result in the macOS jobs. For example, four discs of radius 0.1 at the quarter points of a unit square should give four cells of area 0.25, and pyvoro-mmalahe returns four cells of area 1.0. Voro++ fixed the bug upstream in February 2026. As we discussed, I published a fork, pyvoro-rimoli (repository), which bundles the current Voro++ and has wheels for Linux, macOS and Windows, and MicroStructPy now depends on it. With it, the whole test suite passes, and our regression cases give bitwise the same meshes as the build we used during development.
  • The Linux jobs were killed for a different reason. One of the new tests built a reference tessellation that needed about 10 GB of memory, and the runners ran out of it and were shut down. The reference now uses only the copies of the seeds near the domain, with the same values and a third of the memory.
  • Read the Docs used Python 3.8, which pyvoro-rimoli does not support. It now uses Python 3.10 and installs requirements.txt, like the documentation check.

One check is still red. Read the Docs now installs everything, but the build runs every example, and with the new periodic examples it goes past the 15-minute limit of the free plan. The documentation checks on GitHub build the same pages and pass.

Trying it

pyvoro-rimoli installs the same pyvoro module as pyvoro-mmalahe, so uninstall the old one first:

pip uninstall pyvoro-mmalahe
pip install -e .
microstructpy --demo=periodic_2D.xml

🤖 Generated with Claude Code

rimoli and others added 30 commits September 22, 2026 20:02
- Ellipsoid.approximate: correct axis mapping for the c >= a >= b ordering
  and sort the b >= c >= a ordering explicitly (grains were tessellated
  with the wrong orientation)
- Ellipsoid.limits: exact bounding box for rotated ellipsoids
- Ellipsoid: fix the (c, ratio_bc) solver, write rot_seq with quoted axes
- Ellipse: axes keyword, orthonormality check of matrix/orientation,
  'axes' in area_expectation
- Ellipse/Ellipsoid.reflect: broadcasting and unscaling
- *_expectation: accept numpy scalars; seed the Monte-Carlo estimates
- NBox.within honours the rotation; NBox.__str__ full precision
- Rectangle(length=...) alone; Square.area_expectation return value
- Sphere.plot on a fresh figure; NSphere.best_fit with integer points
- __eq__ for Ellipse, Ellipsoid and NBox

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- from_info: iterate keywords in sorted order (the RNG chain depended on
  the per-process set order) and do not modify the rng_seeds argument
- calc_rtol is the single implementation of the overlap-tolerance fit
  (position() used a duplicate); handles a single seed
- Seed: breakdowns are float arrays also when read from a file (seeds
  loaded from seeds.txt could not be repositioned); a geometry center or
  position is respected; update_breakdown(); robust __eq__; clear error
  for unsupported shapes; parseable __str__/__repr__
- position(): 'random' entries in per-axis position distributions
- sample_pos_within raises instead of looping forever
- SeedList(seeds=None); numpy arrays as per-item plot keywords;
  plot_breakdown in 3D on a fresh figure; remove unused sampling helpers

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Cells that intersect a circular/elliptical domain without a vertex
  inside it are no longer dropped; cells cut twice are clipped correctly;
  a cell containing the domain becomes a boundary polygon
- _loop_area indexes the loop (stored areas of clipped cells were wrong)
- _segment_cross uses a fixed number of bisections (hung for large
  coordinates)
- edge_opt works on a copy, displaces seeds rigidly and applies only the
  accepted moves to the caller's seeds; quiet unless verbose
- Full-precision points in text files; write(format='poly') writes the
  file; plot() in 3D on a fresh figure; numpy arrays as per-item plot
  keywords; __eq__ is silent and linear

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- 3D: honour mesh_max_volume and per-phase max_volume (TetGen never saw
  them); 2D: per-phase values are not capped by the global one and an
  infinite global value is not passed as the mis-parsed switch 'ainf'
- RasterMesh: 'ij' grid indexing (elements were clockwise / inverted),
  facets derived from cell differences, correct Abaqus face ids, vtk
  output (also with voids), 3D plot
- tet/tri writer: valid .ele/.edge/.face files
- Abaqus: exterior surface unions only reference defined surfaces;
  polymesh optional
- Full-precision points in text files; no dangling headers; clear errors
  for unknown meshers and non-simplex elements; gmsh path with default
  phases and without unused nodes; _sort_facets reports disjoint loops

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- cdf inputs: pass density=False to rv_histogram (distributions were
  distorted for unevenly spaced x); 'pdf' as an alias of 'histogram'
- plot_tri: the 3D visibility walk terminates when voids touch the
  boundary (it looped forever)
- dict_convert passes the input file path into repeated tags (relative
  filenames failed with more than one material)
- from_str: no infinite recursion on 'true'/'false' substrings; inf/nan
- run(): no mutable defaults, arguments are not modified; unsupported
  output types and missing input files reported clearly; mesher names
  case-insensitive
- <include>: repeated materials are concatenated instead of discarded
- verification: angle_rad inputs are kept, 'random' orientations and
  vector-valued parameters work, unknown phase fields are ignored, the
  caller's phases are not modified; single-material colour maps

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SeedList.position(periodic=...) accepts True, a list of booleans or axis
names ('x', 'xy', ...). A seed that crosses a periodic face of a
rectangular domain takes part in the overlap test through its images
translated across the domain (all combinations of the crossed faces, so
edges and corners are covered); placed seeds enter the AABB tree with
their images. _misc.periodic_axes parses the specification and
_misc.periodic_domain_limits checks the domain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PolyMesh.from_seeds(periodic=...) computes the Laguerre tessellation in
Voro++'s periodic mode (breakdown centers wrapped into the domain), cuts
each wrapped cell at the periodic faces and translates the outside
pieces into the domain, so the pieces of a seed tile the unit cell. Cut
edges become wall facets; neighbors are resolved to the pieces sharing
the edge. Points and facets on opposite faces are paired, snapped to
exact translates, and stored in periodic_axes / periodic_points /
periodic_facets, which the text format persists. 3D raises
NotImplementedError for now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A periodic PolyMesh gives a periodic TriMesh: the facets on opposite
periodic faces are subdivided identically and Triangle runs with -Y (no
Steiner points on the boundary), so the nodes on opposite faces are
images of each other. Nodes and facets are paired, snapped to exact
translates and stored in periodic_axes / periodic_nodes /
periodic_facets, persisted in the text format and written to Abaqus as
node sets in matching order. Raster meshes of periodic polymeshes are
paired too (the mesh size must divide the domain length). gmsh is not
supported for periodic meshes. The pairing helpers live in _misc and are
shared with PolyMesh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ation

<periodic> is a field of <domain> in the XML input (True, a list of
flags, or axis names such as 'xy'); cli.run(periodic=...) passes it to
the seed placement and the tessellation, the meshes inherit it.
seeds_of_best_fit unwraps the points of grains split by the periodic
faces around their seed before fitting. Documented in the domain page
and the changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cells that cross a periodic face of a box are cut by the face planes
(convex polyhedron clipping, with the cap face taken as the convex hull
of the cut points), the pieces are translated back into the domain and
their neighbours are resolved, so the polyhedral mesh tiles space along
the periodic axes. Vertices within a small tolerance of a cut plane are
snapped onto it before cutting and nearly coincident points are merged
globally afterwards, which keeps the cuts of the two cells sharing a
face consistent and lets the points and facets on opposite faces be
paired exactly, as in 2D. Pieces are dropped only when they are flat.

Validated against the non-periodic tessellation of the 3x3x3 tiled
seeds (per-seed volumes) and against a single sphere tiling the cube.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The polygons on the periodic faces of the polyhedral mesh are
fan-triangulated and their images on the opposite face receive the
corresponding triangles, then TetGen runs with -Y (no Steiner points on
the boundary), so the nodes and the surface triangles on opposite faces
coincide. Raster meshes, the periodic node/facet pairs in the text files
and the Abaqus periodic node sets already worked in 3D and are now
tested there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cells of the same amorphous phase that share a facet are merged into one
region of the mesh. The Triangle/TetGen mesher labelled the region with
the seed number of its first cell while the raster mesher, the gmsh
mesher and the Abaqus writer used the smallest seed number of the region.
Both conventions agree when the regions of the polymesh are in seed
order, but not in a periodic mesh, where the pieces of the cells that
cross the periodic faces reorder the regions.

A single helper now computes the label of every region: the smallest
seed number of the merged region, which in a periodic mesh also joins the
pieces of one seed and the amorphous cells that touch across a periodic
face, since they form one region of the periodic medium.

The new tests check that the cells of a periodic mesh partition the
domain (closed convex cells, volumes adding up to the domain, random
points in exactly one cell) and that the elements partition the cells
(positive volumes, manifold faces, attribute volumes equal to the cell
volumes, element centroids inside the cells of their attribute).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With -Y (no Steiner points on the boundary), TetGen can leave some of
the sub-faces of a facet unmarked even though the tetrahedra conform to
it. The region attribute of the cell on one side then floods through the
unmarked sub-faces into the cell on the other side, and the facet is
incomplete in the output. This happened for one facet in 30 random
periodic meshes (two tetrahedra with the wrong seed number).

For periodic meshes, the element attributes and the facets are now
computed from the geometry of the polymesh: each element gets the label
of the convex cell that contains its centroid, the facets are the faces
between elements with different labels and the faces on the boundary,
and each is numbered by the polymesh facet it lies on, found from the
cells that contain the center of the face (merged amorphous cells are
not separated by facets of the mesh, so an element may span several of
them). A mesh that does not conform to the polymesh raises an error
instead of being mislabelled silently.

The geometry tests now also check that every face between elements with
different attributes is a facet of the mesh and that the facets cover
the facets of the polymesh exactly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The quality and size settings of the triangular/tetrahedral mesh
(min_angle, max_volume, the max_volume of each phase, max_edge_length)
now act on periodic meshes as on non-periodic ones. Fixing the boundary
with -Y is not an option: the bundled Triangle then degrades every
wall-adjacent triangle, and the bundled TetGen refuses any Steiner point
that encroaches a sub-face, interior facets included, unless every facet
is already at the target size.

In 2D, the cells that touch the periodic faces are copied outside the
faces while meshing, so that Triangle sees the same geometry on both
sides of each face and refines both faces of a pair the same way. The
mesh is built without -Y; when some nodes on the faces have no image,
the points Triangle added on the faces are put on both faces (as fine as
the finest of the two, not their union) and the mesh is built again,
which converges in one to three passes. The elements outside the domain
are removed, and the rare nodes still without an image are mirrored by
splitting the element behind them. The quality equals that of the free
mesh: the triangles below the minimum angle are those at cell corners
with smaller angles, in both.

In 3D, the domain is meshed once as usual (with all the facets given as
constrained Delaunay triangulations, since TetGen 1.5 crashes on polygon
facets with collinear vertices), then every facet is triangulated with
the points TetGen added on it, the facets on opposite periodic faces
with the points of both faces (merged by a spacing-aware rule) and with
a minimum angle of 20 degrees and the area given by the size settings,
and the mesh is built again with the facets fixed. Compared with the
free mesh of the same periodic polymesh, the mesh has 20-25 % more
tetrahedra and a 5th-percentile dihedral angle of 18-19 instead of
20-21 degrees; the maximum volumes, per phase too, and the maximum edge
length on the faces are met, and the nodes on opposite faces match
exactly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two CLI examples, periodic_2D.xml (a square periodic in both directions,
with circular and elliptical grains, edge optimization and mesh quality
settings) and periodic_3D.xml (a cube of two phases of spherical grains,
periodic in the three directions), and a package example,
periodic_tiling.py, which builds a 2D and a 3D periodic microstructure
with the API and plots them tiled, so that the grains continue across the
periodic faces and the paired nodes of the triangular mesh are visible.
Each has a page in the examples of the documentation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The coefficients of the rtol='fit' rational polynomial dated from the
original submission of the paper; these are the published ones. For very
wide size distributions they allow less overlap (2D asymptote 0.18
instead of 0.36), so some seeds of high-cv inputs may be rejected during
placement (basalt example: 41 of 1300).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
max_edge_length now acts in 3D on the grain boundaries: when it is set,
the facets of the polyhedral mesh are triangulated to that edge length
(with Triangle, minimum angle 20 degrees) before TetGen meshes the cells,
for periodic and non-periodic meshes alike, so that the elements can be
smaller at the interfaces than inside the grains. TetGen has no such
control of its own.

Meshing a periodic polymesh with many small cells showed that the
geometric tests of a mesh against its polymesh were too strict: the
snapping of the points to the periodic faces makes some facets
non-planar by up to a few 1e-7 of the domain size, more than the
tolerances of 1e-9 and 1e-8 used to find the cell of an element and the
facet of a face, so that a conforming mesh was rejected as
non-conforming. The tolerance is now four times the largest
non-planarity of the facets, each element is assigned to the cell in
which its centroid is deepest, and the extra points of a facet are
projected on its plane.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…aces

Voro++ computes each cell on its own, so two cells that share a vertex
hold copies of it that differ by its precision (a few 1e-9 of the domain
here), and in periodic mode the copies can lie in different images of
the domain. The pieces of the two cells were matched along their shared
edges with a tolerance of 1e-10, which failed for such a vertex during an
edge optimization: "Cannot resolve the neighbor of the edge ...".

The copies of the vertices that coincide, modulo the length of the domain
along the periodic axes, are now replaced by their mean before the cells
are cut, and snapped onto the periodic faces once for all cells, so that
the cells are cut consistently and their pieces match exactly; the pieces
are matched with the merge tolerance of the points. Verified on the
failing case, the tiled-reference tests and a stress test of random
realizations with edge optimization in 2D and 3D.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e refinement

pbx_2D.xml and pbx_3D.xml build a periodic particulate composite, such as
a plastic-bonded explosive: crystalline inclusions with lognormal sizes in
a binder, which is a matrix phase seeded with small circles or spheres.
pbx_interface_2D.xml and pbx_interface_3D.xml mesh the same material with
elements refined along the grain boundaries (the binder-inclusion
interfaces) and coarse inside the grains, through max_edge_length and a
larger max_volume. Each has a page in the examples of the documentation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A seed that barely crosses a periodic face, or ends just inside it,
leaves a thin piece of its grain on the opposite face, and the mesh has
elements much smaller than its target size there. The new periodic_margin
option of SeedList.position (and setting of the CLI) rejects the
positions where a seed ends within the margin of a periodic face or
crosses it by less than the margin, and tries another position; 'auto'
in the CLI sets the margin to half the target edge length of the mesh,
from mesh_max_edge_length or mesh_max_volume.

Over eight realizations of the periodic 2D example, a margin of half the
target edge length halves the number of boundary triangles with edges
below a quarter of the target and removes a third of the thin pieces;
the edge optimization, which does not act on the thin pieces, reduces
the small elements elsewhere, and the two combine. The thin pieces that
remain come from corners of cells crossing the faces, which the geometry
of the seeds cannot predict. The periodic 2D example uses the margin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The points of a periodic polymesh are snapped onto the periodic faces
within _SNAP_TOL of the domain size, which makes some facets non-planar
by up to that distance and the cell volumes of the two sides of such a
facet differ at the same level. The partition tests now use twice the
snapping tolerance for planarity, convexity and containment, compare the
volume sums at 1e-6 and check for gaps and overlaps with widened and
shrunk cells respectively, instead of tolerances of 1e-8 and 1e-9 that a
snapped vertex can exceed. A cube case with such a vertex is added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The edge optimization now works on the features of the mesh: its edges
and, in periodic meshes, the thickness of every piece of a cell at a
periodic face. With a positive periodic_margin (a new argument of
PolyMesh.from_seeds, passed on by the CLI from its setting), every piece
thinner than the margin is a target besides the shortest feature: the
seed of the piece is pushed so that the piece reaches the margin or
retracted so that the cell ends a margin inside the face, then it and the
seeds of its neighbors are moved randomly normal to the face and to their
facets with it. A trial is accepted when the shortest feature that it
changes gets longer (the features of the two meshes are matched by their
geometry), which is the previous criterion for the shortest edge and lets
the optimization move on to the next target when one is stuck. Seeds
moved across a periodic face are wrapped back into the domain.

The CLI writes and plots the seeds again after the optimization, so that
the seed file reproduces the polygonal mesh. The periodic tiling example
uses the margin, and the documentation describes the optimization.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 'auto' periodic_margin is now the smaller of half the target edge
length of the mesh and an eighth of the size of the smallest seed (its
smallest diameter or side): the smallest particle needs about four
elements across it, so the mesh cannot be coarser than a quarter of it,
and a seed cannot satisfy a margin larger than itself, which rejected the
small seeds near the faces at placement.

The periodic examples ship with periodic_margin auto and edge_opt (10
iterations in 3D, 25 in 2D), and the tiling example computes the same
margin from its seeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A corner of a cell at a periodic face that is narrower than the minimum
angle of the mesh cannot be meshed to that angle: the mesher refines it
in shells of very small elements instead (in the periodic meshes, each
pass deepens the shells further). Such corners are now features of the
edge optimization, with the size of the elements they force (a quarter
of the thickness of the wedge at the end of its shorter side), and are
targets like thin pieces when under the margin. The facet turns with the
line between the seeds on its two sides, so the first trial moves them
along the facet, in opposite directions, by the amount that opens the
corner to the minimum angle plus two degrees, and the others move them
along the facet randomly.

PolyMesh.from_seeds takes the minimum angle of the mesh to be built as
min_angle; the CLI passes its mesh_min_angle setting, so no new setting
is needed. The tiling example passes it too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When the nodes on the periodic faces did not match after a pass, the next
pass put the points that Triangle had added on the faces back into the
facets and meshed the cells again from scratch. In a corner narrower than
the minimum angle, where Triangle refines in shells of small elements,
each pass then split the segment at the tip once more, one level per
pass, down to very small elements. Now all the points of the previous
mesh are the input of the next pass, with the points on a periodic face
and on its image merged and put on both faces, so that Triangle only
refines the mesh around the merged points, in the same surroundings (the
copies of the cells outside the faces) that produced them. The faces
match after two passes in the examples, nothing is mirrored, and the
quality is that of the first pass.

The copies of the cells at the corners of the domain, moved along both
axes, were open on the side of the periodic wall facet that was skipped
as if it lay on the opposite face; Triangle discarded part of them. The
copies are closed now, which also makes the first pass match the faces
better, and the mesher checks that Triangle kept its input points.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pages of the interface refinement examples have the Materials and
Domain Geometry sections of the other pages and point to the seed and
polygonal mesh figures of the binder and inclusions examples, which they
share; the tiling example page has the sections of the other package
examples (Domain, Phases, Seeds, Meshing, Plotting). Two numbers in the
pages that no longer matched the input files (the overlap tolerance of
the 3D binder example, the maximum edge length of the 3D interface
example) are corrected, the pages mention the narrow corners that the
optimization opens, the comments of the periodic 2D input file describe
the current margin rule, and the mesh comment of the 3D interface input
file sits next to the mesh settings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The periodic domain and settings pages referred to each other with the
labels cli_domain and cli_settings, which did not exist. The labels are
added at the top of the two pages; the documentation now builds without
warnings. The outputs that the documentation build writes into the
package (the example results, the gallery and the copied CSV files) are
ignored by git.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wall ids of a rectangular domain are decoded by one helper, the
check that a cell of a periodic tessellation is narrower than the domain
is one function for 2D and 3D, the points of a region are collected by
one helper, the CLI evaluates the periodic margin once, and the shortest
edge finder that the optimizer no longer uses is removed. No arithmetic
changes: the meshes of the examples and of the regression cases are
identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rimoli and others added 12 commits September 23, 2026 15:22
The wedge configuration, the shortest edge of a polygonal mesh, the
volumes of the cells of each seed, the tiled reference tessellation and
the check of the pairs on the periodic faces were written twice, for 2D
and 3D or in two test files. They are one module now, and the 2D and 3D
tests use the same checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The disjoint sets of the point mergers, of the merging of amorphous
cells and of the classes of periodic edges are one class, and the
clustering of close points (the pairs of a KD-tree, the sets, the mean
of each cluster) is one function used by both point mergers. The
partitions and the means are computed the same way as before: the
meshes of the regression cases are identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Matching a piece by the two ends of an edge is matching it by the
vertices of a face applied to two points, and the wall on which an edge
lies is found the way the wall of a face is: the 3D helpers serve both
dimensions. Same distances, same tests: the regression cases are
identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The polygonal and the triangular meshes paired the points and the
facets on their periodic faces with the same two calls, the triangular
mesh allowing for a missing facet list; that is one function of the
helpers module now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The loop over the cells whose bounding box contains some points, with
the signed distances of those points to the facet planes, was written
three times in the attribute assignment and in the collection of the
facet points of the 3D meshes; it is one method of the cell geometry
helper now, with the same products in the same order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cutting a cell at the periodic faces (snap the vertices, check the
width, clip below and above each face, drop the flat pieces, translate
the outside pieces into the domain) was written once with the loop
clipper and once with the polyhedron clipper; the loop takes the clipper
as an argument now. Same operations in the same order: the regression
cases are identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Triangle and TetGen list the edges/faces of a mesh in an order, and with
an orientation, that vary from one run to the next, so the mesh files of
otherwise identical runs differed in their facet lists. The facets are
now sorted when the mesh is created, whatever the mesher: nodes in
ascending order within a facet and facets in lexicographic order, with
their attributes carried along. The orientation of a facet is not used
by the package. The facets of the periodic meshes, computed from the
polygonal mesh, were already in that order and are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CI import-order check (isort, force_single_line, microstructpy as
the first-party section) rejected five files: the scipy import of the
mesh module belonged after the matplotlib imports, and the imports of
the shared test helpers belong with the third-party section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The workflow installed pytest 6.2.5 and a current pytest-cov, then the
requirements pinned tox 3.14, whose pluggy < 1 requirement downgraded
pluggy to 0.13; pytest-cov then failed to load ("unexpected keyword
argument 'wrapper'") and no test ran. The workflow installs the current
pytest, tox is no longer a requirement of the package (nothing imports
it; tox.ini stays for local use), and the jobs of the test matrix no
longer cancel each other when one fails, so that every platform reports
its own result.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pyvoro-mmalahe bundles Voro++ 0.4.6, whose radical (Laguerre)
tessellation can return a cell uncut: the cells then overlap, and
Triangle and TetGen can crash on the polygonal mesh. This is why the
tests fail on Linux and macOS in the continuous integration. The
pyvoro-rimoli fork provides the same pyvoro module with the current
Voro++, where this is fixed, and has wheels for Linux, macOS and
Windows.

The README and the installation guide tell users to uninstall
pyvoro-mmalahe before upgrading, since both packages install the same
files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_periodic_3d_matches_tiled_reference compares the periodic
tessellation of 83 seeds (about 3000 breakdown spheres) with a
non-periodic tessellation of the seeds copied across the periodic
faces. With all 27 copies, that reference needs about 10 GB of memory
(peak footprint on macOS, where memory compression hides part of it
from the resident size), in the test process and again in the pyvoro
probe subprocess. The Linux runners of the continuous integration
(16 GB, no memory compression) ran out of memory there and were shut
down, which is why the Linux test jobs failed.

tiled_reference_volumes takes an optional margin: only the copies whose
center is within that distance of the domain are kept, and the tiled
domain ends there, plus the extent of the largest breakdown. The 3D
test uses a margin of 1, half the side of the domain; the reference
volumes are the same as with all the copies to rounding (relative
difference 3e-15; a margin of 0.7 is not enough). The peak memory of
the test file goes from 10 GB to 3.3 GB and its time from 77 s to
32 s. The 2D tests keep all the copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read the Docs used Python 3.8, for which pyvoro-rimoli has no release
(it supports Python 3.9 and later), so the build stopped when installing
the package; it already failed on 3.8 for other pull requests, when a
pinned dependency dropped 3.8. It now uses Python 3.10 and installs
requirements.txt as well, like the documentation check of the
continuous integration, so that the same versions (numpy < 2 among
them) are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant