Skip to content

Remove the Null_Boundary fake natural BC and the label it was built on - #503

Open
lmoresi wants to merge 3 commits into
developmentfrom
bugfix/null-boundary-hack
Open

Remove the Null_Boundary fake natural BC and the label it was built on#503
lmoresi wants to merge 3 commits into
developmentfrom
bugfix/null-boundary-hack

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 6, 2026

Copy link
Copy Markdown
Member

What this removes

Two things, in two commits, plus a test.

  • 866ae55d — the fake natural BC. Whenever a solver had any natural BC, it
    manufactured one more on Null_Boundary, commented as "a workaround for some
    problem in the PETSc machinery where we need a surface integral term somewhere
    on every process if we have a contribution from anywhere".
  • feb86401 — the label itself. Null_Boundary, value 666, was added to every
    UW3 mesh at construction.
  • 2aec7923tests/parallel/test_0768_unowned_boundary_natural_bc_mpi.py,
    which pins the case the workaround claimed to need.

The measured basis

Value 666 marks no facet. Measured after a solve on a unit box: 198 points,
all vertices, zero edges. A boundary residual attached to a value that marks no
facet assembles over nothing, so the fake term was identically zero. The source
comment said as much ("integrates nothing over a bunch of points") without
following the thought through.

The DS was already rank-identical. Natural BCs are registered against the
consolidated UW_Boundaries label unconditionally on every rank —
PetscDSAddBoundary_UW runs in a loop with no rank-local skip. So the hack could
not have been fixing a rank-consistency problem. The one thing it could still
have supplied was a locally non-empty stratum on every rank, and that is what the
probes were built to test.

Answers do not move. A long flat box (4.0 x 0.25) makes the partitioner cut
along x, so the short end faces land on one rank each and everyone else owns
none of them. With a natural BC on such a face, results are bit-identical np=1
through np=8 with up to 7 of 8 ranks owning no facet of it: scalar Poisson
against the P2-exact T = x² at tolerance = 1e-10, a BdIntegral sweep
(byte-for-byte identical, diff clean), and Stokes traction and Nitsche free-slip
for the vector path the fake condition (0,)*u.shape[1] was actually shaped for.

Old checkpoints still read. A file written before the removal loads at np=1
and np=3, area exactly 1.0000000000, keeping its own Null_Boundary label and
enum member from the file. Old data keeps its labels; new meshes simply stop
manufacturing one. That is also why the sentinel skips stay — a reloaded mesh
still carries Null_Boundary in its boundaries enum, as do several caller enums
in docs/examples and test_1012.

What the test coverage actually is

Please do not read the parallel-suite pass count as evidence for this change. Of
the parallel tests run, exactly one existing file creates a natural BC at all
(test_0770_submesh_extract_mpi), and in it every rank owns part of both natural
boundaries — Internal [26,26] at np=2 and [12,14,13,13] at np=4. Ranks owning
none: zero. test_1064_rotated_freeslip_parallel uses a strong rotated
constraint, which never populates natural_bcs, so it never entered the removed
branch. Those tests are regression cover that nothing else broke. They say
nothing about this.

The new file is the coverage. Owned facets of Right, ranks owning none: 1 of 2,
2 of 3, 3 of 4, 5 of 6, 7 of 8. It carries a vacuity guard, because without
one a partitioner change would make every other test in it pass for no reason.
Both its negative controls were run and fire: dropping the scalar flux term moves
the relative L2 error from 3.6e-11 to 8.010e-03, and dropping the vector traction
makes the two Stokes solves agree to every digit.

It is a tripwire on current behaviour, not a proof that the PETSc problem the
workaround was aimed at is gone. Removing a failed fix does not establish that
the thing it failed to fix has gone away.

Hazard left in place, deliberately

Beside the DS registration loop sits

bc_is = bc_label.getStratumIS(value)   # petsc_generic_snes_solvers.pyx:3284

computed and never read. That dead line invites if bc_is is None: continue as
an apparent optimisation, which would reintroduce precisely the failure the
workaround claimed to guard against — and, before this PR, with every test still
green. It is also a latent issue-#291 segfault that never fires only because it
is never dereferenced. Out of scope here; flagged so it is not lost.

Where this came from

Found while landing the mesh DELETE primitive. Null_Boundary marks every vertex
of every mesh and UW_Boundaries re-packs it, so reading labelled points as
material interfaces refused 1114 of 1114 mesh-repair candidates, silently. That
fix (read edges, not points) is independent and already landed; this is the
upstream cleanup.

Verification

  • serial: 93 passed, including test_1012_stokesImportedDMPlex and the smoothing
    tests that skip the sentinel;
  • parallel np=2 and np=4: 23 passed each, including
    test_0766_box_internal_boundary_mpi — the issue-[BUG] The natural boundary condition is not working on the internal boundary #162 case, where losing these
    labels once silenced BoxInternalBoundary's natural BCs;
  • Nitsche paths, which also triggered the hack and were missing from the first
    sweep: test_1065_nitsche_local_h, test_0641_wave_c_api_shims, 48 passed;
  • new file: 4 passed at np=2 and np=4, skips cleanly in serial.

Full logs and probe scripts under ~/+Simulations/null_boundary_bc_hack/.

Underworld development team with AI support from Claude Code

lmoresi added 3 commits August 4, 2026 20:37
…ated nothing

Every solver with a natural BC quietly gained one it never asked for, on a
label marking every vertex of the mesh, justified by a comment that named no
PETSc version, no error and no test:

    a workaround for some problem in the PETSc machinery where we need a
    surface integral term somewhere on every process if we have a
    contribution from anywhere

Measured on the case that describes — a Neumann condition on one face of a long
flat box, at rank counts where up to 7 of 8 ranks own no facet of it, checked
against a manufactured solution rather than against convergence, because a
silently dropped surface term still converges, just to the wrong field. The
solution is x^2, which P2 reproduces exactly, and the solver tolerance is
tightened to 1e-10, so the error sits at the tolerance and a missing term is
unmissable rather than buried under discretisation error.

The answers are bit-identical to every digit, with and without the workaround,
at np = 1, 2, 3, 4, 6 and 8. A negative control that zeroes the flux term —
what a dropped surface integral amounts to — moves the error from 3.6e-11 to
8.0e-03, so the probe demonstrably can fail. Boundary integrals over the same
geometry are byte-for-byte identical too.

Why there was nothing to fix: natural BCs are registered by
PetscDSAddBoundary_UW against the consolidated UW_Boundaries label, with the
boundary's own value, unconditionally on every rank. There is no rank-local
skip, so the DS boundary list was already identical everywhere. (The
bc_is = bc_label.getStratumIS(value) alongside it is dead — computed, never
read.) And the fake BC could not have contributed anyway: value 666 marks 198
points on a test mesh, all of them VERTICES and none a facet, measured after a
solve, so PETSc never completes it into something integrable.

What it did do was mislead. Reading labelled points to find material interfaces
saw every vertex of every UW3 mesh labelled and silently refused 1114 of 1114
mesh-repair candidates.

test_stokes_natural_bc_constant_no_recompile asserted the workaround's presence
— it was guarding an older bug where _build() re-added the BC on every call,
resetting is_setup and forcing a recompile. That bug class cannot exist once
nothing is manufactured, so the assertion becomes its opposite: a solver's
natural_bcs holds exactly what the user asked for. The test's real subject, that
changing a natural-BC constant does not recompile, is untouched and still passes.

Probes and logs: ~/+Simulations/null_boundary_bc_hack/

Underworld development team with AI support from Claude Code
…misleads

Every UW3 mesh carried a label marking EVERY VERTEX in its chart with the
reserved value 666. It marks no facet, so it integrates nothing, and its only
functional consumer — a fake natural BC the solver manufactured — was removed in
the previous commit, measured to change no answer at any rank count.

What it cost is legibility. A label's presence on a point says nothing about
whether it means a material interface, and this label put a blanket over the
whole vertex stratum: a mesh-repair pass that looked for interfaces by reading
labelled points saw every vertex labelled and silently declined 1114 of 1114
candidates. Nothing errored; the feature was simply a no-op on every real mesh.

Removed from the two injected boundaries enums, from the label-creation block,
and from segmented.py's own enum.

KEPT DELIBERATELY: the sentinel skips in graph.py, discretisation_mesh.py and
adaptivity.py. A caller may still supply a boundaries enum declaring
Null_Boundary = 666 (tests/test_1012_stokesImportedDMPlex and several
docs/examples do), and a mesh reloaded from an older checkpoint still carries
both the label and the enum member. Downstream readers therefore cannot assume
it is absent, and _dm_unstack_bcs restores it only when the enum names it, so it
becomes a no-op for our meshes and stays correct for a caller's.

Backward compatibility measured rather than assumed: a checkpoint written before
the removal reads at np=1 and np=3 with area exactly 1.0000000000, keeping its
own label and enum member from the file. Old data keeps its labels; new meshes
stop manufacturing one.

Verification, against a baseline recorded before either commit: all three probe
sweeps byte-for-byte identical at np = 1, 2, 3, 4, 6, 8; 93 serial tests pass,
including the caller-declared-enum case; 23 parallel tests pass at np=2 and np=4,
including test_0766_box_internal_boundary_mpi — the issue-#162 case, where losing
these labels once made BoxInternalBoundary's natural BCs contribute nothing. The
heavy solver suite matches baseline exactly, down to the same single pre-existing
unrelated failure.

test_null_boundary_marks_every_vertex becomes
test_no_null_boundary_label_is_manufactured, which also pins the distinction from
All_Boundaries: the two are not synonyms and only one of them is removable.
All_Boundaries (1001) is a real geometric boundary — every exterior FACET, from
markBoundaryFaces — and is itself one of the values inside UW_Boundaries, the
single consolidated label the DS is pointed at. Measured on a unit box:
All_Boundaries 32 edges / 0 verts, Null_Boundary 0 edges / 98 verts.

The absence is asserted against the DM's list of label NAMES, not against
`getLabel(...) is None`: getLabel returns a non-None DMLabel wrapper with a null
handle for a name the DM does not have, so the `is None` form reports every
absent label as present. Same family as issue #291.

Probes and logs: ~/+Simulations/null_boundary_bc_hack/

Underworld development team with AI support from Claude Code
The removal of the fake Null_Boundary natural BC is pinned by tests; the
claim that it was SAFE to remove was pinned by nothing. No test in the
repo, before or after, exercises a natural BC on a boundary some rank
owns no part of. The only parallel test that creates a natural BC at all
is test_0770_submesh_extract_mpi, and there every rank owns part of both
natural boundaries (Internal [26,26] at np=2, [12,14,13,13] at np=4).

A long flat box (4.0 x 0.25) gets the discriminating case reliably: the
partitioner cuts along x, so the short end faces land on one rank each.
Measured owned facets of Right: [5,0] at np=2, [0,0,5,0] at np=4,
[5,0,0,0,0,0,0,0] at np=8 -- 7 of 8 ranks owning none.

Four tests: a vacuity guard that asserts some rank really does own none
(without it a partitioner change makes the rest pass for no reason), a
BdIntegral case, a scalar Poisson natural BC against the P2-exact
manufactured solution T = x^2, and a vector Stokes traction asserted by
difference rather than against a stored number.

Both negative controls fire. Dropping the scalar flux term moves the
relative L2 error from 3.6e-11 to 8.010e-03. Dropping the vector traction
makes the two Stokes solves agree to every digit (2.582851221410e-01),
which is what a term that never reached the residual looks like.

This is a tripwire on current behaviour, not a proof that the PETSc
problem the workaround was aimed at is gone -- removing a failed fix does
not establish that. The live hazard it guards is the dead
`bc_is = bc_label.getStratumIS(value)` sitting beside the DS registration
loop, which invites an `if bc_is is None: continue` optimisation that
would reintroduce exactly the failure the workaround claimed to prevent.

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings August 6, 2026 08:10
@lmoresi

lmoresi commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — bugfix/null-boundary-hack (866ae55d, feb86401)

Reviewed: removal of the fake Null_Boundary natural BC, and of the label.

Findings

1. The parallel-suite evidence was overstated. We reported "23 parallel
tests pass" as evidence for the removal. Of the parallel tests run, only
test_0770_submesh_extract_mpi creates a natural BC
(stokes.add_natural_bc(...), lines 178-179).
test_1064_rotated_freeslip_parallel uses add_rotated_freeslip_bc, a strong
constraint that never populates natural_bcs, so it never entered the removed
branch (if len(self.natural_bcs) > 0). Those tests are regression cover that
nothing else broke — not evidence about this change. Real parallel natural-BC
coverage in the suite: one file.

2. Nitsche was an unexamined surface. add_nitsche_bc appends directly to
self.natural_bcs (petsc_generic_snes_solvers.pyx:4221 vector, :6314
Stokes saddle-point), so every Nitsche solve triggered the hack. Two
hack-triggering test files were in neither the baseline nor the after run:
test_1065_nitsche_local_h.py, test_0641_wave_c_api_shims.py. Now run: 48
passed.

3. The vector case had no probe. The fake condition was built as
(0,)*self.Unknowns.u.shape[1] — shaped for a vector unknown — and every probe
was scalar Poisson. Added stokes_nitsche_unowned_box.py. Stokes traction on a
face 7 of 8 ranks own no facet of: vrms 2.582785783291e-01 (np=1) →
...783278e-01 (np=8), 12 figures. Nitsche free-slip at the bundle default
tolerance: converged, vrms 2.582981693232e-01 (np=1) vs 2.582998224075e-01
(np=4).

4. First version of that probe was true by construction. Uniform gravity in
a closed incompressible box is hydrostatic — vrms came out 1.25e-13, and
np-independence of zero is not evidence for anything. Re-posed as lid-driven,
with a -uw_mode none liveness control: none vrms 2.582851221413e-01 vs
natural 2.582785783291e-01, so the boundary term is live.

5. DIVERGED_LINE_SEARCH in the Nitsche arm is ours, not the code's.
At solver.tolerance = 1e-10 Nitsche fails the line search (reason −6) at every
rank count. Attributed by rebuilding at 22aae82c (pre-removal): identical
reason −6, and np=1 vrms bit-identical at 2.583006748159e-01. It is a
tolerance artefact — at the bundle default it converges (reason 3) at np=1 and
np=4. Not a defect, not attributable to this change.

6. Our own absence assertion was wrong. test_no_null_boundary_label_is_manufactured
first asserted dm.getLabel("Null_Boundary") is None and failed against a DM
that genuinely does not have the label: getLabel returns a non-None DMLabel
wrapper with handle 0, so is None reports every absent label as present. Now
asserted against [dm.getLabelName(i) for i in range(dm.getNumLabels())]. Same
family as #291.

7. The justification is not pinned by any test. Two different claims:

  • the removal happened — pinned. Re-adding the fake BC fails test_1001
    (natural_bcs == ["Top"]); re-adding the label fails test_0001.
  • the removal was safe — i.e. natural BCs assemble correctly when a rank owns
    no facet of the natural boundary — pinned by nothing in tests/.

The suite has one parallel test that creates a natural BC, test_0770, and in
it every rank owns part of both natural boundaries: Internal [26,26] at np=2
and [12,14,13,13] at np=4; Lower [13,13] and [6,5,6,9]. Ranks owning none:
zero. The discriminating condition is exercised by no test in the repo,
before or after this change. Our evidence for it (np=1..8, up to 7 of 8 ranks
owning nothing, bit-identical) lives in ~/+Simulations, which CI does not run.

The live hazard: the DS loop registers every natural BC on every rank
unconditionally, and bc_is = bc_label.getStratumIS(value) at :3284 sits
beside it computed and never read. That dead line invites
if bc_is is None: continue as an apparent optimisation, which would
reintroduce exactly the failure the workaround claimed to guard against, with
every test still green.

Attacks that failed

  • Label completion. Value 666 measured AFTER a solve: 198 points, all
    vertices, 0 facets. PETSc never completes it into anything integrable.
  • Accumulation across rebuilds. Guard held; no duplicates before or after.
  • Issue [BUG] The natural boundary condition is not working on the internal boundary #162 (losing these labels once silenced BoxInternalBoundary's
    natural BCs): test_0766_box_internal_boundary_mpi passes np=2 and np=4.
  • Caller enums declaring Null_Boundary = 666 (test_1012, several
    docs/examples): pass. extend_enum merges the caller's member; the sentinel
    skips were kept for exactly this.
  • Old checkpoints. A pre-removal file reads at np=1 and np=3, area exactly
    1.0000000000, keeping its own label and enum member from the file.
  • Scalar unowned-boundary case. Bit-identical to every digit, np=1..8.
  • BdIntegral sweep. Byte-for-byte identical (diff clean).
  • Dead code. bc_is = bc_label.getStratumIS(value) at :3284 is computed
    and never read — a latent Stokes_Constrained segfaults at np>1 in the interior-multiplier section reduction #291 segfault that never fires because it is not
    dereferenced. Left alone; out of scope.

Merge conditions

  • Land the unowned-boundary case as a parallel test
    (tests/parallel/test_0768_unowned_boundary_integral_mpi.py), scalar at
    minimum, so finding 7 is closed.
  • State in the PR body that parallel natural-BC coverage rests on one existing
    test file plus the new one — not on the 23-test count.

@lmoresi

lmoresi commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the legacy Null_Boundary sentinel label (value 666) and the solver-side “fake” natural BC that was being injected when any natural BCs were present, and adds a parallel regression test to ensure natural BC assembly and boundary integrals behave correctly even when some ranks own no facets of the target boundary.

Changes:

  • Remove the solver workaround that auto-added a zero traction natural BC on Null_Boundary.
  • Stop manufacturing the Null_Boundary label/member on newly constructed meshes (including segmented meshes).
  • Add an MPI regression test covering natural BCs and BdIntegral on boundaries that some ranks do not own.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_1001_poisson_constants.py Updates the Stokes constant-traction natural-BC test to assert no manufactured Null_Boundary BC; docstring updated.
tests/test_0001_meshes.py Changes mesh test from asserting presence of Null_Boundary to asserting its absence and validating All_Boundaries facet marking.
tests/parallel/test_0768_unowned_boundary_natural_bc_mpi.py Adds MPI “unowned boundary” regression coverage for boundary integrals, scalar Poisson natural BCs, and vector Stokes traction.
src/underworld3/meshing/segmented.py Removes Null_Boundary = 666 from the segmented mesh boundary enum.
src/underworld3/discretisation/discretisation_mesh.py Removes Null_Boundary enum patching and DM label construction from mesh initialization path; updates docstring accordingly.
src/underworld3/cython/petsc_generic_snes_solvers.pyx Removes the solver _build() block that injected a fake natural BC on Null_Boundary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +66
if uw.mpi.size > 1:
_nroots, ilocal, iremote = dm.getPointSF().getGraph()
ghosts = (set(range(0 if iremote is None else len(iremote)))
if ilocal is None else set(int(p) for p in ilocal))

Comment thread tests/test_0001_meshes.py
Comment on lines +208 to +209
all_bd = mesh.dm.getLabel("All_Boundaries")
assert all_bd is not None
Comment on lines +265 to +267
recompile. That fake BC has since been removed altogether — it was a
workaround for a PETSc problem that no longer exists, and it integrated
nothing (value 666 marks vertices, never a facet). So the assertion is now
@lmoresi

lmoresi commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

The dead bc_is line flagged in the PR body is now tracked as #506. It turned out to be four sites, not one — SNES_Scalar (twice), SNES_Vector, SNES_MultiComponent — all assigned and never read. Still out of scope here; the test this PR lands is what would catch a regression if someone "optimised" them into a rank-local skip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants