Adaptivity round 3: the 3D mover+adapt combination, spherical domains, and boundary snapping#409
Merged
Merged
Conversation
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
…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
Member
Author
|
Added in
AnnulusInternalBoundary gains the Underworld development team with AI support from Claude Code |
Underworld development team with AI support from Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()thenadapt()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
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
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