From ea571542648fa0b7591450e2de05a8f3fe109178 Mon Sep 17 00:00:00 2001 From: Julian Rimoli Date: Fri, 16 Oct 2026 11:33:04 -0700 Subject: [PATCH] Apply max_volume to the default phases in 2D Without phases, TriMesh.from_polymesh uses one default phase per phase number, and those carried 'max_volume': inf. The 2D meshers read the area limit of each region as phase.get('max_volume', max_volume), so the infinite default always won. The global limit is no longer passed to Triangle under the regional ones (the periodic mesher needs different limits for the cells and their copies), so the max_volume argument was ignored: a square meshed with max_volume=0.4 kept two triangles of area 8. On master the global limit applied underneath. The default phases no longer have a max_volume, so the argument applies to their regions, in the bounded and the periodic 2D meshers and in _facet_sizes. An explicit infinite max_volume of a phase still means no limit for that phase, as the changelog now says. The new test meshes a single cell without phases, bounded and periodic, and checks the largest triangle against the limit. The example digest of the documentation is updated for the change of the library. Reported by Kip Hart in the review of PR #114. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.rst | 5 ++++- docs/source/sphinx_gallery/plot_demos.py | 2 +- src/microstructpy/meshing/trimesh.py | 16 +++++++++------- tests/meshing/test_trimesh_fixes.py | 13 +++++++++++++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cbbdbc3..7b5f16c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -97,7 +97,10 @@ Fixed - 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``. + 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. diff --git a/docs/source/sphinx_gallery/plot_demos.py b/docs/source/sphinx_gallery/plot_demos.py index bf455ec..3692b42 100644 --- a/docs/source/sphinx_gallery/plot_demos.py +++ b/docs/source/sphinx_gallery/plot_demos.py @@ -24,7 +24,7 @@ # them is edited, and the build would serve stale figures. # # Managed by docs/example_digest.py. Do not edit by hand. -EXAMPLES_DIGEST = '2d39355b71ddd257' +EXAMPLES_DIGEST = '22bb4ed7ea2ddd9f' example_dir = '../../../src/microstructpy/examples' diff --git a/src/microstructpy/meshing/trimesh.py b/src/microstructpy/meshing/trimesh.py index b7b7f7c..c72350f 100644 --- a/src/microstructpy/meshing/trimesh.py +++ b/src/microstructpy/meshing/trimesh.py @@ -217,8 +217,8 @@ def from_polymesh(cls, polymesh, phases=None, mesher='Triangle/Tetgen', polymesh (PolyMesh): A polygon/polyhedron mesh. phases (list): *(optional)* A list of dictionaries containing options for each phase. - Default is - ``{'material_type': 'solid', 'max_volume': float('inf')}``. + Default is ``{'material_type': 'solid'}`` for each phase, + meshed with the ``max_volume`` argument. mesher (str): {'Triangle/TetGen' | 'Triangle' | 'TetGen' | 'gmsh'} specify the mesh generator. Default is 'Triangle/TetGen'. min_angle (float): The minimum interior angle, in degrees, of an @@ -844,8 +844,7 @@ def from_polymesh(cls, polymesh, mesh_size, phases=None): mesh_size (float): The side length of each pixel/voxel. phases (list): *(optional)* A list of dictionaries containing options for each phase. - Default is - ``{'material_type': 'solid', 'max_volume': float('inf')}``. + Default is ``{'material_type': 'solid'}`` for each phase. """ if phases is None: @@ -1965,10 +1964,13 @@ def _amorphous_seed_numbers(pmesh, phases): def _default_phases(polymesh): - """Default phases: one solid phase per phase number of the polymesh.""" + """Default phases: one solid phase per phase number of the polymesh. + + They have no ``max_volume``, so the ``max_volume`` argument of the + mesher applies to their regions. + """ n_phases = int(np.max(polymesh.phase_numbers)) + 1 - return [{'material_type': 'solid', 'max_volume': float('inf')} - for _ in range(n_phases)] + return [{'material_type': 'solid'} for _ in range(n_phases)] def _pt3d(pt): diff --git a/tests/meshing/test_trimesh_fixes.py b/tests/meshing/test_trimesh_fixes.py index dbe0623..4fb7d83 100644 --- a/tests/meshing/test_trimesh_fixes.py +++ b/tests/meshing/test_trimesh_fixes.py @@ -342,6 +342,19 @@ def test_triangle_per_phase_max_volume_exceeds_global(case_2d): assert np.all(areas[elem_phases != 0] <= 1e-3 + 1e-9) +@pytest.mark.parametrize('periodic', [False, True]) +def test_triangle_max_volume_without_phases(periodic): + # Without phases, the max_volume argument limits the area of the + # triangles. The default phases must not carry a limit of their own, + # which would replace it (an infinite one left it unapplied). + domain = msp.geometry.Square(side_length=4, corner=(0, 0)) + seeds = msp.seeding.SeedList([msp.seeding.Seed.factory( + 'circle', r=1.5, position=(2.2, 1.9))]) + pmesh = msp.meshing.PolyMesh.from_seeds(seeds, domain, periodic=periodic) + mesh = TriMesh.from_polymesh(pmesh, min_angle=20, max_volume=0.4) + assert np.max(simplex_volumes(mesh)) <= 0.4 + 1e-9 + + def test_triangle_infinite_max_volume_not_passed(case_2d, monkeypatch): pmesh, phases, _ = case_2d captured = {}