Skip to content

Rotated free-slip silently ignores a mesh-owned custom-FMG hierarchy (adapt children fall back to GAMG) #467

Description

@lmoresi

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:8825after 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

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