A mesh.adapt() refinement child carries its static coarse tail as mesh._custom_mg_coarse_meshes. On the standard solve path, auto_inject_custom_mg turns that into a registered solver._custom_mg so every solver on an adapted mesh drives geometric MG with no per-solver call. Under rotated free-slip that never happens, and the solve silently falls back to GAMG.
Measured
velocity_pc as reported by solver._rotated_freeslip_info (annulus, rotated free-slip on Upper):
| configuration |
velocity_pc |
| no hierarchy at all |
GAMG |
mesh-owned tail (what adapt() leaves) |
GAMG |
explicit set_custom_fmg |
custom-FMG |
The mesh-owned case is indistinguishable from having no hierarchy at all.
Repro: ~/+Simulations/rotated_pmat_audit/mesh_owned_fmg_pickup.py.
Cause
Two facts that only bite in combination:
auto_inject_custom_mg(self, field_id=0) is invoked from the standard Stokes solve at petsc_generic_snes_solvers.pyx:8825 — after the rotated free-slip dispatch at :8682 has already early-returned into rotated_bc.solve_rotated_freeslip.
rotated_bc._build_rotated_custom_Pl consults only solver._custom_mg. It never looks at solver.mesh._custom_mg_coarse_meshes.
So the auto-pickup that exists precisely to make adapted meshes get FMG for free is unreachable from the one BC that most needs geometric MG.
Why it matters
This is the adapt-on-top-faults workflow's own configuration: a fault resolved by local refinement on an adapt() child, with rotated strong free-slip (chosen over Nitsche because it composes with transverse isotropy). That combination is exactly the one that loses its multigrid, and it loses it silently — the solve converges, just on a much weaker preconditioner.
The adapt-on-top-faults skill documents the plain refined Annulus case ("native FMG is ignored; to get FMG you must build a coarse-mesh tail and call set_custom_fmg"). The adapt-child case is worse and is not documented: the hierarchy genuinely exists and is discarded.
Suggested fix
Not a reordering — the injection is late on the standard path because the velocity sub-PC is only reachable once the monolithic Jacobian is assembled, and the rotated path builds its own IS-based fieldsplit inside rotated_bc anyway.
Instead, _build_rotated_custom_Pl should fall back to the mesh-owned tail when no solver-set hierarchy is registered: build a CustomMGHierarchy([*coarse, solver.mesh], field_id=0) itself, with the same opportunistic try/except and barycentric→RBF builder fallback auto_inject_custom_mg uses (see #424), so a failed build degrades to GAMG rather than crashing the solve.
Worth also asserting velocity_pc == "custom-FMG" in a test on an adapt child, now that the rotated solve reports it (#465).
Found while auditing the rotated preconditioner in #465; not fixed there to keep that change scoped.
Underworld development team with AI support from Claude Code
A
mesh.adapt()refinement child carries its static coarse tail asmesh._custom_mg_coarse_meshes. On the standard solve path,auto_inject_custom_mgturns that into a registeredsolver._custom_mgso every solver on an adapted mesh drives geometric MG with no per-solver call. Under rotated free-slip that never happens, and the solve silently falls back to GAMG.Measured
velocity_pcas reported bysolver._rotated_freeslip_info(annulus, rotated free-slip onUpper):GAMGadapt()leaves)GAMGset_custom_fmgcustom-FMGThe mesh-owned case is indistinguishable from having no hierarchy at all.
Repro:
~/+Simulations/rotated_pmat_audit/mesh_owned_fmg_pickup.py.Cause
Two facts that only bite in combination:
auto_inject_custom_mg(self, field_id=0)is invoked from the standard Stokes solve atpetsc_generic_snes_solvers.pyx:8825— after the rotated free-slip dispatch at:8682has already early-returned intorotated_bc.solve_rotated_freeslip.rotated_bc._build_rotated_custom_Plconsults onlysolver._custom_mg. It never looks atsolver.mesh._custom_mg_coarse_meshes.So the auto-pickup that exists precisely to make adapted meshes get FMG for free is unreachable from the one BC that most needs geometric MG.
Why it matters
This is the
adapt-on-top-faultsworkflow's own configuration: a fault resolved by local refinement on anadapt()child, with rotated strong free-slip (chosen over Nitsche because it composes with transverse isotropy). That combination is exactly the one that loses its multigrid, and it loses it silently — the solve converges, just on a much weaker preconditioner.The
adapt-on-top-faultsskill documents the plain refinedAnnuluscase ("native FMG is ignored; to get FMG you must build a coarse-mesh tail and callset_custom_fmg"). The adapt-child case is worse and is not documented: the hierarchy genuinely exists and is discarded.Suggested fix
Not a reordering — the injection is late on the standard path because the velocity sub-PC is only reachable once the monolithic Jacobian is assembled, and the rotated path builds its own IS-based fieldsplit inside
rotated_bcanyway.Instead,
_build_rotated_custom_Plshould fall back to the mesh-owned tail when no solver-set hierarchy is registered: build aCustomMGHierarchy([*coarse, solver.mesh], field_id=0)itself, with the same opportunistictry/exceptand barycentric→RBF builder fallbackauto_inject_custom_mguses (see #424), so a failed build degrades to GAMG rather than crashing the solve.Worth also asserting
velocity_pc == "custom-FMG"in a test on an adapt child, now that the rotated solve reports it (#465).Found while auditing the rotated preconditioner in #465; not fixed there to keep that change scoped.
Underworld development team with AI support from Claude Code