Rotated free-slip: the custom-FMG velocity sub-KSP must converge, not just apply - #465
Rotated free-slip: the custom-FMG velocity sub-KSP must converge, not just apply#465lmoresi wants to merge 12 commits into
Conversation
…rotated path (#438) - v_n = u_n now carried through the manual Newton/Picard loop: accepted iterates are kept feasible (affine snap in _impose_normal_constraint), so increments stay homogeneous (n.delta = 0) and the datum never touches the tangent, the increment RHS, the custom-FMG prolongation or the nullspace handling. - A COLD start imposes the datum through the FIRST increment's affine lift (zeroRowsColumns x/b at the rest-state tangent, exactly the linear one-shot's treatment): snapping the zero state onto the datum manufactures a boundary-strip strain state whose shear-thinning tangent cannot descend (measured: first Newton step 4 orders too large, no line-searched alpha improves). Warm starts take the exact affine snap directly. - Collective datum-activity flag in BOTH solve paths: branching on the rank-local datum_map desyncs the ranks' collective sequences at np>1 (the two zeroRowsColumns variants scatter differently; the lift skips the line-search collectives). The linear path carried the same latent pattern, tolerated only because annulus partitions happen to give every rank a piece of the outer arc. - Regression: nonlinear power-law annulus with a u.n = cos(theta) datum imposed to machine precision through genuine Newton iteration (test_1018). Tests: test_1018 serial 18/18; test_1066 np2; test_1070 serial + np2 (7); test_1071 serial. Underworld development team with AI support from Claude Code
…t bare MPI The rank-consistent datum-activity flag now rides the x-hat lift vector's collective norm (PETSc-before-MPI house rule): x-hat carries the prescribed rotated-frame datum for the zeroRowsColumns lift anyway, so one object does both jobs and the hand-rolled ownership loop collapses into _set_rows_local. Removes the mpi4py allreduce introduced in the previous commit. The deeper cure is a surface submesh (the 1D-manifold gap, #202 lineage): a boundary-field datum would make this flag a field norm and retire the hand-rolled surface bookkeeping in this layer - workstream C. Underworld development team with AI support from Claude Code
…d solves; datum via conds (#438, #403) One constraint, one path: the linear one-shot is deleted and every rotated solve runs the manual Newton/Picard loop. A linear model converges after its first increment (the cold-start affine lift IS the linear solve), so the up-front _residual_is_nonlinear probe leaves the dispatch entirely - two Jacobian assemblies saved per linear solve (test_1018: 60s -> 55s). The probe survives only as a lazy guard in the picard + pure-Newton corner, where a linear model now ignores the meaningless warmup instead of raising. - solver._rotated_use_lu becomes an increment-solver choice (direct LU per Newton increment, serial PC-free diagnostic; naive pressure pin extracted to _naive_pressure_pin with its TODO(BUG)). Verified against the iterative path to 6e-10. - One result-dict shape: reaction = F(u) always (equals A.u - b exactly for linear residuals); the linear-only A/b keys and their consumer fallback in boundary_normal_traction retire. ksp_its is always the per-increment list (test_1018 3D assertion moved to max-per-increment). - add_rotated_freeslip_bc(conds, ...) accepts the non-zero wall-normal datum (number or scalar expression, value-first; #403 item 2) and stores it in _rotated_freeslip_datum; vector values rejected. Tests and FreeSurface now use the public argument instead of poking the attribute. - FreeSurface: the penalty-fallback NotImplementedError catch in _solve_consistent is gone - consistent_constraint="strong" now runs through the SNES rotated path for nonlinear rheologies. Linear solutions change at solver-tolerance level only (agreed relaxation of bit-preservation); an occasional cheap polish increment can follow the lift when the first residual check sits at the tolerance boundary (free-surface plume wall-time unchanged). Tests: test_1018 serial 18/18; test_1066 np2; test_1070 serial + np2 (7); test_1071 serial; LU-vs-iterative and lazy-probe smokes. Underworld development team with AI support from Claude Code
…ll modes Two defects surfaced by the TI fault smoke (#438 acceptance): 1. The step-norm exit reported converged=True on tiny increments while the residual was still O(1) (a stiff power-law tangent at the regularisation floor produces tiny steps far from the solution). The exit is now VERIFIED against the problem's intrinsic scale ||F(0)||: a warm start at the solution passes; a stagnating crawl goes back through the line search and either progresses (bounded by max_it) or ends on the stall exit honestly. 2. _mode_satisfies_constraints admitted rigid-rotation modes that are PINNED by an essential BC on another boundary (it only checked the rotated constraint rows). The admitted mode was projected out of every increment RHS, leaving an irreducible residual component: Newton converged superlinearly to rel ~2e-5 and floored there (measured on the essential-inner + rotated-outer power-law annulus). Modes are now also verified as null vectors of the ASSEMBLED operator (||J.m|| against the velocity diagonal scale), which catches any pinning without enumerating BC types; the null space is built after the first Jacobian assembly to make that possible. Same fix protects the post-solve rotation-gauge removal. With the fix the same solve converges to rel 2e-13 in 11 iterations and correctly leaves the gauge in place. TI fault-bearing smoke (power-law background, weak-plane band, cos(theta) datum) now passes: 24 its to rel 6e-8, datum at the quadrature floor, sigma_nn recovery clean. Tests: test_1018 serial 18/18 (incl. gauge-removal cases), test_1066 np2 (reference unchanged - the linear-case imprint of the spurious projection is below the 1e-6 test tolerance), test_1070 serial (7) + test_1071. Underworld development team with AI support from Claude Code
…scale convergence for warm starts - FreeSurface._solve_consistent now warm-starts from the free solve's fields: the consistent solution is the free solution with a small material-boundary datum correction, and the free solve has already converged this step. Starting there keeps a power-law tangent at physical strain rates - a cold start puts it at the regularisation floor, where the Newton line search stalls at O(0.1) relative residual (measured, acceptance attempt 2). With the warm start the first acceptance step converges in 3 iterations. - The rotated loop's convergence reference is now max(r0, ||F(0)||): rtol relative to a warm start's own (small) initial residual demands ever-more absolute accuracy the better the guess (the warm-start rtol trap). The rest-state residual is the intrinsic forcing scale; cold starts unchanged (their r0 IS that scale). The step-norm verification and the non-convergence warning read the same reference. Acceptance (#438) evidence in ~/+Simulations/FreeSurface/annulus_fs_convection/b_snes_datum_acceptance/: power-law annulus FS convection with consistent_constraint="strong" through the SNES rotated path holds the material boundary at the strong-datum level (err 5.3e-3 vs penalty 3.7e-2; net flux 1.4e-4 vs 2.0e-3) at matched steps; TI fault-bearing smoke passes (24 its to rel 6e-8). Open finding recorded there: deformed-ring datum compatibility floors the interior residual at rel ~2e-3 (honestly reported unconverged; suspected arc-length-vs-FE-quadrature demean gap - the principled fix is a trace-mass-weighted demean). Tests: test_1018 18/18 serial; test_1070 serial+np2 (7); test_1071; test_1066 np2. Underworld development team with AI support from Claude Code
…ith the PETSc Jacobian checker Regroup note (#438 B-session): TI power-law under consistent_jacobian=True shows an early trajectory identical to the frozen tangent and Picard-paced convergence (24 its vs ~11 isotropic) - consistent with the dC/d(eps_II) director-orientation terms being dropped or mis-oriented in the JIT-lowered tangent, while the isotropic terms survive (isotropic Newton is superlinear through the same rotated machinery, which also argues the rotation algebra itself is transparent). Discriminating harness (native -snes_test_jacobian vs rotated directional-derivative probe, with an isotropic control) is staged in the #438 acceptance run directory; not yet run - joint item. Underworld development team with AI support from Claude Code
#457 The PETSc Jacobian checker (bounded-curvature protocol; at 1e-12 regularisation the FD reference is invalid for power-law and every model reads the same large mismatch) convicts the TI anisotropy tangent on the NATIVE path: ||J-Jfd||/||J|| ~ 2e-3-5e-3 in developed flow vs an FD-limited 4e-6 isotropic control. The rotated machinery is exonerated. Evidence and harness recorded in #457. Underworld development team with AI support from Claude Code
…inear datum test - docs/developer/subsystems/rotated-freeslip.md: the unified rotated path - constraint semantics, datum-through-Newton design (feasible iterates, cold-start lift), convergence reference scale, operator-verified null modes, parallel rules, reaction/sigma_nn hand-off. Linked from the developer index. - Changelog entry for the #438/#403 work at the conceptual level. - tests/parallel/test_1066: nonlinear (power-law) rotated datum at np>=2 - guards the collective-sequence class through the Newton loop (lift, line-search residuals, mode verification). Passes np2 and np4. Underworld development team with AI support from Claude Code
… refusal Conflict in the rotated free-slip dispatch only: development added the armed wall-clock-guard refusal (#442) around the old two-path dispatch; this branch had unified the dispatch into one Newton loop. Resolution keeps BOTH: the guard re-check (an armed guard would be silently inert on the rotated path, which runs its own Krylov loop outside self.snes) followed by the single unified call. Development's _record_convergence_status hook after the rotated report (#441 warm-start flag) auto-merged and stands. Underworld development team with AI support from Claude Code
…ust apply
The rotated free-slip path drove its custom-FMG velocity block with `preonly`
("a full-MG cycle per Schur application, by design"). PCFieldSplit forms the
Schur complement S = A11 - A10 A00^-1 A01 and applies A00^-1 through that same
velocity KSP, so `preonly` replaces A00^-1 with a single multigrid cycle and
hands the pressure Krylov a different system S~ != S, preconditioned by a 1/mu
mass matrix built for S.
Measured (annulus, weak plane reaching the constrained boundary,
eta_1/eta_0 = 1e-3, transversely isotropic): under `preonly` the pressure
residual falls 4.4e4 in ~16 iterations then STAGNATES at a floor ~3.1e-7 and
burns the remaining 184 iterations of its cap for nothing, every outer
iteration -- 9 outer iterations, 2.77 s. Under FGMRES it converges 1.1e8
monotonically in 17 pressure iterations -- 1 outer, 0.67 s (4.1x). The isotropic
control moves the same way (5 -> 1), so this is the Schur application and not the
anisotropy. SolCx box + 2-level barycentric hierarchy (the test_1018 FMG
configuration): 6 outer -> 1.
The Schur application is bitwise reproducible under both settings (measured), so
the floor is NOT "the operator changes between applications" -- S~ is a fixed
linear operator, just the wrong one. The precise origin of the floor is not
isolated.
Wrap the FMG in FGMRES: max_it 200 matches the GAMG fallback and the native path,
rtol 0.1 x tol matches the GAMG fallback (the native path asks 0.033 x tol).
Found while auditing whether the rotated path preconditions the ROTATED operator.
It does: an algebraic identity probe against independently formed references
(block the native Jacobian, then rotate -- the opposite order to the solver) shows
the velocity sub-KSP operator, the velocity sub-PC operator and every Galerkin
multigrid level operator match the rotated reference exactly, while differing
from the un-rotated reference by 7e-2 to 2.3e-1. The Schur preconditioner
correctly comes un-rotated from the native Pmat: Q is the identity on pressure,
and the p-p block of the rotated operator is identically zero. No un-rotated
matrix reaches the preconditioner.
Also:
- warn when the velocity sub-KSP exhausts its cap. It returns KSP_DIVERGED_ITS,
which KSPCheckSolve deliberately does not escalate, so FGMRES can degrade
silently where `preonly` could not fail.
- report `velocity_pc`, `schur_pre`, `velocity_pc_type` (PETSc's own view of the
sub-PC) and `vel_its_last` / `pres_its_last` in `_rotated_freeslip_info`. The
outer count alone hides a degraded inner solve, which is how this went
unnoticed. The `_last` names are deliberate: these are last-application
samples, not the summed work axis solver_health uses -- wiring the rotated path
into SolverInstrumentation.sub_reports() is the proper fix and is not done here.
- guard the FMG configuration in test_1018 against a return to `preonly`, and
assert the velocity sub-PC really is multigrid rather than restating our own
bookkeeping.
- fix a pre-existing failure in test_1064: `int(info["ksp_its"])` on a value the
unified rotated loop made a list, so the 3D spherical partition-independence
test errored at np>1 before reaching its assertions.
Tests: test_1018 18 passed; test_1064 + test_1066 10 passed at np2 and np4;
TI fault smoke passes (24 nonlinear its, datum relL2 4.4e-05).
Underworld development team with AI support from Claude Code
d9f7651 to
4a557b8
Compare
Adversarial reviewFindings against the first push ( 1. The stated mechanism was inverted — accepted, correctedWe wrote that Measured directly: applying the Schur operator twice to the same vector gives What the residual history actually shows (pressure sub-KSP monitor, TI annulus): So it is stagnation at a floor, not slow convergence and not non-convergence against a moving operator. 2. FGMRES can now fail silently where
|
| velocity sub-KSP | outer | best (s) | median (s) |
|---|---|---|---|
preonly |
9 | 2.767 | 2.968 |
fgmres 0.1×tol |
1 | 0.672 | 0.705 |
4.1x. The iteration-count story holds.
8. max(info["ksp_its"]) on an empty list — accepted as latent, not fixed
A zero-increment exit (warm start already converged) leaves ksp_its == [] and max raises. Not reachable from these cold-start tests, and the same pattern pre-dates this PR at test_1018:101. Left alone rather than widened here.
9. max(...) vs sum(...) in test_1064 — accepted, left as is
_capture_rotated_report sums; our one-line fix uses max, matching the pre-existing test_1018:101. It weakens its <= 25 from a total to a per-increment bound, with no practical difference for this ksponly single-increment case. Consistency across the two test files was the deciding factor; worth revisiting if ksp_its semantics are ever settled properly.
10. GOLDEN_ANNULUS_FMG sits on the changed path and was not re-recorded — checked
Correct that it is the only golden on this path. It passes unchanged at np2 and np4, which is the check; we did not re-record it.
11. Options-database growth — real, pre-existing, not ours to fix here
_configure_pcmg writes ~7 keys under the unique-per-solve rotfsN_fieldsplit_vel_ prefix and only mg_coarse_pc_type is removed, so the database grows under time-stepping. The "stays clean (and bounded under time-stepping)" comment in the block we touched is therefore false as written. Pre-existing and outside this change; flagging rather than fixing.
Not accepted, for the record: nothing. Every finding above was either a real defect or a real overclaim.
Underworld development team with AI support from Claude Code
…ent the Krylov axes
The rotated path was looser than the native Stokes path on both fieldsplit
sub-solves, with no recorded reason:
rotated (was) native
velocity rtol 0.1 x tol 0.033 x tol
pressure rtol 1.0 x tol 0.1 x tol
House policy is that defaults err on the side of robust generality, with callers
loosening guardrails at their own risk. Matching native costs ~17% wall clock with
IDENTICAL outer iteration counts, measured on both velocity-block routes (custom
FMG and the GAMG fallback) and both rheologies (isotropic and transversely
isotropic). It also drops the TI fault smoke from 24 to 15 nonlinear iterations —
tighter inner solves feed a cleaner Newton step.
A too-loose inner solve does not degrade gracefully: it walks back toward the
`preonly` failure this branch already fixed (velocity rtol 1e-1 -> 6 outer
iterations, 1e-2 -> 4, 0.033 x tol -> 1). Cheaper remains available to anyone who
measures their own configuration; it is not the default.
Also documents the three axes that keep getting argued as one, in the solvers
subsystem page (which listed "preconditioner selection missing" as a known gap):
1. Flexible vs non-flexible Krylov. The question is whether the PRECONDITIONER is
stationary, not whether the operator is symmetric — the velocity block is SPD,
so cg is admissible on symmetry grounds and still fails, because GAMG with
mg_levels_ksp_converged_maxits is a non-linear application. Settled by #147.
2. `preonly` vs an iterative wrapper. Not a Krylov-taste question but a positional
one: a preconditioner application may be `preonly`, an operator inverse
underneath a Schur complement may not, because it defines the system the
pressure Krylov solves rather than merely conditioning it.
3. Inner tolerance. The genuine judgment call, decided here by the guardrail
policy.
Plus how to detect a degraded sub-solve: the outer count hides it, an exhausted cap
returns KSP_DIVERGED_ITS which KSPCheckSolve does not escalate, and
KSPGetIterationNumber on a sub-KSP is a last-application sample rather than work.
Tests: test_1018 18 passed; test_1064 + test_1066 10 passed at np2 and np4; TI
fault smoke passes (15 nonlinear its, datum relL2 4.4e-05).
Underworld development team with AI support from Claude Code
Follow-up: sub-solve tolerances brought to native parity, and the Krylov axes written downLouis's steer: defaults should err on the side of robust generality, with callers removing guardrails at their own risk. Applying that here found two more places where the rotated path was the looser of two paths solving the same problem, with no recorded reason:
Measured cost of matching native, timed sequentially in-process, three repeats:
~17–30%, with identical outer iteration counts in every case. Adopted, because a too-loose inner solve does not degrade gracefully — it walks back toward the Unexpected bonus: the TI fault smoke dropped from 24 → 15 nonlinear iterations. Tighter inner solves feed a cleaner Newton step. Worth noting against #457 — some of that 24 was loose inner solves rather than the tangent alone, though the remaining Picard-pace behaviour is still consistent with the TI tangent defect. Cheaper remains available to anyone who measures their own configuration; it is no longer the default. Documentation
Plus how to detect a degraded sub-solve, since all three failures are quiet: the outer count hides them, an exhausted cap returns Tests: Underworld development team with AI support from Claude Code |
…ov are one design Provenance and framing from Louis. The Stokes configuration descends from the Citcom solver of Moresi & Solomatov (1995), whose central choice is that the inner solves are DELIBERATELY inexact. Two consequences follow and must hold together: 1. Inexact inner solves perturb the search directions, so the outer/Schur Krylov must be FLEXIBLE (fgmres or similar) — a non-flexible method's residual recurrence is invalidated by a search direction that drifts. 2. Inexact is not unbounded: an inner solve must still converge WELL BELOW the tolerance demanded of the outer solve. The 0.033 and 0.1 factors are that margin — a safety margin, not a tuned constant. So what looked like two independent axes (flexible-or-not, how-tight) is one decision: flexibility buys tolerance of inexactness, the margin bounds how inexact. Arguing them separately is why the question never settled. `preonly` is then the degenerate case — no tolerance at all, so no margin for the flexible outer Krylov to work with, which is exactly the stagnation floor measured on this branch. This also sharpens the previous commit's justification. The rotated path ran its outer KSP at rtol = tol and its pressure sub-solve ALSO at rtol = tol: the inner solve permitted to be no better than the answer it feeds. That is the invariant broken outright, not a tuning inconsistency with the native path. Recorded in the solvers subsystem page and at the point in rotated_bc.py where the margin is set. The size of the factors is noted as inherited convention with no derivation recorded beyond "well below the outer tolerance". Tests: test_1018 18 passed. Underworld development team with AI support from Claude Code
Stacked on #458 (
feature/rotated-datum-snes). Retarget todevelopmentwhen that merges — do not delete the base branch first, it would close this PR.Why
Louis asked whether the rotated free-slip BC, which rotates the matrices after they are assembled, does that successfully for the preconditioner matrix as well — the concern being that an error there would be invisible under isotropy (a viscous operator is nearly rotation-invariant) but not under anisotropy, and the free-surface + transverse-isotropy fault model had flagged something.
The audit answer: the preconditioner is fully rotated
An algebraic identity probe reaches into the live PC after
PCSetUpand compares every matrix it holds against two independently-formed references:d_rot(the rotated block, built by the opposite route to the solver — block the native Jacobian, then rotate) andd_unrot(the same block un-rotated, which serves as the negative control).Identical for the isotropic control. So:
ksp.setOperators(Ahat)is single-argument, so the velocity preconditioner is the rotated operator.Qis the identity on pressure, and the p-p block of the rotated operator is identically zero, so it must come from the native Pmat. UW3's Pmat differs from its Amat only in that p-p block (the velocity and coupling blocks are registered with identical pointwise functions,petsc_generic_snes_solvers.pyx:7803-7809), so discarding the rest costs nothing.No un-rotated matrix reaches the preconditioner. The original hypothesis is not the defect.
The defect that is there
The custom-FMG velocity block was driven by
preonly— "a full-MG cycle per Schur application, by design". ButPCFieldSplitformsS = A11 - A10 A00^-1 A01and appliesA00^-1through that same velocity KSP, sopreonlyreplacesA00^-1with a single multigrid cycle and hands the pressure Krylov a different systemS~ != S, preconditioned by a 1/mu mass matrix built forS.Pressure sub-KSP residual history (TI annulus, weak plane reaching the constrained boundary, eta_1/eta_0 = 1e-3):
It stagnates at a floor, on every outer iteration. Not slow convergence, and not a moving operator: applying the Schur operator twice to the same vector is bitwise identical under both settings.
S~is a fixed linear operator, just the wrong one. Why the floor sits at 3.1e-7 is not isolated here (a range/null-space inconsistency betweenS~and the constant-pressure null space attached toSis the obvious suspect).Cost, timed sequentially in-process, three repeats:
preonly(shipped)fgmres 0.1 x tol4.1x. The isotropic control moves the same way (5 -> 1 outer), so this is the Schur application rather than the anisotropy — the anisotropy only makes it hurt more. On the
test_1018FMG configuration (SolCx box, 2-level barycentric hierarchy): 6 outer -> 1.Is this a general problem with our FMG defaults?
No — the rotated custom-FMG branch was the sole exception. Verified at runtime on the native path with the identical hierarchy: velocity sub-KSP is
fgmreswith PCmg, outer 1 / vel 6 / pres 68. The native path sets that KSP type explicitly andcustom_mg._configure_pcmgonly replaces the PC; the rotated GAMG fallback was already FGMRES.The transferable rule, now in
docs/developer/subsystems/rotated-freeslip.md: a velocity sub-KSP that ispreonlyunderpc_fieldsplit_schur_fact_type=fullgives the pressure Krylov the wrong system to solve. Multigrid is fine as the preconditioner; it cannot be the whole solve when a Schur complement is applied through it.Changes
max_it 200matches the GAMG fallback and the native path;rtol 0.1 x tolmatches the GAMG fallback (the native path asks0.033 x tol).KSP_DIVERGED_ITS, whichKSPCheckSolvedeliberately does not escalate, so FGMRES can degrade silently wherepreonlycould not fail.velocity_pc,schur_pre,velocity_pc_type(PETSc's own view of the sub-PC) andvel_its_last/pres_its_lastin_rotated_freeslip_info. The outer count alone hides a degraded inner solve — a full Schur factorisation with a good pressure mass still reports ~1 outer iteration while the inner solve grinds underneath. That is how this went unnoticed. The_lastnames are deliberate: these are last-application samples, not the summed work axissolver_healthuses. Wiring the rotated path intoSolverInstrumentation.sub_reports()is the proper fix and is not done here.test_1018FMG configuration against a return topreonly, and assert the velocity sub-PC really is multigrid rather than restating our own bookkeeping.test_1064failure:int(info["ksp_its"])on a value the unified rotated loop of Rotated free-slip: prescribed-normal datum through the SNES path; one unified Newton loop (#438, #403) #458 made a list, so the 3D spherical partition-independence test errored at np>1 before reaching any of its assertions.Also measured, no change made
The constraint-row diagonal (
zeroRowsColumns(normal_rows, diag=s)) is a free conditioning parameter — any positivesis exact. Before the fix it was a real lever on the GAMG route (37 velocity iterations ats <= 1e-2 x mean, 44 at the shipped mean, 142 at1e4 x mean), and the isotropic control had the same shape, so it was general rather than an anisotropy defect. After the fix, on the FMG route it is completely inert: 11 velocity / 17 pressure iterations at every factor from 1e-4 to 1e4. The sensitivity was a symptom of the degraded configuration, so nothing was changed here.Tests
test_101818 passed;test_1064+test_106610 passed at np2 and np4; the TI fault smoke passes (24 nonlinear iterations, datum relL2 4.4e-05).Its 24 iterations are #457 (the TI consistent tangent is wrong when anisotropy is active), a native-path defect that reproduces with no rotation involved. It is not a preconditioner problem and should not be confounded with one.
Full study, scripts and numbers:
~/+Simulations/rotated_pmat_audit/README.md. Adversarial review in the comment below.Filed separately while auditing: #463 (TI-VEP
.viscositydiscards the yield-limited eta_1), #464 (block-constrained Stokes + rotated free-slip is unguarded).Underworld development team with AI support from Claude Code