Skip to content

3D nested P1 MG prolongation has O(1) wrong entries (nested_prolongation_from_dms), invisible to its own test #449

Description

@lmoresi

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:

  1. exact match against coarse vertices and coarse edge midpoints;
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions