Skip to content

Adaptivity round 3: the 3D mover+adapt combination, spherical domains, and boundary snapping#409

Merged
lmoresi merged 7 commits into
developmentfrom
feature/adaptivity-round3
Jul 24, 2026
Merged

Adaptivity round 3: the 3D mover+adapt combination, spherical domains, and boundary snapping#409
lmoresi merged 7 commits into
developmentfrom
feature/adaptivity-round3

Conversation

@lmoresi

@lmoresi lmoresi commented Jul 24, 2026

Copy link
Copy Markdown
Member

What this delivers

The final round of the 3D adaptivity capstone: the mover and the refiner now compose in 3D, both work on spherical domains, and refined curved boundaries follow the true geometry instead of staying frozen on the base mesh's chords. Three bug fixes and one ruled feature, each found or decided by running the actual target problems.

The combination works in 3D

redistribute_nodes() then adapt() on a tetrahedral mesh now runs end to end. On the dipping-fault box it does what the 2D experiment promised: pre-grading the base with the mover spends the refinement budget better — 33,570 cells against 36,040 for adapt alone, slightly finer on the fault, with the over-refinement halo reduced from 46 to 39 percent of cells. Geometric multigrid on the combined mesh converges in 3 iterations and reproduces the exact linear solution.

Getting there exposed a PETSc subtlety worth knowing about: DMLabelSetValue does not remove a point from its previous stratum. Re-seeding the refinement-state label on the moved mesh therefore left every cell in two strata, and readers got the old value back — the refiner was silently running the moved geometry with a stale seed, and its conforming closure deadlocked. (Before finding this, the C driver itself was cleared by exhaustive comparison against the Python reference implementation: all 72 single-cell states through three passes, and all 3,456 valid two-cell state pairs.) The seed writer now destroys and recreates the label; a regression test locks re-seeding against a fresh seed of the same geometry.

Spherical domains

  • The node mover needed no changes for spherical shells: with the shells' registered analytic surfaces, a boundary-layer metric grades the mesh radially (ratio 1.32) with zero folded cells, and nodes slide on the spheres to machine precision.
  • A real multigrid gap surfaced: a refinement pass whose new vertices all land on a Dirichlet boundary adds only constrained degrees of freedom, making the finest transfer square — and the automatic multigrid pickup was rejecting that as 'no coarsening'. Boundary-focused metrics (exactly what curved domains invite) hit this every time, so every annulus boundary-layer adapt was silently falling back to the default preconditioner. With the guard corrected to reject only genuine size mismatches, annulus children solve with geometric multigrid in 2 iterations, identical to the algebraic reference.

Boundary snapping (the chord-vs-snap ruling)

Bisection places new boundary vertices at chord midpoints, so a curved domain's boundary geometry stayed frozen at base resolution no matter how deep the refinement — a measured radius error of about h²/8R that refinement never reduced. Per the maintainer ruling, every refinement generation now snaps its new boundary vertices onto the mesh's registered analytic surfaces, so each intermediate level is a valid mesh in its own right (extractable for solvers) and the metric marks on true geometry. On flat domains the snap is exactly a no-op (a chord midpoint of a plane face already lies in the plane), so all existing flat-mesh results are bit-unchanged.

An independent adversarial review of the branch caught one genuine hole before merge: in 3D parallel, a rank can hold a new boundary vertex through interior cells only, see no labelled boundary face for it, and skip the snap its neighbour applies. The snap mask is now made collective over the parallel point-sharing graph before use. The proof it works: an adapted spherical shell is exactly confluent at 1, 2, and 4 ranks (12,784 cells) with both spheres at analytic radius to 2e-16 on every rank, ghost vertices included. The review also added a loud fold guard (a base mesh too coarse for its boundary curvature now errors instead of tangling silently) and documented the disjoint-surfaces assumption at the snap site.

Evidence

  • Serial: mover/adapt family green (38 tests in the confluence-sensitive set plus the full family sweep); new gates for the re-seed, the annulus and shell snap (child and intermediate levels, <1e-12), all hardened against vacuous passes.
  • Parallel (np = 1/2/4): snapped annulus exactly confluent (1,068 cells, 2e-16); spherical shell exactly confluent (12,784 cells, 2e-16, ghosts included); the 3D mover-adapt composition runs clean at every rank count; the flat-domain confluence gates (test_0839/test_0842) unchanged.
  • Style gate clean, no allowlist additions.

Design note with the round-3 status and measurements: docs/developer/design/ADAPTIVITY_3D_SPHERICAL_2026-07.md. Renders of the 3D combination: ~/+Simulations/nvb_3d_adapt_evaluation/combo3d_*.png.

Underworld development team with AI support from Claude Code

lmoresi added 5 commits July 24, 2026 09:22
DMLabelSetValue does not remove a point from its previous stratum, so
overwriting an existing seed left every cell in TWO strata and readers
got the OLD value back. The redistribute-then-adapt composition on a 3D
mesh re-seeds the refinement state on the moved geometry — the driver
then silently ran the moved mesh with the stale unmoved-order seed and
the conforming drain deadlocked ('UWNVB drain stalled', found by the
round-3a combination experiment; a 72-state decode sweep and a
3456-case two-tet pair sweep against the Python engine cleared the C
driver itself first). write_tagged_state_label now destroys and
recreates the label. Regression test locks the re-seed against a
single fresh seed of the same geometry.

With this, the 3D mover+adapt combination runs end to end: 33,570
cells vs 36,040 adapt-only on the dipping-fault box (finer on-fault,
over-refinement fraction 46 -> 39 percent), MG 3 its, exact solution
9e-10.

Underworld development team with AI support from Claude Code
…true size mismatches

The mesh-owned FMG auto-pickup rejected any finest transfer with
pc >= pr as 'no coarsening'. But a refinement generation whose new
vertices all land on a Dirichlet boundary adds only CONSTRAINED dofs,
so the free-dof counts of the last two levels coincide and the
transfer is square — redundant but correct. Boundary-focused metrics
(exactly what curved domains invite) hit this every time: every
annulus boundary-layer adapt was silently downgraded to the default
preconditioner. With the guard relaxed to genuine row/operator
mismatches, the annulus child solves with custom MG in 2 iterations
(identical solution to the GAMG reference), snapped or unsnapped.

Underworld development team with AI support from Claude Code
…d/snap probes measured

Underworld development team with AI support from Claude Code
…ERY generation

Bisection puts new boundary vertices at chord midpoints, freezing a
curved domain's boundary geometry at base resolution no matter how deep
the refinement (measured: radius error h_base^2/8R ~ 2e-3 on the probe
annulus, unchanged by depth). Per the 2026-07-24 ruling, every
refinement generation now snaps its new boundary vertices onto the
mesh's registered analytic bounding surfaces (radial/plane BoundingSurface
restore) — not just the returned child — so each intermediate level is a
valid mesh in its own right, extractable for solvers, and the metric
marks on true geometry.

The serial cell-list engine snaps its OWN coordinates (they feed the
next generation's marking and midpoints); the native and SBR paths snap
each level DM. On plane surfaces the chord midpoint already lies in the
plane, so flat-domain results are bit-unchanged (confluence gates
green). Adapted annulus and SphericalShell children now hold analytic
radii to <1e-12 at every level (new gates in test_0836/test_0840); the
snapped children solve with custom-P MG in 2 iterations, identical to
the GAMG reference.

Underworld development team with AI support from Claude Code
…dened gates

The adversarial review of this branch found one real hole on the
capstone's target path: in 3D parallel, a boundary-edge midpoint can
live on a rank whose only cells containing it are INTERIOR members of
the edge's star — that rank sees no labelled face, skips the snap the
face-owning rank applies, and one global vertex ends with two
coordinates (rank-divergent marking, broken confluence). The snap mask
is now reduced over the point SF (ADD as logical-or) before snapping:
pre-snap coordinates are rank-consistent and the surface restore is a
pure function of them, so a consistent mask gives a consistent snap by
construction. Gate: SphericalShell adapt is exactly confluent at
np=1/2/4 (12,784 cells) with both spheres at 2e-16 on every rank's
chart, ghosts included.

Also from review: a loud fold guard after snapping (a cell incident to
a snapped vertex may not flip orientation — a base with h ~ R would
otherwise tangle silently), the disjoint-surfaces limitation documented
at the snap site (sequential restore does not converge to curved-curved
junctions), and the intermediate-level radius gate hardened against
vacuous passes.

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings July 24, 2026 01:47

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…keep them pinned

Embedded internal surfaces (the Internal circle/sphere of the
*InternalBoundary meshes) were topologically preserved by refinement
(conforming bisection + label carry) but geometrically chord-frozen,
and invisible to the snap because no bounding surface was registered
for them. They now register as radial surfaces flagged interior=True:

- adapt() includes them in the per-generation snap, so refinement
  follows the true interface radius (gate: AnnulusInternalBoundary
  child holds the internal circle to <1e-12);
- the slip machinery explicitly skips interior surfaces, so interface
  nodes stay FULLY pinned under the movers even with
  slip_surfaces=True — no normal or tangential motion. Interface
  motion is physics-owned (maintainer ruling, 2026-07-24; tangential
  slide would be a one-flag change if ever wanted).

AnnulusInternalBoundary gains the refinement knob the other
constructors already expose (adapt needs the hierarchy); the
SphericalShellInternalBoundary registration is in place for when the
gmsh blocker clears. Gate locks both behaviours: snap under adapt,
bitwise-unmoved interface nodes under the mover.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Added in 2508596b, following the maintainer discussion on internal surfaces: embedded interfaces (the Internal circle/sphere of the *InternalBoundary meshes) now register as radial bounding surfaces flagged interior=True. Refinement was already preserving them topologically (conforming bisection splits interface faces with their edges, labels carry through both refinement paths) — but geometrically they stayed chord-frozen and the snap could not see them. Now:

  • adapt() snaps refinement onto the true interface radius (gate: an adapted AnnulusInternalBoundary holds its internal circle to 1e-12);
  • the movers keep interface nodes fully pinned — no normal or tangential motion, even with slip_surfaces=True — since interface motion is physics-owned (the slip machinery explicitly skips interior-flagged surfaces; the gate asserts interface nodes are bitwise unmoved while the rest of the mesh redistributes). Tangential slide along an interface would be a one-flag change if ever wanted.

AnnulusInternalBoundary gains the refinement knob the other constructors already expose; the SphericalShellInternalBoundary registration is in place for when the gmsh meshing blocker clears.

Underworld development team with AI support from Claude Code

Underworld development team with AI support from Claude Code
@lmoresi
lmoresi merged commit dafdcc6 into development Jul 24, 2026
2 checks passed
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