fix(rotated-bc): warn on non-converged linear KSP#315
Conversation
`ksp.solve` never raises on `KSP_DIVERGED_*`, so the two linear paths in `rotated_bc` (the default fieldsplit-Schur iterative solve at `_solve_rotated_iterative` and the opt-in direct MUMPS LU at `solve_rotated_freeslip`) both returned partial answers silently. That silence is what let the 3D rotation-nullspace bug fixed by #306 look like a working solve for as long as it did: the outer Krylov hit its `ksp_max_it` ceiling (300 in the pre-#306 code) with a residual well above tolerance, and neither the KSP nor the caller flagged it. The solution then propagated into downstream diagnostics that eventually exposed it as wrong physics rather than as a solver failure. Add a small `_warn_if_ksp_diverged` helper and call it immediately after each of the two `ksp.solve` sites. Rank-0 warning only (via `uw.mpi.pprint`), same style as the existing nonlinear-driver warning at the end of `solve_rotated_freeslip_nonlinear`. No behaviour change on the happy path; non-convergence is now loud instead of silent. test_1018_rotated_freeslip.py: 14/14 pass (2D behaviour unchanged). Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
Adds a small safety guard to the rotated free-slip linear solves so that PETSc KSP divergences (which ksp.solve() does not raise) are no longer silent, improving diagnosability of partial / non-converged rotated-frame solutions.
Changes:
- Introduces
_warn_if_ksp_diverged(ksp, kind)to emit a rank-0 warning when KSP finishes with a non-converged reason. - Calls the helper after both
ksp.solve()sites (direct LU path and iterative fieldsplit-Schur path).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| reason = int(ksp.getConvergedReason()) | ||
| if reason >= 0: | ||
| return |
…erged Copilot review on #315 noted the convention split: * KSP.getConvergedReason() > 0 → converged * KSP.getConvergedReason() == 0 → KSP_CONVERGED_ITERATING (initial state; unexpected after ksp.solve() returns) * KSP.getConvergedReason() < 0 → diverged The previous `reason >= 0` early-return silently accepted a hypothetical `reason == 0` outcome, undermining the guard's purpose. Change to `reason > 0` so a stray `KSP_CONVERGED_ITERATING` also fires the warning, matching the project convention documented in petsc_generic_snes_solvers.pyx:2979. test_1018_rotated_freeslip.py: 14/14 pass (unchanged). Underworld development team with AI support from Claude Code
|
Thanks @copilot-pull-request-reviewer — good catch on the reason==0 case. Applied in 89cf405: |
…tion (#291) (#318) * fix(rotated-bc): warn on non-converged linear KSP `ksp.solve` never raises on `KSP_DIVERGED_*`, so the two linear paths in `rotated_bc` (the default fieldsplit-Schur iterative solve at `_solve_rotated_iterative` and the opt-in direct MUMPS LU at `solve_rotated_freeslip`) both returned partial answers silently. That silence is what let the 3D rotation-nullspace bug fixed by #306 look like a working solve for as long as it did: the outer Krylov hit its `ksp_max_it` ceiling (300 in the pre-#306 code) with a residual well above tolerance, and neither the KSP nor the caller flagged it. The solution then propagated into downstream diagnostics that eventually exposed it as wrong physics rather than as a solver failure. Add a small `_warn_if_ksp_diverged` helper and call it immediately after each of the two `ksp.solve` sites. Rank-0 warning only (via `uw.mpi.pprint`), same style as the existing nonlinear-driver warning at the end of `solve_rotated_freeslip_nonlinear`. No behaviour change on the happy path; non-convergence is now loud instead of silent. test_1018_rotated_freeslip.py: 14/14 pass (2D behaviour unchanged). Underworld development team with AI support from Claude Code * review(Copilot): treat KSP_CONVERGED_ITERATING (reason=0) as non-converged Copilot review on #315 noted the convention split: * KSP.getConvergedReason() > 0 → converged * KSP.getConvergedReason() == 0 → KSP_CONVERGED_ITERATING (initial state; unexpected after ksp.solve() returns) * KSP.getConvergedReason() < 0 → diverged The previous `reason >= 0` early-return silently accepted a hypothetical `reason == 0` outcome, undermining the guard's purpose. Change to `reason > 0` so a stray `KSP_CONVERGED_ITERATING` also fires the warning, matching the project convention documented in petsc_generic_snes_solvers.pyx:2979. test_1018_rotated_freeslip.py: 14/14 pass (unchanged). Underworld development team with AI support from Claude Code * fix(constrained): guard empty stratum IS in interior-multiplier reduction (#291) `Stokes_Constrained` segfaulted at np>1 in `_constrain_interior_multipliers_in_section`: on a rank owning zero points with a given boundary label value, `dm.getLabel("UW_Boundaries").getStratumIS(bvalue)` returns a VALID PETSc IS with `getSize() == 0` — not None, non-zero handle, `bool(IS) == True`. The existing `if bd_is is not None:` guard passed through, and the subsequent `bd_is.getIndices()` crashed with SIGSEGV in the PETSc IS binding. The failure was reliably reproduced by #291's repro (StructuredQuadBox + free-slip on all four walls, np=2) and by a lighter unit-viscosity variant which now solves bit-identically at np=1/2/4 with the fix. Fix: replace `if bd_is is not None:` with `if bd_is and bd_is.getSize() > 0:`. `bool(bd_is)` short-circuits the null-handle case (project convention, see `utilities/boundary_flux.py:70`); `.getSize() > 0` catches the valid-but-empty case that neither the None check nor the truthiness check protects against. ### Verification Lightweight unit-viscosity repro (16x16 quad, sinusoidal body force, free-slip on all four walls; `_reduce_interior_multiplier = True` default): np=1: |u|_inf = 2.533e-02 (6.6 s) np=2: |u|_inf = 2.533e-02 (8.5 s) [previously: SIGSEGV] np=4: |u|_inf = 2.533e-02 (5.0 s) [previously: SIGSEGV] Bit-identical parallel-vs-serial in this regime. `tests/test_1062_constrained_solcx.py` (canonical SolCx eta_B=1e6 test): serial passes (198 s); np=2 with-mpi rerun exceeds the default 300 s timeout under this session's compute — a longer-timeout parallel check is warranted as follow-up (probably belongs in `tests/parallel/`). Underworld development team with AI support from Claude Code * test(#291): add lightweight np≥2 regression + un-skip custom-FMG parallel test Copilot review on #318 rightly pointed out that a parallel-only SIGSEGV regression should not merge without a re-enabled parallel test. Two changes: ## 1. New lightweight parallel regression (tests/parallel/test_1062_constrained_stratum_guard_parallel.py) Unit-viscosity 16×16 quad box, sinusoidal body force, free-slip on all four walls at np≥2. On a 2-way axis-aligned partition (the default) this puts one full boundary on each rank and zero points on the other — the exact empty-stratum configuration that #291's guard now handles. Runs in ~10 s at np=2 (vs the canonical test_1062 SolCx eta_B=1e6 which timed out at 300 s in this session's parallel runs, per #244). Asserts `|u|_inf` matches the serial GOLDEN of 2.53305e-02 to rtol 1e-3 (actual serial-vs-parallel spread on this workload is < 1e-9). ## 2. Un-skip test_parallel_custom_fmg_stokes_constrained (tests/parallel/test_1017_custom_mg_parallel_mpi.py) Its skip reason explicitly cited "#291; auto-enables once #291 is fixed." The guard fix is now in place, so the test can run. Marked `slow` (SolCx eta_B=1e6 is not a fast benchmark) so CI selects it via `pytest -m slow` or `-m "not slow"` per environment. Underworld development team with AI support from Claude Code
|
Closing as superseded: the entire change — Underworld development team with AI support from Claude Code |
Summary
ksp.solvenever raises onKSP_DIVERGED_*, so the two linear paths insrc/underworld3/utilities/rotated_bc.py(the default fieldsplit-Schur iterativesolve in
_solve_rotated_iterativeand the opt-in direct MUMPS LU insolve_rotated_freeslip) both returned partial answers silently.That silence is what let the 3D rotation-nullspace bug fixed by #306 look
like a working solve for as long as it did: on the 3D Zhong
SphericalShellInternalBoundarycase (test_1064setup) the outer Krylovhit its `ksp_max_it` ceiling (300 in the pre-#306 code) with a residual
around 2.7e-7 (target rtol was 1e-7) while producing rotated-frame
solutions that the caller then rotated back and returned. Diagnosed under
the /remote-control session that surfaced #306.
Change
Add a small
_warn_if_ksp_diverged(ksp, kind)helper at the top of themodule and call it immediately after each of the two `ksp.solve` sites.
Rank-0 warning only (via `uw.mpi.pprint`), same style as the existing
nonlinear-driver warning at the tail of `solve_rotated_freeslip_nonlinear`.
Impact
KSP finished on a negative converged-reason.
rank-0 warning with the reason code, iteration count, and final
residual, so a caller inspecting stdout can see it.
Tests
`tests/test_1018_rotated_freeslip.py`: 14/14 pass (13 linear + 1
nonlinear-guard, unchanged from post-#306 tip). No new tests here — the
guard fires only on divergence, and forcing a divergence in-test would
require patching in a deliberately-broken preconditioner.
Related
codebase, but this PR is scoped to `rotated_bc` only.
Underworld development team with AI support from Claude Code