From 9bf307df71625c312b6acec1ee1b2e48ed1f056c Mon Sep 17 00:00:00 2001 From: Tyagi Date: Fri, 23 Oct 2026 17:45:00 +1100 Subject: [PATCH] Preserve globally surviving submesh boundary names on empty ranks Region filtering previously selected boundary enum members from rank-local strata. A coarse extracted annulus lost Upper on some ranks, causing KeyError in natural Stokes boundary registration while peers entered collectives. Union label presence over the DM communicator and create empty labels where globally retained boundaries have no local points. Preserve candidate order and exclude globally absent labels. Validation: two focused regressions pass serial and on each of two MPI ranks, including actual region extraction. No equations, coordinates, partitioning or finite-element values change. Next validate the existing int32 Gadi installation gates and rerun only the four affected coarse Poisson models. --- .../discretisation/discretisation_mesh.py | 13 ++++++-- .../test_1085_submesh_boundary_names.py | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/parallel/test_1085_submesh_boundary_names.py diff --git a/src/underworld3/discretisation/discretisation_mesh.py b/src/underworld3/discretisation/discretisation_mesh.py index ccfca8c36..2d94eee9f 100644 --- a/src/underworld3/discretisation/discretisation_mesh.py +++ b/src/underworld3/discretisation/discretisation_mesh.py @@ -1933,8 +1933,9 @@ def _surviving_labels(self, subdm, candidates): Returns ------- dict - The subset of ``candidates`` present on ``subdm`` with a - non-empty stratum, preserving candidate order. + Candidates with a non-empty stratum anywhere on the DM communicator, + preserving candidate order. Empty ranks retain the same names and + empty labels, so solver boundary registration is rank independent. """ present = set() for i in range(subdm.getNumLabels()): @@ -1961,6 +1962,14 @@ def _surviving_labels(self, subdm, candidates): lsis = lab.getStratumIS(value) if lsis is not None and lsis.getSize() > 0: present.add(name) + # A boundary may be absent from this rank while present on its peers. + # Its enum and label must nevertheless exist everywhere: natural BC + # registration and solver setup use the same named boundaries on all ranks. + present = set().union(*subdm.comm.tompi4py().allgather(present)) + local_names = {subdm.getLabelName(i) for i in range(subdm.getNumLabels())} + for name in candidates: + if name in present and name not in local_names: + subdm.createLabel(name) return {name: v for name, v in candidates.items() if name in present} def extract_region(self, label_name, label_value=None): diff --git a/tests/parallel/test_1085_submesh_boundary_names.py b/tests/parallel/test_1085_submesh_boundary_names.py new file mode 100644 index 000000000..a6795158d --- /dev/null +++ b/tests/parallel/test_1085_submesh_boundary_names.py @@ -0,0 +1,31 @@ +"""Submesh boundary names must survive on ranks owning no boundary points.""" + +from petsc4py import PETSc +import underworld3 as uw + + +def test_surviving_labels_use_global_presence_and_create_empty_local_labels(): + dm = PETSc.DMPlex().createBoxMesh([2, 2], simplex=False, comm=uw.mpi.comm) + if uw.mpi.rank == 0: + dm.createLabel('OnlyOnZero') + dm.setLabelValue('OnlyOnZero', dm.getChart()[0], 42) + surviving = uw.discretisation.Mesh._surviving_labels( + None, dm, {'OnlyOnZero': 42, 'AbsentEverywhere': 99} + ) + assert surviving == {'OnlyOnZero': 42} + assert dm.getLabel('OnlyOnZero') is not None + assert 'AbsentEverywhere' not in [dm.getLabelName(i) for i in range(dm.getNumLabels())] + if uw.mpi.rank > 0: + values = dm.getLabel('OnlyOnZero').getValueIS() + assert values is None or len(values.getIndices()) == 0 + dm.destroy() + + +def test_extracted_region_boundary_enums_are_identical_on_every_rank(): + mesh = uw.meshing.BoxInternalBoundary( + elementRes=(12,12), zelementRes=(10,2), zintCoord=.85, + minCoords=(0.,0.), maxCoords=(1.,1.), simplex=True, qdegree=2) + child = mesh.extract_region('Outer') + names = [(b.name,b.value) for b in child.boundaries] + assert all(other == names for other in uw.mpi.comm.allgather(names)) + assert 'Top' in dict(names)