Summary
The vertex-level nested MG prolongation recorded by mesh.adapt (landed in #433) produces O(1) wrong values at 1–2 fine vertices per refinement pass in 3D. The affected vertices lie on no coarse edge, which is exactly the set tests/test_0753_nested_mg_prolongation.py::test_reproduces_an_arbitrary_coarse_field skips — so it merged undetected.
Root cause
underworld3/utilities/nvb.py::nested_prolongation_from_dms resolves in rounds:
- exact match against coarse vertices and coarse edge midpoints;
- rounds 2+: any still-unresolved fine vertex equal to the average of two already-resolved fine neighbours takes the average of their weights.
Round 2 is inference, not topology — the function's own comment says so ("being fine-mesh neighbours does not by itself make the segment an edge of the refinement tree").
In 3D the Maubach split bisects (tuple[0], tuple[tag]) and creates edges from the new midpoint m to the two off-refedge vertices. Those edges run through the coarse tet's interior, so their midpoints are not at any coarse edge midpoint and are not matched in round 1. Round 2 then assigns them 1/2, 1/2 on a coarse edge that does not contain them.
Evidence
Band-refined 3D box (UnstructuredSimplexBox(cellSize=0.5, refinement=1), adapt(metric, max_levels=2)), random coarse nodal field, compared against an independent reference (containing cell by geometric point location, value by the closed-form simplex Lagrange basis — deliberately not uw.function.evaluate, cf #432):
| generation |
disagreeing nodes |
|vertex transfer − truth| |
|cell-based transfer − truth| |
lie on a coarse edge |
| 2 |
2 / 1004 |
1.64 |
5.3e-16 |
0 / 2 |
| 3 |
1 / 1608 |
1.47 |
0.0 |
0 / 1 |
| 4 |
1 / 2136 |
2.31 |
4.4e-16 |
0 / 1 |
Generations 0, 1 and 5 are identical between the two constructions. 2D is unaffected (identical at every generation).
The comparison does not assume which construction is right: both are measured against the same independent reference.
Why the test missed it
test_reproduces_an_arbitrary_coarse_field validates a fine vertex by locating the coarse edge it lies on and taking the P1 value along it, then asserts only checked > 0.9 * (fvE - fvS). Vertices on no coarse edge are silently skipped — and those are precisely the wrong ones. The slack is structural in 3D, not a tolerance: the Maubach diagonal midpoints can never lie on a coarse edge, so tightening the bound to 100% would fail rather than catch this.
Possible impact
This is a candidate explanation for the recorded observation that the exact nested P1 transfer converged one iteration worse than the barycentric geometric builder in 3D (5,6,5 vs 4,5,4). It was installing a prolongation with a few O(1)-wrong entries.
Status / suggested fix
Work on #425 (exact any-degree nested transfer) builds the prolongation from the parent cell map returned by the refinement transform, evaluating the coarse basis inside the parent. That construction is exact at degree 1 too and matches the independent reference to machine precision, so #425 routes all degrees through it and no longer consumes _adapt_prolongation.
That leaves two options for this issue:
A regression test is included with #425
(tests/test_0847_nested_transfer_any_degree.py::test_the_vertex_transfer_is_wrong_in_3d_and_the_cell_transfer_is_not)
which pins the defect and will fail loudly if it is fixed, as a prompt to retire the test with the issue.
Underworld development team with AI support from Claude Code
Summary
The vertex-level nested MG prolongation recorded by
mesh.adapt(landed in #433) produces O(1) wrong values at 1–2 fine vertices per refinement pass in 3D. The affected vertices lie on no coarse edge, which is exactly the settests/test_0753_nested_mg_prolongation.py::test_reproduces_an_arbitrary_coarse_fieldskips — so it merged undetected.Root cause
underworld3/utilities/nvb.py::nested_prolongation_from_dmsresolves in rounds:Round 2 is inference, not topology — the function's own comment says so ("being fine-mesh neighbours does not by itself make the segment an edge of the refinement tree").
In 3D the Maubach split bisects
(tuple[0], tuple[tag])and creates edges from the new midpointmto the two off-refedge vertices. Those edges run through the coarse tet's interior, so their midpoints are not at any coarse edge midpoint and are not matched in round 1. Round 2 then assigns them1/2, 1/2on a coarse edge that does not contain them.Evidence
Band-refined 3D box (
UnstructuredSimplexBox(cellSize=0.5, refinement=1),adapt(metric, max_levels=2)), random coarse nodal field, compared against an independent reference (containing cell by geometric point location, value by the closed-form simplex Lagrange basis — deliberately notuw.function.evaluate, cf #432):Generations 0, 1 and 5 are identical between the two constructions. 2D is unaffected (identical at every generation).
The comparison does not assume which construction is right: both are measured against the same independent reference.
Why the test missed it
test_reproduces_an_arbitrary_coarse_fieldvalidates a fine vertex by locating the coarse edge it lies on and taking the P1 value along it, then asserts onlychecked > 0.9 * (fvE - fvS). Vertices on no coarse edge are silently skipped — and those are precisely the wrong ones. The slack is structural in 3D, not a tolerance: the Maubach diagonal midpoints can never lie on a coarse edge, so tightening the bound to 100% would fail rather than catch this.Possible impact
This is a candidate explanation for the recorded observation that the exact nested P1 transfer converged one iteration worse than the barycentric geometric builder in 3D (5,6,5 vs 4,5,4). It was installing a prolongation with a few O(1)-wrong entries.
Status / suggested fix
Work on #425 (exact any-degree nested transfer) builds the prolongation from the parent cell map returned by the refinement transform, evaluating the coarse basis inside the parent. That construction is exact at degree 1 too and matches the independent reference to machine precision, so #425 routes all degrees through it and no longer consumes
_adapt_prolongation.That leaves two options for this issue:
nested_prolongation_from_dmsandMesh._adapt_prolongationonce Use the nested hierarchy for MG transfers on adapt children (topological prolongation) #425 lands (nothing in the library would consume them), orA regression test is included with #425
(
tests/test_0847_nested_transfer_any_degree.py::test_the_vertex_transfer_is_wrong_in_3d_and_the_cell_transfer_is_not)which pins the defect and will fail loudly if it is fixed, as a prompt to retire the test with the issue.
Underworld development team with AI support from Claude Code