Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5189b34
Fix geometry bugs: ellipsoid axis ordering, exact limits, keywords
rimoli Sep 23, 2026
db9a238
Fix seeding bugs: reproducibility, file round trips, positioning
rimoli Sep 23, 2026
ba996f4
Fix PolyMesh bugs: clipping to circular domains, edge_opt, I/O
rimoli Sep 23, 2026
b49bb90
Fix TriMesh/RasterMesh bugs: mesh size limits, raster meshes, writers
rimoli Sep 23, 2026
ae2b00b
Fix CLI, input parsing and verification bugs
rimoli Sep 23, 2026
25cff31
Add changelog entries for the bug fixes
rimoli Sep 23, 2026
9f4b213
Add periodic seed placement (per-axis)
rimoli Sep 23, 2026
13da404
Add periodic 2D tessellation
rimoli Sep 23, 2026
b5e169d
Add periodic 2D triangular and raster meshes
rimoli Sep 23, 2026
d243eb7
Add the periodic option to the CLI and unwrap split grains in verific…
rimoli Sep 23, 2026
d09ad03
Add periodic 3D tessellation
rimoli Sep 23, 2026
939bd01
Add periodic 3D tetrahedral meshes
rimoli Sep 23, 2026
a39cd17
Label merged amorphous cells consistently in periodic meshes
rimoli Sep 23, 2026
f8a399f
Take the attributes and facets of periodic meshes from the polymesh
rimoli Sep 23, 2026
24fa757
Mesh periodic domains with the quality and size settings of other meshes
rimoli Sep 23, 2026
4070fef
Add examples of periodic microstructures
rimoli Sep 23, 2026
f335141
Use the published overlap-tolerance fit (CMAME 370, Eqs. 14-15)
rimoli Sep 23, 2026
6b32d15
Size the triangles of all facets with max_edge_length in 3D
rimoli Sep 23, 2026
f066632
Unify the vertices of the cells before cutting them at the periodic f…
rimoli Sep 23, 2026
31517cb
Add examples of a periodic binder-inclusion composite and of interfac…
rimoli Sep 23, 2026
f8ae0a2
Add a margin between the seeds and the periodic faces
rimoli Sep 23, 2026
9cce19a
Use the snapping tolerance in the periodic geometry tests
rimoli Sep 23, 2026
8fde1e0
Optimize the thin pieces of periodic cells with edge_opt
rimoli Sep 23, 2026
294c129
Cap the automatic periodic margin by the size of the smallest seed
rimoli Sep 23, 2026
414ffa0
Open the narrow corners of the cells at the periodic faces with edge_opt
rimoli Sep 23, 2026
628d4e6
Stop the passes of the 2D periodic mesher from deepening the corners
rimoli Sep 23, 2026
5d74cd0
Present the periodic examples like the others
rimoli Sep 23, 2026
df42a68
Fix two cross-reference labels in the CLI documentation
rimoli Sep 23, 2026
86d8dfa
Do not repeat 'example' after the titles of the referenced example pages
rimoli Sep 23, 2026
22fc079
Share the small idioms of the periodic code
rimoli Sep 23, 2026
7b3d734
Share the helpers of the periodic tests
rimoli Sep 23, 2026
f99c00d
One union-find and one point clustering for the periodic code
rimoli Sep 23, 2026
387cce5
One piece matcher and one wall lookup for 2D and 3D
rimoli Sep 23, 2026
c304415
One function pairs the points and facets of a periodic mesh
rimoli Sep 23, 2026
e467749
The cell geometry helper answers which cells contain which points
rimoli Sep 23, 2026
6bf0dcb
One cutting loop for the 2D and 3D periodic pieces
rimoli Sep 23, 2026
ee870b1
Sort the facets of the meshes created from a polygonal mesh
rimoli Sep 23, 2026
f6f2543
Sort the imports as the package's isort configuration requires
rimoli Sep 24, 2026
e618e25
Let the tests run in the continuous integration
rimoli Sep 24, 2026
2959311
Install pyvoro from pyvoro-rimoli, which bundles the current Voro++
rimoli Sep 30, 2026
654fa27
Keep the tiled reference of the 3D periodic tessellation test small
rimoli Sep 30, 2026
5b26948
Build the documentation on Read the Docs with Python 3.10
rimoli Sep 30, 2026
df1095c
Accept a number as the periodicity of a domain
rimoli Oct 8, 2026
c428e83
Keep the geometry classes hashable
rimoli Oct 8, 2026
fe7d152
Use only the absolute tolerance when matching periodic boundary points
rimoli Oct 8, 2026
35b7478
Build no mesh after the last pass of the 2D periodic mesher
rimoli Oct 8, 2026
29eb126
Compare 2D orientations modulo the symmetry of the shape
rimoli Oct 8, 2026
543684e
Keep numpy below 2 in the documentation requirements
rimoli Oct 8, 2026
f8e2e41
Write the added prose as sentences, with US spelling
rimoli Oct 8, 2026
3697c37
Restore the overlap tolerance fit of master, which matches Fig. 10
rimoli Oct 8, 2026
e4df822
warm start for the docs build (#115)
kip-hart Oct 14, 2026
46c3179
Pin pyvoro-rimoli 1.4.1 (#117)
rimoli Oct 14, 2026
176d2a0
Apply max_volume to the default phases in 2D (#118)
rimoli Oct 17, 2026
725a181
Version 1.6.0 (#119)
rimoli Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions .github/workflows/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ name: Documentation check

on: [pull_request]

jobs:
permissions:
contents: write

jobs:
docs-checks:
name: ${{ matrix.doc-type }}
strategy:
# Let every format report. Without this, one failing format cancels
# the other two, and a build that fails partway through the examples
# takes the evidence of the others with it.
fail-fast: false
matrix:
doc-type: [html, latex, epub]

runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -28,9 +35,62 @@ jobs:
run: |
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install -e .
- name: Check the example digest is current
run: python docs/example_digest.py --check

- name: Build ${{ matrix.doc-type }} documentation
run: sphinx-build -Wnb ${{ matrix.doc-type }} docs/source/ docs/build-${{ matrix.doc-type }}/


# Read the Docs stops a build at fifteen minutes and the examples take
# about twenty, so it restores what the examples produced here instead
# of running them. The archive is named by the digest of the example
# inputs and of the library, and Read the Docs only restores one whose
# digest matches the commit it is building.
- name: Publish the example cache
if: matrix.doc-type == 'html' && github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
name=$(python docs/example_cache.py name)
python docs/example_cache.py pack "$name"
gh release view docs-cache >/dev/null 2>&1 \
|| gh release create docs-cache --title "Documentation example cache" \
--notes "Figures the documentation examples produce, named by the digest from docs/example_digest.py. Read the Docs restores the archive matching the commit it builds. Managed by the documentation workflow." \
--latest=false
gh release upload docs-cache "$name" --clobber

# A pull request that changes an example has no cache for its new
# digest until the step above finishes, so the Read the Docs build
# that started alongside this job ran the examples and timed out.
# Build it again now that the archive is published, and it is warm.
#
# Needs a READTHEDOCS_TOKEN secret, from Settings -> API Tokens on
# Read the Docs. Without it this step says so and does nothing, and
# the only cost is having to press Rebuild by hand.
- name: Rebuild the docs on Read the Docs
if: matrix.doc-type == 'html' && github.event.pull_request.head.repo.full_name == github.repository
env:
RTD_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }}
RTD_VERSION: ${{ github.event.pull_request.number }}
run: |
if [ -z "$RTD_TOKEN" ]; then
echo "No READTHEDOCS_TOKEN secret, skipping the rebuild."
echo "Press Rebuild on the Read the Docs build to pick up the cache."
exit 0
fi
# A pull request build is a version named after the pull request.
code=$(curl -sS -o /tmp/rtd.json -w '%{http_code}' -X POST \
-H "Authorization: Token $RTD_TOKEN" \
"https://app.readthedocs.org/api/v3/projects/microstructpy/versions/$RTD_VERSION/builds/")
echo "Read the Docs answered $code"
cat /tmp/rtd.json
case "$code" in
202) echo "Rebuild triggered." ;;
*) echo "Could not trigger a rebuild. The cache is published, so a"
echo "rebuild by hand will be warm."; exit 0 ;;
esac


- name: Prepare documentation artifact
run: |
# Define Path to Upload
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/python_package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ jobs:

runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: [3.9, '3.10', '3.11', '3.12']
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
Expand All @@ -30,7 +31,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
pip install flake8 pytest==6.2.5 pytest-cov coveralls
pip install flake8 pytest pytest-cov coveralls
- name: Install package requirements
run: pip install -r requirements.txt
- name: Install package
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
test_reports/
.vscode/
docs/build*/
# generated by the documentation build (the examples are run by sphinx-gallery)
docs/source/auto_examples/
docs/source/sphinx_gallery/*.csv
src/microstructpy/examples/*/

###############################################################################
# #
Expand Down
27 changes: 26 additions & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,33 @@ formats: all
build:
os: ubuntu-22.04
tools:
python: "3.8"
python: "3.10"
apt_packages:
- freeglut3-dev
jobs:
# Running every example takes about twenty minutes, and a build here
# is stopped at fifteen. The documentation workflow runs them on a
# machine without that limit and publishes what they produced, keyed
# by a digest of the example inputs and of the library. Restore that
# and Sphinx-Gallery skips the examples.
#
# The archive is only restored when its digest is the digest of this
# commit, so an edited example is never served with older figures.
# When there is no archive to match, nothing is restored, the
# examples run, and the build is likely to time out. That is the
# intended failure: publish the cache for this commit, then rebuild.
post_install:
- |
set -u
name=$(python docs/example_cache.py name) || exit 0
url=https://github.com/kip-hart/MicroStructPy/releases/download/docs-cache
echo "Looking for $name"
if curl -sSfL -o /tmp/examples.tar.gz "$url/$name"; then
python docs/example_cache.py restore /tmp/examples.tar.gz \
|| echo "The archive was refused, the examples will be run."
else
echo "No archive published for this commit, the examples will be run."
fi

# Build documentation in the "docs/" directory with Sphinx
sphinx:
Expand All @@ -24,5 +48,6 @@ sphinx:
python:
install:
- requirements: docs/requirements.txt
- requirements: requirements.txt
Comment thread
kip-hart marked this conversation as resolved.
- method: pip
path: .
189 changes: 188 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,192 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.

`1.6.0`_ - 2026-10-18
--------------------------
Added
'''''
- Periodic microstructures, in 2D and 3D. The ``<periodic>`` field of the
domain selects the periodic axes: ``True`` (or ``1``) for all of them, or
their names, such as ``xz``. The same option is an argument of
``cli.run``, ``SeedList.position`` and ``PolyMesh.from_seeds``.
- Periodic seed placement: seeds crossing a periodic face are placed
without overlapping the seeds on the opposite side.
- Periodic tessellation: the Laguerre tessellation is periodic across the
faces. Cells crossing a face are cut, and their pieces tile the domain.
- Periodic meshes: the triangular, tetrahedral and raster meshes have
matching nodes on opposite faces. The quality and size settings
(``min_angle``, ``max_volume``, the ``max_volume`` of each phase,
``max_edge_length``) act as on non-periodic meshes. In 2D, the cells next
to the periodic faces are copied outside the faces while meshing, so that
Triangle refines both faces the same way. In 3D, the facets are
triangulated with the points that TetGen adds on them in a first pass,
identically on opposite faces, and the mesh is built again with the
facets fixed.
- gmsh is not supported for periodic meshes, which need the default mesher,
Triangle/TetGen.
- Output of periodic meshes: the pairs of periodic points, nodes and facets
are stored in the meshes and in their text files. The Abaqus output has a
node set per periodic face, in matching order. The verification unwraps
grains that are split by the faces.
- Cells of the same amorphous phase that touch across a periodic face are
merged into one region, like cells that share a facet. Every mesher and
writer labels the merged region with the smallest seed number among its
cells.
- The element attributes and the facets of periodic meshes are computed
from the geometry of the polymesh. TetGen can leave sub-faces of a facet
unmarked when it may not modify the boundary, and its region attributes
then leak between cells.
- Examples: ``periodic_2D.xml``, ``periodic_3D.xml`` and
``periodic_tiling.py`` demonstrate periodic microstructures,
``pbx_2D.xml`` and ``pbx_3D.xml`` a periodic particulate composite
(crystalline inclusions in a binder), and ``pbx_interface_2D.xml`` and
``pbx_interface_3D.xml`` meshes refined at the grain boundaries.
- ``periodic_margin`` (a setting, and an argument of ``SeedList.position``
and ``cli.run``): the minimum distance between the surface of a seed and
a periodic face. A seed that ends within the margin of a face, or crosses
it by less, is placed elsewhere, since it would leave a thin piece of its
grain on the opposite face and elements much smaller than the target size
of the mesh there. ``auto`` uses half the target edge length of the mesh,
or an eighth of the size of the smallest seed if that is smaller (the
smallest seed needs about four elements across it, and cannot satisfy a
margin larger than itself).
- ``PolyMesh.from_seeds(edge_opt=True, periodic_margin=...)``: the edge
optimization of periodic meshes treats the thickness of each piece of a
cell at a periodic face as a feature, like an edge. It moves the seeds of
the pieces thinner than the margin, and of their neighbors, normal to the
face, until the piece reaches the margin or the cell no longer crosses
the face. The CLI passes its ``periodic_margin`` setting on.
- ``PolyMesh.from_seeds(edge_opt=True, min_angle=...)``: the corners of the
cells at the periodic faces narrower than the minimum angle of the mesh
are features too, since the mesher cannot reach the minimum angle there
and fills them with shells of very small elements. The seeds on both
sides of the facet are moved along it to open the corner. The CLI passes
``mesh_min_angle``.
- Edge optimization: a trial is now kept when the shortest feature that it
changes gets longer (for the shortest edge of the mesh, the criterion is
unchanged). A target that does not improve in ``n_iter`` trials is left
alone and the next one is taken, and the 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 files match the polygonal
mesh. The periodic examples use ``periodic_margin`` ``auto`` and
``edge_opt``.
- When the nodes on the periodic faces of a 2D mesh do not match after the
first pass, the next pass starts from all the points of the mesh (those on
a periodic face and on its image merged and put on both faces), so that
Triangle only refines it around the merged points, instead of meshing the
cells again with the points on the faces only, which split the narrow
corners of the cells again at every pass, down to very small elements.
The copies of the cells at the corners of the domain, used while meshing,
were open on one side and partly discarded by Triangle.

Fixed
'''''
- Seed generation is reproducible: the RNG seed chain no longer depends on
the (hash-randomized) iteration order of the phase keywords, and
``SeedList.from_info`` and ``cli.run`` no longer modify the ``rng_seeds``
and ``filetypes`` arguments (or their mutable defaults).
- ``<dist_type> cdf </dist_type>`` inputs are no longer distorted when the
x-values in the CSV file are not evenly spaced (``density=False`` is now
passed to ``scipy.stats.rv_histogram``). ``pdf`` is accepted as an alias
of ``histogram``.
- 3D ``mesh_max_volume`` and per-phase ``max_volume`` are now honored by
TetGen. In 2D, a per-phase ``max_volume`` larger than the global value is
no longer capped, and an infinite ``mesh_max_volume`` is no longer passed
to Triangle as the (mis-parsed) switch ``ainf``. The ``max_volume`` of a
phase replaces the global value for that phase, an infinite one included,
and phases without one, such as the default phases when none are given,
use the global value.
- ``Ellipsoid.approximate`` mapped the axes incorrectly for the ordering
c >= a >= b, so those grains were tessellated with the wrong orientation.
The b >= c >= a ordering is now sorted explicitly as well.
- Seeds read back from ``seeds.txt`` can be repositioned. Ellipsoid seeds
with a rotation sequence are written in a form that can be read back.
- Cells that intersect a circular or elliptical domain without having a
vertex inside it are no longer dropped, cells cut twice by the boundary
are clipped correctly, and the stored areas of clipped cells are correct
(``PolyMesh.volumes``, ``verification.volume_fractions``).
- ``_segment_cross`` no longer hangs for large coordinate values, and
``sample_pos_within`` raises instead of looping forever when the position
distribution does not cover the domain.
- ``cli.plot_tri`` no longer hangs in 3D when a void grain touches the
boundary of the domain.
- Relative ``<filename>`` and ``<directory>`` paths inside repeated tags
(e.g. several ``<material>`` blocks) are resolved relative to the input
file. A top-level ``<include>`` no longer discards materials, values such
as ``true_cdf.csv`` no longer cause infinite recursion, and ``inf`` is
parsed as a float.
- Verification: ``angle_rad`` inputs are no longer replaced by a uniform
distribution, ``<orientation> random </orientation>`` and vector-valued
parameters (``side_lengths``, ``axes``) no longer crash, unknown phase
fields are ignored, the caller's phases are not modified. The statistics
of 2D orientations compare the angles modulo the symmetry of the shape,
a half turn for ellipses and rectangles and a quarter turn for squares.
- ``RasterMesh``: elements are counter-clockwise / right-handed (valid for
Abaqus CPS4/C3D8), facets and their attributes are correct, ``vtk`` and
``abaqus`` output work (including with voids), 3D plotting works.
- ``TriMesh.write``: valid ``.ele``/``.edge``/``.face`` files, Abaqus
exterior surface unions reference only defined surfaces, full-precision
points in text files, no dangling headers for meshes without attributes.
- ``PolyMesh.from_seeds(edge_opt=True)`` leaves the seed list in the
accepted state (positions and breakdowns consistent) and is quiet unless
``verbose``. ``PolyMesh.write(format='poly')`` writes the file, and
``PolyMesh.__eq__`` is silent and no longer cubic.
- ``Circle`` and ``Sphere`` are hashable, by identity, like the other
geometries, so they can be put in sets and used as dictionary keys.
- ``Ellipse(axes=...)``, ``Ellipse(matrix=...)``, ``Ellipsoid(c=..,
ratio_bc=..)``, ``Square.area_expectation(side_lengths=...)``, the
``*_expectation`` methods with numpy scalars, ``Sphere.plot`` and 3D
``PolyMesh.plot``/``SeedList.plot_breakdown`` on a fresh figure,
``Rectangle.within`` for rotated rectangles, ``reflect`` for ellipses
and ellipsoids, single-material ``color_by`` settings, numpy arrays as
per-item plot keywords.

Changed
'''''''
- pyvoro is installed from the ``pyvoro-rimoli`` package instead of
``pyvoro-mmalahe``. Both provide the same ``pyvoro`` module.
pyvoro-mmalahe bundles Voro++ 0.4.6, whose radical (Laguerre)
tessellation can return a cell uncut when the compiler fuses
multiply-adds. On macOS, pip compiles pyvoro-mmalahe from source, and
Apple clang fuses them by default on Apple silicon. The cells then
overlap, and Triangle and TetGen can crash on the resulting polygonal
mesh. pyvoro-rimoli bundles the current Voro++, which gives the same
cells either way, and has wheels for Linux, macOS and Windows. Uninstall
pyvoro-mmalahe before upgrading (``pip uninstall pyvoro-mmalahe``),
since the two packages install the same files.
- The continuous integration installs the current pytest and no longer
installs tox from the requirements. The pinned tox 3.14 forced an old
pluggy, with which pytest itself fails to import, so no test could run.
The jobs of the test matrix no longer cancel each other on a failure.
- Read the Docs builds the documentation with Python 3.10 instead of 3.8,
which pyvoro-rimoli and the current versions of other dependencies do not
support. It installs ``requirements.txt`` like the documentation check of
the continuous integration, and ``docs/requirements.txt`` also keeps numpy
below 2, so that numpy is installed once.
- The facets of the triangular and tetrahedral meshes created from a
polygonal mesh are sorted (nodes in ascending order within a facet,
facets in lexicographic order), whatever the mesher. 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 the order of their facets.
- ``max_edge_length`` (``mesh_max_edge_length``) acts in 3D on the triangles
of 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 (see the ``pbx_interface_3D.xml`` example). The
geometric tests of a mesh against its polymesh use the non-planarity of
the facets (from the snapping of the points to the periodic faces) as
their tolerance, and each element is assigned to the cell in which its
centroid is deepest.
- ``Ellipsoid.limits`` is exact for rotated ellipsoids (it was sampled).
- A ``Seed`` created with a ``position`` (or a geometry with a center) has
its breakdown at that position. The geometry center is no longer reset
to the origin.
- ``Ellipse``, ``Ellipsoid`` and ``NBox`` geometries compare equal when
their parameters are equal, and keep their identity-based hash.
- Unused sampling helpers were removed from ``seeding.seedlist``.

`1.5.9`_ - 2023-10-05
--------------------------
Added
Expand Down Expand Up @@ -322,7 +508,8 @@ Added

.. LINKS

.. _`Unreleased`: https://github.com/kip-hart/MicroStructPy/compare/v1.5.9...HEAD
.. _`Unreleased`: https://github.com/kip-hart/MicroStructPy/compare/v1.6.0...HEAD
.. _`1.6.0`: https://github.com/kip-hart/MicroStructPy/compare/v1.5.9...v1.6.0
.. _`1.5.9`: https://github.com/kip-hart/MicroStructPy/compare/v1.5.8...v1.5.9
.. _`1.5.8`: https://github.com/kip-hart/MicroStructPy/compare/v1.5.7...v1.5.8
.. _`1.5.7`: https://github.com/kip-hart/MicroStructPy/compare/v1.5.6...v1.5.7
Expand Down
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ To install MicroStructPy, download it from PyPI using::

If there is an error with the install, try ``pip install pybind11`` first,
then install MicroStructPy.
When upgrading from an earlier version, run ``pip uninstall pyvoro-mmalahe``
first: MicroStructPy now uses ``pyvoro-rimoli``, which installs the same
``pyvoro`` module.


MicroStructPy can also be installed from source::
Expand Down
Loading
Loading