Design note: docs/developer/design/nested-vs-geometric-mg-transfers.md (commit e984a3b).
The inconsistency
mesh.adapt() maintains an exact refinement hierarchy — that is what newest-vertex bisection with conforming closure is for, and what the escape/halo cost buys. The MG transfer then discards it and re-derives an approximation geometrically:
tri = Delaunay(coarse_coords) # re-triangulate the coarse DOF cloud
simp = tri.find_simplex(fine_coords) # locate each fine DOF in one simplex
We pay for exact hierarchy maintenance and then do not use it.
This is the root cause of #424, not a separate concern: the barycentric builder has only local support, so moving fine coordinates (relax(), snapping, free-surface deformation) can leave a coarse DOF with no fine image -> zero column -> singular PᵀAP.
The nested transfer is trivial
Every fine vertex is either an inherited coarse vertex (weight 1 on itself) or the midpoint of a coarse edge (weights ½, ½ on its endpoints). It is:
Both engines already record it: edge2mid in NVBMesh and TaggedBisectionMesh, and the native nvb_transform.c is a real DMPlexTransform with the same split-edge bookkeeping. Separately, petsc_dm_set_regular_refinement — our wrapper for the flag PETSc checks to take its exact nested interpolation path — appears exactly once in the repo: its own definition. Nothing calls it.
Scope: keep BOTH paths
The geometric builder stays, for the non-nested pairs it was asked for (moved base vs adapted child, unrelated meshes, and the planned non-hierarchical variant with newly added seed points). The change is to stop using it where a topological relation exists.
Selection: topological when the parent/child map is present, geometric otherwise. The RBF retry from #424 then becomes a fallback that should essentially never fire on adapt children.
The hard part — capture ordering
The map must be in DM point numbering, and the bridge from engine vertex ids to DM points is coordinate matching, which is exact only while midpoints are still exact float averages of their parents. Snapping and relaxation destroy that:
- native path:
_nvbx.refine() -> DM, then snap_level_boundaries(). There is a clean window.
- cell-list path: the snap is applied to
nvb.coords before to_dm(), so by the time a DM exists the window has closed.
Preferred fix: have to_dm() return its engine-id -> DM-point map directly, removing the dependence on coordinate matching altogether.
Proposed shape
child._adapt_prolongation[k] = (parents, weights), parents shape (n_fine, 2) in DM vertex numbering; inherited vertex is (c, c) with weights (1, 0).
custom_mg gains a "topological" builder consuming it, expanded from vertices to DOFs for the field basis.
To measure, not assume
With snap-every-generation a midpoint is moved onto a curved boundary, so it is no longer at ½(a+b) physically, and the ½,½ transfer is then not the FE-exact interpolation there — the geometric builder is. MG needs full rank and good representation of smooth functions, not FE-exactness, so ½,½ should be fine (it is the classical geometric-MG choice). But on strongly curved or relaxed meshes geometric may be more accurate. Accuracy vs robustness — measure on the annulus and spherical-shell cases before settling the default.
Design note:
docs/developer/design/nested-vs-geometric-mg-transfers.md(commit e984a3b).The inconsistency
mesh.adapt()maintains an exact refinement hierarchy — that is what newest-vertex bisection with conforming closure is for, and what the escape/halo cost buys. The MG transfer then discards it and re-derives an approximation geometrically:We pay for exact hierarchy maintenance and then do not use it.
This is the root cause of #424, not a separate concern: the barycentric builder has only local support, so moving fine coordinates (
relax(), snapping, free-surface deformation) can leave a coarse DOF with no fine image -> zero column -> singularPᵀAP.The nested transfer is trivial
Every fine vertex is either an inherited coarse vertex (weight 1 on itself) or the midpoint of a coarse edge (weights ½, ½ on its endpoints). It is:
Both engines already record it:
edge2midinNVBMeshandTaggedBisectionMesh, and the nativenvb_transform.cis a realDMPlexTransformwith the same split-edge bookkeeping. Separately,petsc_dm_set_regular_refinement— our wrapper for the flag PETSc checks to take its exact nested interpolation path — appears exactly once in the repo: its own definition. Nothing calls it.Scope: keep BOTH paths
The geometric builder stays, for the non-nested pairs it was asked for (moved base vs adapted child, unrelated meshes, and the planned non-hierarchical variant with newly added seed points). The change is to stop using it where a topological relation exists.
Selection: topological when the parent/child map is present, geometric otherwise. The RBF retry from #424 then becomes a fallback that should essentially never fire on adapt children.
The hard part — capture ordering
The map must be in DM point numbering, and the bridge from engine vertex ids to DM points is coordinate matching, which is exact only while midpoints are still exact float averages of their parents. Snapping and relaxation destroy that:
_nvbx.refine()-> DM, thensnap_level_boundaries(). There is a clean window.nvb.coordsbeforeto_dm(), so by the time a DM exists the window has closed.Preferred fix: have
to_dm()return its engine-id -> DM-point map directly, removing the dependence on coordinate matching altogether.Proposed shape
child._adapt_prolongation[k] = (parents, weights),parentsshape(n_fine, 2)in DM vertex numbering; inherited vertex is(c, c)with weights(1, 0).custom_mggains a"topological"builder consuming it, expanded from vertices to DOFs for the field basis.To measure, not assume
With snap-every-generation a midpoint is moved onto a curved boundary, so it is no longer at ½(a+b) physically, and the ½,½ transfer is then not the FE-exact interpolation there — the geometric builder is. MG needs full rank and good representation of smooth functions, not FE-exactness, so ½,½ should be fine (it is the classical geometric-MG choice). But on strongly curved or relaxed meshes geometric may be more accurate. Accuracy vs robustness — measure on the annulus and spherical-shell cases before settling the default.