From ebbe0079348f44b24dba0a396811207137bc216d Mon Sep 17 00:00:00 2001 From: lmoresi Date: Fri, 24 Apr 2026 14:30:11 +1000 Subject: [PATCH] Fix AMR swarm migration IndexError after mesh.adapt() (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nuke_coords_and_rebuild invalidated the kd-tree, mesh sizes, centroids, and (since #130) the per-variable coord cache, but did NOT invalidate the per-cell face control-point arrays (faces_outer_control_points, faces_inner_control_points). These are populated lazily by _get_mesh_face_control_points and sized (num_faces, num_local_cells, dim) — keyed on cell count. After mesh.adapt() the new mesh has a different cell count, so the stale arrays from the old mesh would be indexed with new-mesh cell IDs in _test_if_points_in_cells_internal, raising "IndexError: index N is out of bounds for axis 1 with size M" when the new cell count exceeded the old one (and silently corrupting results when it did not). Set both arrays to None inside nuke_coords_and_rebuild so the next call to the populator regenerates them at the new cell count. Adds tests/test_0810_amr_swarm_migration_regression.py, a level_1 regression that runs the failing case from the issue (UnstructuredSimplexBox + h_min=0.05 metric) and asserts swarm migration completes without IndexError. Skipped automatically when the running PETSc lacks the pragmatic backend. Verified: pre-fix raises IndexError on the reproducer; post-fix the reproducer and the new regression test both pass. Underworld development team with AI support from Claude Code --- .../discretisation/discretisation_mesh.py | 10 ++++ ...est_0810_amr_swarm_migration_regression.py | 58 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/test_0810_amr_swarm_migration_regression.py diff --git a/src/underworld3/discretisation/discretisation_mesh.py b/src/underworld3/discretisation/discretisation_mesh.py index 14f85a105..e955b18a4 100644 --- a/src/underworld3/discretisation/discretisation_mesh.py +++ b/src/underworld3/discretisation/discretisation_mesh.py @@ -1653,6 +1653,16 @@ def nuke_coords_and_rebuild( # Invalidate projected boundary normals (rebuilt lazily on access) self._projected_normals = None + # BUGFIX(#135): invalidate the per-cell face control-point arrays. + # These are populated lazily by _get_mesh_face_control_points, sized + # (num_faces, num_local_cells, dim). After mesh.adapt() the new mesh + # has a different cell count, so the stale arrays from the old mesh + # would be indexed with new-mesh cell IDs in + # _test_if_points_in_cells_internal — producing IndexError when the + # new cell count exceeds the old one (and silent corruption otherwise). + self.faces_outer_control_points = None + self.faces_inner_control_points = None + # BUGFIX(#130): refill the coord cache for every already-registered # variable. Variables created before this rebuild would otherwise # have their cache entry (from __init__) wiped above and refill diff --git a/tests/test_0810_amr_swarm_migration_regression.py b/tests/test_0810_amr_swarm_migration_regression.py new file mode 100644 index 000000000..727c17d86 --- /dev/null +++ b/tests/test_0810_amr_swarm_migration_regression.py @@ -0,0 +1,58 @@ +"""Regression test for issue #135. + +After mesh.adapt(), swarm migration into the new mesh raised IndexError in +Mesh._test_if_points_in_cells_internal because the per-cell face control-point +caches (faces_outer_control_points / faces_inner_control_points) were not +invalidated by nuke_coords_and_rebuild. Cell IDs from the new mesh would index +into stale arrays sized for the old mesh's cell count. + +Reproducer reduced from the user's report (bknight1, 2026-04-23). Skipped if +the running PETSc was not built with the pragmatic backend. +""" + +import pytest +import numpy as np +import sympy + +import underworld3 as uw +from test_0800_optional_modules import requires_amr + + +pytestmark = [pytest.mark.level_1, requires_amr] + + +@requires_amr +def test_swarm_migration_after_adapt_does_not_raise(): + """mesh.adapt() then swarm._force_migration_after_mesh_change() must not + raise IndexError on the failing case from issue #135. + """ + mesh = uw.meshing.UnstructuredSimplexBox( + minCoords=(-1.0, 0.0), + maxCoords=(1.0, 1.0), + cellSize=0.2, + regular=False, + qdegree=3, + ) + + swarm = uw.swarm.Swarm(mesh=mesh) + swarm.populate(fill_param=4) + + cells_before = mesh.dm.getHeightStratum(0)[1] + + x, h_min, h_max, sigma = sympy.symbols("x h_min h_max sigma", positive=True) + h_expr = h_min + (h_max - h_min) * (1 - sympy.exp(-((x / sigma) ** 2))) + h_subbed = h_expr.subs({h_min: 0.05, h_max: 0.5, sigma: 0.25}) + h_vals = uw.function.evaluate(h_subbed, mesh.X.coords) + + metric = uw.adaptivity.create_metric(mesh, h_vals) + mesh.adapt(metric) + + cells_after = mesh.dm.getHeightStratum(0)[1] + assert cells_after != cells_before, ( + "test premise: mesh.adapt should change cell count for this metric" + ) + + # Pre-fix: this line raises + # IndexError: index N is out of bounds for axis 1 with size M + # because faces_*_control_points kept the old (cells_before-sized) arrays. + swarm._force_migration_after_mesh_change()