Repository navigation
Conversation
- 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>
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>
|
I'm getting ready to post feedback on this PR. Nothing major. Currently we're over the docs builder's (readthedocs) limit of 15 minutes. I'm testing if tweaking the parameters for the basalt circle example can get us under that limit. That testing is in #115 . |
kip-hart
left a comment
There was a problem hiding this comment.
I pulled the branch and ran it instead of reading the diff alone. Python 3.12.3,
a fresh venv built from this branch's requirements.txt, package installed with
pip install -e ..
The suite passes. 217 passed in 169s, no failures, which matches what the
description claims. The jump in coverage is in real tests rather than in
parametrization: master has 14 test functions across tests/, this branch has
191, and those 191 collect as the 217 cases.
CI claims that check out
The tox==3.14.0 diagnosis is right, and I reproduced it in the order the
workflow actually installs things. Installing flake8 pytest pytest-cov coveralls first and then requirements.txt pulls pluggy back down to 0.13.1,
and pytest then fails to import at all:
ImportError: cannot import name 'HookimplOpts' from 'pluggy'
One correction for the description. It says the old pluggy is one "that the
current pytest-cov cannot load". It is pytest itself that cannot load, not
pytest-cov. Same root cause, same fix.
The pyvoro fork is what it says it is. I diffed both sdists from PyPI.
pyvoro/__init__.py, pyvoro/vpp.cpp and pyvoro/vpp.h are byte identical to
pyvoro-mmalahe 1.3.4, so nothing changed in the Python facing layer. The bundled
Voro++ really is updated, 2396 lines of difference across the 27 sources in
src/.
CI's blocking lint passes. flake8 src tests setup.py --exclude=__init__.py --select=E9,F63,F7,F82 returns 0. The five remaining cosmetic warnings are all
in docs_banner.py and logo.py, which this branch does not touch.
What I found
Four defects I confirmed by running code, in the order I would fix them:
- The 2D orientation error statistics are wrong by up to 360 degrees, because
the angles are subtracted without wrapping. A fit whose true worst error is
0.8 degrees reportsmae=359.50andR^2=-2.997. Details in the inline
comment onverification.py. <periodic>0</periodic>raisesTypeErrorinstead of turning periodicity
off.periodic_axeshas no integer branch.- Six geometry classes lost hashability, because
__eq__was added without
__hash__. - One whole domain Triangle build per non converging input is computed and
thrown away.
One more that is not a defect but is a cost, in the inline comment on
calc_rtol. The new fit lowers the tolerance on high cv inputs, which rejects
more seed placements. On basalt_circle.xml, 1300 seeds at cv 4.27 and no
explicit rtol, positioning goes from 6.1 s to 35.8 s with everything but the
tolerance held fixed. That accounts for 29.7 s of the 32.5 s that example got
slower overall. Every input with a long tailed size distribution and a default
rtol pays it.
Two more that are real but narrow. The two np.allclose(..., atol=tol) calls in
the periodic 2D boundary code leave rtol=1e-5 in place, which defeats the
tol = 1e-9 * scale the code sets deliberately. I worked out what it takes to
bite: a false match needs 1e-5 * |mins| >= lengths, so an offset to size ratio
of 1e5 or more. At exactly that ratio the periodic build does fail, with
RuntimeError: Triangle did not keep the input points., but I could not
attribute that failure to the tolerance leak rather than to double precision at
those coordinates. I am flagging the leak as latent rather than as a
demonstrated wrong answer.
Two things I want your input on
A note on the pyvoro reproducer, which does not change the conclusion. Four
discs of radius 0.1 at the quarter points of a unit square give the correct
[0.25, 0.25, 0.25, 0.25] under pyvoro-mmalahe here, not the four cells of area
1.0 the description reports. I ran a differential fuzz past that too, 200 2D
cases per periodicity setting at unequal radii plus 120 3D cases, identical
seeds against both builds, and the outputs are bitwise identical.
That is a statement about this machine and not a challenge to the swap. Two
things explain it. pyvoro-mmalahe publishes wheels only for manylinux x86_64,
so macOS and Windows have always compiled it from the sdist, with clang and
MSVC. And the upstream fix is a numerical robustness change: rad_option.hh
drops sqrt from the two radical cutting predicates and computes r_mul as a
difference of squares, while config.hh replaces the 1e-11 tolerance and the
whole marginal case buffer with 10*DBL_EPSILON and an extra search stack. In
strict IEEE double the old and new predicates agree on four million random
draws I threw at them. They diverge once a compiler contracts expressions or
keeps intermediates in wider precision, which is exactly how this ends up
showing on one platform and not another.
So the swap looks justified to me. The ask is smaller: say in the description
which platform the four disc case was observed on, since it reads as a flat
statement about the library and someone on Linux will try it and see 0.25.
The other item is calc_rtol. See the inline comment for the numbers.
On the new dependency
pyvoro-rimoli 1.4.0 is a single release uploaded 2026-09-30, with no
attestations or Trusted Publishing provenance in its PyPI metadata. Both
setup.py and requirements.txt point at it, so every pip install microstructpy and every CI job pulls it. The author_email in the metadata is
still Joe Jordan's, inherited from upstream, which is worth fixing on your side
so the package reports its actual maintainer.
The wheels cover cp39 through cp314 and requires_python is >=3.9, so the
3.9 entry in the test matrix is fine.
Documentation
The docs hold up. The new example pages follow the same section structure as
the existing ones, the docstring convention is unchanged, and coverage of it
went up rather than down: Returns: goes from 44 to 95 and Raises: from 1 to
10 across the library.
Three small things, all in inline comments. The first Added bullet of the
changelog runs 30 lines against a median of 4 and buries the gmsh limitation at
the end of it. The added prose joins clauses with semicolons 54 times where the
existing docs never do. And labelled appears four times in a repo that is
otherwise consistently US spelled.
The remaining red check is the Read the Docs timeout, and it is now handled
separately in #115, so nothing is needed here for it. The short version is that
the build runs every example and cannot finish inside the fifteen minutes Read
the Docs allows, so #115 moves that work to the documentation workflow and has
Read the Docs restore the figures instead. That took it from a 901 s timeout to
96 s.
Two things came out of that work which belong to this branch rather than to
#115. The numpy churn in the inline comment on .readthedocs.yaml, and an
invalid XML declaration in elliptical_grains.xml, which declared
version="0.5" where the specification allows only 1.x. The second is older
than this branch and I fixed it in #115, since it was failing the documentation
check here intermittently: Expat 2.6.1 accepts it and newer Expat rejects it, so
it passed locally and failed on some runners.
| numpy>=1.24.4,<2.0 | ||
| pyquaternion==0.9.5 | ||
| pyvoro-mmalahe==1.3.4 | ||
| pyvoro-rimoli==1.4.0 |
There was a problem hiding this comment.
I went through the fork before commenting here. The Python facing layer is
byte identical to pyvoro-mmalahe 1.3.4, and the Voro++ update is real and
does what the changelog says, so the substance of this is fine by me.
For the record of why, since the four disc case in the description does not
fail on Linux: the fix is in rad_option.hh, which drops sqrt from
r_ctest and r_scale_check and computes r_mul as (r-mx)*(r+mx) instead
of r*r - mx*mx, and in config.hh, which replaces the hard coded 1e-11
tolerance and the marginal case buffer with 10*DBL_EPSILON and an extra
search stack. cell.cc carries 1282 substantive changed lines on top of
that. The old and new predicates agree in strict IEEE double across four
million draws, so the divergence needs a compiler that contracts expressions
or widens intermediates. That fits a failure on the macOS and Windows
runners, which is where this always built from source, given pyvoro-mmalahe
ships wheels for manylinux x86_64 only.
Three things I would still like.
Name the platform in the description for the four disc case. As written it
reads as a property of the library, and anyone on Linux will try it and get
0.25.
Keep the bitwise regression check you mention. Thirteen years of upstream
changes to the cell cutting core is a lot of moving parts under our meshes,
and a stored reference mesh is the cheapest guard against the next Voro++
bump moving results quietly.
Turn on Trusted Publishing in the fork's release workflow so the wheels carry
PyPI attestations. It is cheap to set up and this is now a dependency of a
published package. The author_email in the metadata is also still
joe.jordan@imperial.ac.uk, inherited from upstream, so the package does not
name its actual maintainer.
There was a problem hiding this comment.
Thanks for going through the fork. Three answers.
Platform: you were right, and I confirmed your explanation. The 0.4.6 sources built with Apple clang's defaults on Apple silicon give the four cells of area 1.0. The same sources built with -ffp-contract=off give 0.25 and pass all the tests of the fork. The PR description, the changelog and the fork's README now say this.
Regression guard: pyvoro-rimoli 1.4.1 adds reference tessellations to its tests: the volumes and neighbors of the cells of four fixed configurations, 2D and 3D, bounded and periodic. They are compared to a relative 1e-10, since a bitwise comparison would not survive different compilers, which is the very issue here. The old build with fused multiply-adds fails them, and the same sources built without them pass. The bitwise regression cases of MicroStructPy stay too, and I reran them for this round.
Trusted publishing: 1.4.0 was already published with it, with attestations. PyPI's integrity API returns the provenance of each file, for example https://pypi.org/integrity/pyvoro-rimoli/1.4.0/pyvoro_rimoli-1.4.0.tar.gz/provenance, with publisher rimoli/pyvoro, workflow wheels.yml and environment pypi. The JSON metadata does not show it. The maintainer email is in 1.4.1.
There was a problem hiding this comment.
Thanks for confirming that with trusted publishing. My comment was an AI hallucination that I didn't check myself.
There was a problem hiding this comment.
I think the last thing is to bump the version pin for pyvoro-rimoli to 1.4.1
_misc.from_str reads <periodic> 0 </periodic> as the integer 0, and periodic_axes had no branch for numbers, so it tried to iterate over it and raised a TypeError instead of turning periodicity off (1 failed the same way). Numbers now count as booleans: 0 makes no axis periodic and any other number makes all of them periodic. Reported by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defining __eq__ without __hash__ sets __hash__ to None, so the equality methods added on this branch made Ellipse, Ellipsoid and the NBox family (Rectangle, Square, Box, Cube) unhashable: they could no longer go in a set or be dictionary keys. Circle and Sphere had been unhashable for the same reason since before this branch. Equality allows for round-off, which no hash can follow, so all of them now keep the identity-based hash of object, as they had before __eq__ was defined. Reported by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
np.allclose keeps its default relative tolerance (1e-5) when only atol is given, so three checks of the 2D periodic mesher accepted differences of 1e-5 times the coordinates instead of the intended 1e-9 times the size of the domain: whether a facet lies on a lower periodic face, whether its image on the upper face runs in the same direction, and whether Triangle kept the input points. Far from the origin (offsets about 1e5 times the size of the domain), a facet of the upper face would read as lying on the lower one. The checks now pass rtol=0. No result changes for domains of ordinary placement. Reported by Kip Hart in the review of PR #114 (the first two sites). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the nodes on the periodic faces still did not match at the last pass, the loop prepared the input of another pass and built one more mesh with Triangle, which was never used: the mesh of a pass is only read at the start of the next one. The loop now stops before that build, after checking that Triangle kept the input points of the mesh it returns. The result is the same, without a whole-domain triangulation on the inputs that are already the slowest. Reported by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error statistics of the 2D orientations (angle, angle_deg, angle_rad, orientation and matrix) subtracted the angles as plain numbers. A near-perfect fit across the 0/360 wrap reported errors of almost 360 degrees, and an ellipse recovered with its major axis the other way round reported 180 degrees, although it is the same ellipse. The distribution statistics compared the raw angles with the input distribution in the same way. The angles are now compared modulo the period of the shape, a half turn for ellipses and rectangles and a quarter turn for squares: the errors are the smallest angles between the orientations, R^2 measures the spread of the output angles around their circular mean, and the distribution statistics put the output angles and the samples of the input distribution on the same window of one period (with a two-sample K-S test when the input is a distribution). Reported by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/requirements.txt asked for numpy>=1.24.4 with no upper bound, so Read the Docs installed numpy 2 first, then downgraded it to 1.26.4 when it installed requirements.txt, which pins numpy<2.0. Packages resolved in between (matplotlib, contourpy) were chosen while numpy 2 was installed. Both files now ask for numpy below 2. Reported by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prose added on this branch joined independent clauses with semicolons, in the documentation, the changelog, docstrings and comments, while the rest of the repository never does. Those clauses are now separate sentences. "labelled" is now "labeled", as everywhere else in the repository. The first entry of the changelog, which covered the whole periodic feature in 30 lines, is split into one entry per part, and the gmsh limitation has an entry of its own. The changelog also says that the old pluggy breaks pytest itself, not pytest-cov, that the Voro++ 0.4.6 bug needs a compiler that fuses multiply-adds (as Apple clang does by default on Apple silicon), and records the fixes of this review that users can see. Suggested by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch had replaced the coefficients of the rtol='fit' tolerance with Eqs. (14) and (15) of Hart and Rimoli, CMAME 370 (2020) 113242 (page 12). Those printed equations do not agree with the paper itself: the fitted curves of its Fig. 10, on the same page, follow master's coefficients (largest difference 0.04 in 2D and 0.02 in 3D, against 0.11 and 0.26 for the printed equations), and master's coefficients fit the optimal values plotted in Fig. 10 much better (RMS residual 0.022 and 0.019, against 0.063 and 0.119). Eq. (15) reproduces the 3D curve if its constant term is 0.289 instead of the printed 0.253, so that looks like a typo. Eq. (14) matches the worked example in the text (0.70 at cv = 0.53) but not the figure. calc_rtol uses master's coefficients again, and its docstring cites Fig. 10 and the discrepancy. Results are those of master. This also removes the slowdown Kip Hart measured for wide size distributions with the printed coefficients (positioning in basalt_circle.xml took 35.8 s instead of 6.1 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the careful review, Kip, and for running everything rather than reading the diff. All of it is addressed in eight commits on develop, with a reply on each inline comment:
Checks: with all the code fixes but the last, the six bitwise regression cases are identical. The last one changes only the case that uses On the pyvoro side, pyvoro-rimoli 1.4.1 is out, with the maintainer email and the reference tessellations. The four-disc case is Apple silicon, and your explanation with fused multiply-adds is right. The description now says so. Trusted publishing was on from the start, as explained in the inline reply. One note for #115: these commits change the library and the |
* Halve the docs build time of the basalt_circle example
The documentation build runs every example in src/microstructpy/examples
(docs/source/sphinx_gallery/plot_demos.py globs and runs them all), and
nothing is cached between Read the Docs builds. The periodic examples
added on this branch pushed that past the 15 minute limit of the Read
the Docs free plan.
basalt_circle is the single largest consumer at 216 s, 29% of the 741 s
the examples take in total, and more than any of the new examples. About
180 s of that is meshing, driven by mesh_max_edge_length on a domain of
diameter 10.
Measured on the same machine:
0.01 (before) 216.2 s
0.02 114.4 s
0.03 83.7 s
0.05 63.8 s
0.02 halves the example. Coarser values give much less back, since what
remains is positioning 1300 seeds, plotting them and verification. The
rendered figure is unchanged at publication size, with slightly coarser
triangles inside the large olivine grains.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Restore the example figures on Read the Docs instead of rebuilding them
A build on Read the Docs is stopped at fifteen minutes on the free plan.
The documentation build runs every example in src/microstructpy/examples
through docs/source/sphinx_gallery/plot_demos.py, which takes about
twenty minutes on that class of hardware, so the build cannot finish. The
examples produce the same figures every time, so this moves the work to
the documentation workflow, which has no such limit, and has Read the
Docs restore what it produced.
Sphinx-Gallery already skips a script whose hash matches the .md5 beside
the output of an earlier build. The difficulty is that plot_demos.py
finds the examples with glob and names none of them, so its hash does not
move when an example is edited or added, nor when the library that meshes
them changes. Caching on that hash alone would serve stale figures and
never report it.
docs/example_digest.py closes that gap. It digests the example inputs and
the library source and keeps the result in plot_demos.py as
EXAMPLES_DIGEST, so editing any of them changes the hash of the script
and the examples run again. The documentation check runs it with --check
and fails if the stored digest is out of date.
docs/example_cache.py packs and restores the Sphinx-Gallery output and
the PNG files the examples write, which the pages embed with figure::.
An archive carries the digest it was built from and restore refuses one
that does not match the working tree, so a build that cannot be warm runs
the examples and may time out rather than publish stale figures.
Measured locally, with the examples run on the same machine:
cold build 655.4 s
warm build 6.7 s
The documentation workflow publishes an archive from its html job to the
docs-cache release, and asks Read the Docs to build again so the archive
is picked up. That rebuild needs a READTHEDOCS_TOKEN secret. Without it
the step says so and does nothing, and the build has to be restarted by
hand once the archive is published.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fetch the example archive in one step on Read the Docs
The name of the archive was worked out in one job step and written to a
file, and fetched in the next. If the first step failed, the second
fetched an empty name, and the message about a missing archive hid it.
Keep the name in a variable in a single step, and say which of the two
things happened: no archive was published for this commit, or one was
published and refused because its digest is not the digest of this
commit. Both leave the examples to run, and the second should not happen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Let every documentation format report
The matrix cancels the other two formats as soon as one fails, so a html
build that stops partway through the examples takes latex and epub with
it and there is nothing to compare it against. python_package.yaml
already sets fail-fast: false for the same reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Give elliptical_grains.xml a legal XML version
The file declared version="0.5". The XML specification allows only
'1.' followed by digits, production [26], so the declaration has never
been well formed. Older versions of Expat accepted it and newer ones
reject it, which is why this has only now started failing:
xml.parsers.expat.ExpatError: XML declaration not well-formed:
line 1, column 15
Column 15 is where the version value starts. The example is read by
xmltodict in cli.input2dict, so the failure stops the whole gallery and
takes the documentation build with it.
This has been in the file since before this branch, and it is not
reproducible everywhere: Expat 2.6.1 parses it without complaint, so a
developer can run the examples locally and see nothing wrong while the
same commit fails on a runner with a newer Expat. That is what made the
documentation check look flaky, failing in one format and passing in
another on the same commit.
Editing an example changes EXAMPLES_DIGEST, so the archive of figures is
invalidated and the examples run again, which is the intended behavior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Update the example digest for the fixes on develop
The eight commits merged from develop change the library and
periodic_3D.xml, so the figures the examples produce are no longer the
ones in the published archive. The digest check caught it, which is what
it is for:
EXAMPLES_DIGEST is 314fadcaec7cb6e5, the examples digest to
2d39355b71ddd257.
The documentation workflow publishes an archive for the new digest and
asks Read the Docs to build again with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
pyvoro-rimoli 1.4.1 has the same code as 1.4.0. It names its maintainer in the package metadata and adds reference tessellations to its tests. setup.py keeps requiring 1.4.0 or later, since both give the same results. Requested by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| return [{'material_type': 'solid', 'max_volume': float('inf')} | ||
| for _ in range(n_phases)] |
There was a problem hiding this comment.
max_volume is ignored in 2D whenever the caller does not pass phases.
These defaults set the key to infinity, so phase.get('max_volume', max_volume) at line 1485 finds it present and never falls back to the
max_volume argument. Every region is then given an area constraint of
infinity. On master that did not matter, because the global constraint was
passed to Triangle underneath it:
master meshpy.triangle.build(info, volume_constraints=True,
max_volume=max_volume, ...)
this PR meshpy.triangle.build(info, volume_constraints=True,
max_volume=None, ...)
_build_2d dropped the global in 24fa757 and relies on the per-region
values alone. That is right for the periodic mesher, which needs different
constraints for the cells and their copies, and a single global value would
override them. The non-periodic path goes through the same helper, and for
it the infinity in these defaults became load bearing.
Measured on this branch, same polygonal mesh, only phases differing:
phases=None, max_volume=0.4 elements=208 largest element 2.288
phases=[{max_volume: 0.4}] elements=252 largest element 0.385
The cap is 0.4 and the largest element is 2.288, so it is not being applied
at all. With the suggestion below the first line gives 252 elements and a
largest element of 0.385, and the full suite still passes at 222.
This is visible in the documentation. docs_banner.py calls
TriMesh.from_polymesh(pmesh, min_angle=12, max_edge_length=0.2,
max_volume=0.4)with no phases, and the third panel of banner.png now has a coarse
ellipse where the published one is finely triangulated.
Dropping the key lets the argument through, which is what every reader of a
.get with a default expects:
| return [{'material_type': 'solid', 'max_volume': float('inf')} | |
| for _ in range(n_phases)] | |
| return [{'material_type': 'solid'} for _ in range(n_phases)] |
Two things worth deciding rather than inheriting.
The same .get appears at lines 2036 and 2892, so this changes the fallback
in _facet_sizes and in the periodic region building as well. Only the
phases=None path is affected, and the new behavior is the intended one in
all three, but it is more than one call site.
An explicit {'max_volume': float('inf')} means unconstrained on this
branch, where on master the global still applied to it. The suggestion keeps
that, which I think is the better reading. It is a change from master either
way and belongs in the changelog.
The reason 222 tests did not catch this is that nothing exercises
phases=None with a finite max_volume. docs_banner.py is the only
caller that takes the path, and nothing asserts on what it draws. A test
that meshes one cell that way and checks the largest element against the cap
would close it.
There was a problem hiding this comment.
Sorry I didn't comment on this before, I only noticed the drift after seeing the new docs site.
pyvoro-rimoli 1.4.1 has the same code as 1.4.0. It names its maintainer in the package metadata and adds reference tessellations to its tests. setup.py keeps requiring 1.4.0 or later, since both give the same results. Requested by Kip Hart in the review of PR #114. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.rstunder "Unreleased".Periodic microstructures
<periodic>in<domain>selects the periodic axes (True, or e.g.xy); the same option is an argument ofcli.run,SeedList.positionandPolyMesh.from_seeds.mesh_min_angle,mesh_max_volume, per-phasemax_volume,mesh_max_edge_length) act as on non-periodic meshes.periodic_margin, keeps seeds from leaving thin slivers on the faces. It is off by default, and the examples useauto.edge_optnow also thickens thin pieces of cells at the faces and opens narrow corners there.periodic_2D.xml,periodic_3D.xml,pbx_2D.xml,pbx_3D.xml,pbx_interface_2D.xml,pbx_interface_3D.xmlandperiodic_tiling.py.Bug fixes and changes
The fixes are listed under "Fixed" in the changelog. They include reproducible seeding,
cdfsize distributions, TetGen volume constraints, the orientation of some ellipsoid breakdowns, cells clipped by circular and elliptical domains, several hangs, and the raster mesh andTriMesh.writeoutputs.Some changes affect existing inputs, listed under "Changed":
mesh_max_edge_lengthnow also refines the grain boundaries of 3D meshes.Tests and docs
CI
CI runs again and passes on Linux, macOS and Windows. The fixes on this branch:
tox==3.14.0pin inrequirements.txtforced an old pluggy, with which pytest itself fails to import. This would also have hit master on its next push.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
pyvoromodule as pyvoro-mmalahe, so uninstall the old one first:🤖 Generated with Claude Code