One owner for the geometric-MG option bundle; rotated free-slip picks up a mesh-owned hierarchy (#468, #467) - #471
Conversation
…rarchies on the rotated path (#468, #467) Three routes reach a multigrid Stokes velocity block — native (PETSc interpolation between refined DMPlex levels), custom-P on the standard solve path, and custom-P through the rotated free-slip path. They are the same preconditioner reached three ways, not alternatives: custom-P is mandatory wherever native cannot go, namely rotated BCs (the DM-coupled hierarchy cannot express a per-node rotation) and adapt() children (no DMPlex refinement relation). The option bundle was written in two places and had drifted (#468). The native path had been moved to a measured gmres+sor smoother; custom_mg._configure_pcmg never received that update, and never SET mg_levels_ksp_max_it at all, so the smoother inherited whatever last wrote that options prefix — 3 left behind by the GAMG bundle on the standard path, PETSc's own PCMG default of 2 on the rotated path. The same function smoothed differently depending on what had run before it. utilities/multigrid_options.py now owns both bundles (geometric MG and the GAMG fallback) and all three writers read from it. Each bundle DERIVES the stale keys it must clear — every key a sibling bundle sets that it does not — instead of carrying a hand-maintained delete list; the derivation reproduces all three previous hand-written lists exactly. The rotated route's svd coarse solve is a named variant of the shared bundle (coarse="svd") rather than a call-site override, because the Galerkin-coarsened rotated block inherits the rigid-rotation null space where redundant/LU hits a zero pivot (#306). The #276 single-field injection fragility deliberately stays out of the bundle: it is a routing decision, not an option value. Separately (#467), mesh.adapt() leaves a coarse tail on its refinement child so every solver on an adapted mesh gets geometric MG with no per-solver call. The rotated path never consulted it — the standard path's injection hook runs after the rotated dispatch has already returned — so an adapt child under rotated free-slip fell back to GAMG, indistinguishable from having no hierarchy at all. That is the adapt-on-top-faults workflow's own configuration. custom_mg.build_transfers is now the shared "which hierarchy does this solver get?" rule for both paths, with the same opportunistic barycentric->RBF fallback and degrade-to-GAMG on failure. Measured (same operator, RHS and coarse solve; two-level hierarchy, i.e. where the native measurement says the gmres margin is SMALLEST): route pre-fix unified custom-P standard (isotropic annulus) 5 vel its 4 vel its custom-P rotated (TI, eta1/eta0 = 1e-3) 11 vel its 5 vel its 0.683 s 0.392 s (1.74x, linear solve timed in isolation) Also here: the rotated FMG branch now drops its option keys after setup, as the KSP's own keys already were. The per-solve prefix is unique, so those keys previously accumulated in the global database once per timestep. A new test covers the failure mode that introduces — a later setFromOptions finding pc_type gone and abandoning the multigrid — across eight Newton increments. Tests: tests/test_1021_mg_option_bundle.py reads the smoother configuration back off the LIVE PCs for all three routes and asserts they agree, with the coarse difference asserted rather than tolerated (an options-database assertion would not have caught the drift, which was a key nobody wrote). Verified to fail when the pre-fix bundle is reimposed. The mesh-owned pickup is covered serially there and at np>1 in tests/parallel/test_1064, where the transfers are built cross-partition. Validation: test_1014/1015/1016/1017/1018/1020, test_0753/0835/0836/0840 pass; full "level_1 and tier_a" sweep 526 passed with 2 pre-existing failures (#470). Parallel np2 and np4: test_1017_custom_mg_parallel 6 passed, test_1064_rotated_freeslip_parallel 9 passed, test_1066_rotated_datum_parallel 2 passed. No goldens moved — GOLDEN_ANNULUS_FMG is a converged-solution quantity, so a better preconditioner reaches the same answer, and the iteration guards are upper bounds a better smoother only helps. Underworld development team with AI support from Claude Code
Adversarial reviewWe reviewed our own diff before marking this ready. Findings, most serious first. 1. The smoother we unified on costs wall clock on the standard route, at every depth we measuredThis is the weakest point of the change and it deserves the top of the list rather than a footnote. #468 asked for the native smoother bundle on the custom-P route. We measured what that costs. Outer Stokes KSP timed in isolation (setup warm), nested annulus hierarchy, linear solve, η contrast 1e6, varying only the velocity-block smoother on the live PC:
gmres/4 wins on iterations everywhere and loses on wall clock everywhere, by a margin that grows with depth — the opposite of the depth behaviour the native comment records. Decomposing at 2 levels: richardson/3 → richardson/4 is 2.382 → 2.456 s (the fourth sweep, ~3%); richardson/4 → gmres/4 is 2.456 → 3.437 s (gmres itself, ~40% per cycle). Custom-P uses Galerkin RAP coarse operators built from barycentric transfers, denser than the native nested ones, so the smoother is a larger share of the cycle here than on the route the original measurement came from. We do not think this refutes that measurement. It was taken on the Spiegelman notch with the consistent-Newton tangent — a non-symmetric operator, where richardson is stationary and degrades. The sweep above is linear and symmetric: richardson's best case. The complementary evidence is the rotated TI case in the PR body, which is anisotropic and where gmres/4 wins 11 → 5 iterations and 0.683 → 0.392 s. The gap we could not close: an attempt to measure the non-symmetric case on the standard route ( The call we took: unify on gmres/4 anyway. Richardson's failure on a non-symmetric operator is a robustness failure; gmres's cost on a symmetric one is a speed cost, and defaults err toward robust generality. The consistent-Newton tangent is the recommended setting for nonlinear rheologies, so the symmetric regime is the linear case only. But this is a defaults decision with a measured price on the linear path, and it is @lmoresi's to confirm, not ours. 2. We widened the scope past what #468 asked for#468 is about the geometric-MG bundle. We also brought the GAMG fallback into the owner, because We did not touch the 3. The rotated path now deletes its whole option bundle after setup — a new silent failure modePreviously the FMG branch deleted one key ( Deleting the rest is only safe if nothing re-reads them: a later 4.
|
…ymmetric under the consistent tangent
The comment this PR moved into utilities/multigrid_options.py carried a claim we
inherited without checking, and which the Layer 3 section of
design/nonlinear-solver-homotopy-warmstart.md states outright: that richardson
degrades because the consistent-Newton tangent makes the velocity block
NON-SYMMETRIC.
It does not. For an isotropic eta(eps_II) the Newton term is
2 (eta'/eps_II) (edot(u):edot(du)) (edot(u):edot(v))
a rank-one outer product a (x) a with a = edot(u) — symmetric under swapping du
and v. eta depends on grad v only THROUGH its symmetric part, so both factors
project onto the same tensor and nothing is left to break the symmetry.
Measured as ||A - A^T||_F / ||A||_F on the assembled velocity block
(velocity_block_symmetry.py in the rotated_pmat_audit study), against a linear
calibration of 5.1e-17, with a control on the size of the Newton contribution so
a null result cannot be vacuous:
rheology asym Newton term
linear (calibration) 5.1e-17 —
shear-thinning, isotropic 5.3e-17 2.1e-01
power-law, isotropic 5.3e-17 4.0e-01
Drucker-Prager, pressure-dependent yield 5.1e-17 5.1e-02
transverse isotropic, PICARD tangent 7.2e-02 —
transverse isotropic, Newton tangent 8.9e-02 2.0e-01
The consistent tangent contributed 5-40% of ||A|| in every nonlinear case and the
block stayed symmetric anyway. Non-symmetry appears only under transverse
isotropy — and there it appears under the PICARD tangent too, where a
frozen-coefficient form int edot(du):C:edot(v) is symmetric by construction. That
is a defect signature, not a property of the tangent: linear TI with anisotropy
fully active is symmetric at 6.3e-17. Reported on #457.
The Spiegelman-notch contraction measurement (0.75 richardson vs 0.56 gmres at
four iterations, margin growing with depth) is unaffected — it is a measurement,
not a derivation. The mechanism that fits it AND fits the cost we measured on
well-conditioned problems is operator CONDITIONING: at eta contrast 1e26 the
SOR-preconditioned spectrum is spread far enough that a stationary iteration
stalls where a Krylov smoother adapts its polynomial.
Also records the cost, which was previously unmeasured on this route: nested
annulus, LINEAR (symmetric) velocity block, eta contrast 1e6, outer KSP timed in
isolation, gmres/4 against richardson/3 — iterations x1.45 / x1.60 / x1.20 at 2 /
3 / 4 levels, wall clock x0.86 / x0.77 / x0.55. gmres wins iterations everywhere
and loses wall clock everywhere. It stays the default on both routes because the
failure it avoids is worse than the cost it carries, and it carries that cost
exactly where the problems are easy; the number is recorded so the guardrail can
be removed deliberately.
No behavioural change — comments, docstrings and docs/developer/subsystems/solvers.md.
A TODO(DESIGN) marks the design doc's Layer 3 claim rather than rewriting it.
Underworld development team with AI support from Claude Code
Follow-up: the smoother's recorded mechanism is wrong (2550d9a)@lmoresi queried the "non-symmetric" framing in finding 1 above. He was right, and it does not survive measurement. Correcting it is now part of this PR — comments and docs only, no behavioural change. The claim. Both the bundle comment we moved into On paper it is wrong. For an isotropic η(ε̇_II) the Newton term is Measured, | rheology | asymmetry | Newton term The third column is the control — a null symmetry reading is vacuous if the consistent tangent contributed nothing. It contributed 5-40% of Non-symmetry appears only under transverse isotropy, and there it appears under the Picard tangent too, where A trap worth recording: petsc4py's What this does and does not change. The Spiegelman-notch contraction measurement (ρ 0.75 richardson vs 0.56 gmres, margin growing with depth) is untouched — it is a measurement, not a derivation. Only the explanation attached to it was wrong. The mechanism that fits both it and the cost in finding 1 is operator conditioning: at η contrast 1e26 the SOR-preconditioned spectrum is spread far enough that a stationary iteration stalls where a Krylov smoother adapts its polynomial. That predicts the notch result and predicts that gmres loses on the well-conditioned problems we measured. It also dissolves the gap we flagged in finding 1. We were looking for a "standard path × non-symmetric operator" case to justify gmres there. That cell does not exist for isotropic rheologies — the standard-path block is symmetric whether the tangent is Picard or Newton. So the discriminator is not symmetry but conditioning, and the honest position is:
The design doc's Layer 3 claim is marked with a Underworld development team with AI support from Claude Code |
Scope note: this does not un-gate geometric MG for single-field solvers (#478)A parallel session flagged that geometric FMG is currently gated off. Checking what this PR does and does not close: the gate is real, it is the #276 single-field lockout, and #471 does not remove it — deliberately, as the one design question the brief asked us to settle. Measured off the live PC after setup:
Stokes is fine. Every single-field solver gets algebraic multigrid at any depth, even when geometric is requested explicitly, via We kept that out of the shared bundle owner on the grounds that it is a routing decision — which route a solver may take — not an option value, and so belongs with the route choice rather than with the settings. That reasoning still holds. But it does mean the headline "all three routes now agree" is scoped to the routes a solver is allowed to reach, and for scalar and vector solvers there is only one. What this PR does change for them: the sanctioned workaround ( Filed as #478 with a proposed fix, because #471 makes it small: Repro: Underworld development team with AI support from Claude Code |
Closes #468. Closes #467. Stacked on #465.
The problem
Three routes reach a multigrid Stokes velocity block, and they are the same preconditioner reached three ways, not alternatives:
refinement >= 1, ordinary BCsDMCreateInterpolationbetween refined DMPlex levelsset_custom_fmg, or anadapt()child's mesh-owned coarse tailrotated_bcP̂ = Q_v·Pcustom-P is mandatory wherever native cannot go — rotated BCs (the DM-coupled hierarchy cannot express a per-node rotation) and
adapt()children (no DMPlex refinement relation). So the drifted bundle was on the route that matters most.#468 — the bundle was written in two places. The native path had been moved to a measured
gmres+sorsmoother;custom_mg._configure_pcmgnever received that update, and never setmg_levels_ksp_max_itat all, so the smoother inherited whatever last wrote that prefix: 3 on the standard path (left over from the GAMG bundle) and 2 on the rotated path (PETSc's own PCMG default). The same function smoothed differently depending on what had run before it.#467 —
mesh.adapt()leaves a coarse tail on its refinement child so every solver on an adapted mesh gets geometric MG with no per-solver call. The rotated path never consulted it (the standard path's injection hook runs after the rotated dispatch has already returned), so an adapt child under rotated free-slip reportedvelocity_pc == "GAMG"— indistinguishable from having no hierarchy at all. That is theadapt-on-top-faultsworkflow's own configuration.The change
src/underworld3/utilities/multigrid_options.pyowns both bundles (geometric MG and the GAMG fallback); all three writers read from it. Two design points worth review:setFromOptionsre-reads a leftover. Deriving it from the union reproduces all three previous hand-written lists exactly — which is the check that it is the right abstraction, and it is self-maintaining when a key is added.geometric_mg_bundle(coarse="svd")), not a call-site override, so the one legitimate per-route difference is visible in the same place as everything else. The other native/custom-P asymmetry — native FMG being unusable for single-field solvers becauseDMCreateInjectionis unreliable on a refined DMPlex (FMG (preconditioner="fmg"/"auto") fails with PETSc err62 (injection) for scalar solvers on a refined Plex hierarchy #276) — deliberately stays out of the bundle: it is a routing decision, not an option value, and belongs with the route choice.custom_mg.build_transfersis now the shared "which hierarchy does this solver get?" rule for both the standard injection hook androtated_bc._build_rotated_custom_Pl, with the same opportunistic barycentric→RBF fallback and degrade-to-GAMG on failure.Also: the rotated FMG branch now drops its option keys after setup, as the KSP's own keys already did. The per-solve prefix is unique, so those keys previously accumulated in the global options database once per timestep.
Measured
Same operator, RHS and coarse solve. Two levels — where the native measurement says the gmres margin is smallest.
Reproduce:
~/+Simulations/rotated_pmat_audit/bundle_unification_gain.py(iterations, cold solve each repeat) andsmoother_drift_cost.py(the linear solve timed in isolation, setup warm).fmg_config_diff.pynow shows the three routes agreeing on every row except the deliberatecoarse_pc = svd.Tests
tests/test_1021_mg_option_bundle.pyreads the smoother configuration back off the live PETSc objects for all three routes and asserts they agree, with the coarse difference asserted rather than tolerated. An options-database assertion would not have caught the original drift — the drift was precisely a key nobody wrote. Verified to fail when the pre-fix bundle is reimposed.Also covered: the mesh-owned pickup and its negative control; the rotated FMG PC surviving eight Newton increments (the failure mode the option cleanup introduces); and the same mesh-owned pickup at np>1 in
tests/parallel/test_1064, where the transfers are built cross-partition.Validation
Serial:
test_1014,test_1015,test_1016,test_1017,test_1018,test_1020,test_0753,test_0835,test_0836,test_0840all pass. Fulllevel_1 and tier_asweep: 526 passed, 2 failed — both pre-existing and unrelated (test_0641still asserts a non-zero rotated datum raisesNotImplementedError, which #458 implemented; filed as #470).Parallel np2 and np4:
test_1017_custom_mg_parallel_mpi6 passed,test_1064_rotated_freeslip_parallel9 passed,test_1066_rotated_datum_parallel2 passed.No goldens moved. The brief anticipated re-recording; none was needed.
GOLDEN_ANNULUS_FMGis a converged-solution quantity at 1e-6 rtol, so a better preconditioner reaches the same answer, and the iteration guards (its <= 25,max(ksp_its) <= 3) are upper bounds a better smoother only helps.Underworld development team with AI support from Claude Code