fix(solve-report): SNES converged-reason table is off by one against PETSc - #444
Conversation
… PETSc Every positive SNES reason was shifted by one slot, and the table claimed a code 1 that PETSc does not define: reason 2 PETSc CONVERGED_FNORM_ABS reported as CONVERGED_FNORM_RELATIVE reason 3 PETSc CONVERGED_FNORM_RELATIVE reported as CONVERGED_SNORM_RELATIVE reason 4 PETSc CONVERGED_SNORM_RELATIVE reported as CONVERGED_ITS reason 5 PETSc CONVERGED_ITS reported as UNKNOWN_5 The direction matters. A solve that stopped on the STEP norm -- the weakest criterion, and what a stalled viscoplastic solve reports -- was labelled CONVERGED_ITS, while a genuine residual convergence was labelled CONVERGED_SNORM_RELATIVE. Reading a difficulty report is how a continuation driver decides whether a parameter station is reachable, so a mislabelled convergence is the kind of error that quietly puts false rescues on a regime map. Found while checking why a hard plastic station was reporting CONVERGED_ITS. Also adds DIVERGED_OBJECTIVE_DOMAIN (-13) and DIVERGED_OBJECTIVE_NANORINF (-14), which were unmapped and surfaced as UNKNOWN_n, and corrects -4 to its PETSc name DIVERGED_FUNCTION_NANORINF. The table is hand-written on purpose -- the module must import without the Cython extension -- so it can drift, and the existing test pinned it to ITSELF (reason_string(2) == 'CONVERGED_FNORM_RELATIVE') rather than to the enum, which is how this survived. The KSP table next to it WAS checked against petsc4py and was correct. test_snes_reason_table_matches_petsc now checks both directions: every label matches the enum, and every reason PETSc can return is mapped. Underworld development team with AI support from Claude Code
…y alias A PETSc reason code can carry more than one petsc4py spelling: 0 is both CONVERGED_ITERATING and ITERATING. The new table test kept one name per code via setdefault, so it asserted against whichever name vars() happened to yield first. That is not a petsc4py guarantee, and a build that enumerated the aliases the other way would have failed a level_1/tier_a test on a purely cosmetic difference. Collect the alias set per code and assert membership instead. The existing KSP test sidesteps the same hazard by skipping code 0; this covers it rather than skipping it. Underworld development team with AI support from Claude Code
Adversarial reviewReviewed against the standing rule that a regression test must be shown to fail without its fix, and that a table pinned to itself proves nothing. Verified: the bug is real and the fix is rightI checked the pre-fix table against
So The consequence stated in the commit message checks out: code 3 is a genuine residual convergence and was being reported as Finding (fixed in this PR)
Worth noting the existing Note for anyone reading older logsAny Not coveredThe table is still a hand-copy that only the test keeps aligned; nothing prevents the same drift in the compact map in Underworld development team with AI support from Claude Code |
The table was duplicated: solve_report.REASON_STRINGS (fixed in the previous commit) and SolverBaseClass._convergence_reasons, which adds a one-line gloss for get_convergence_diagnostics and supplies the name _warn_on_divergence prints. Only the first copy was corrected, which left the two disagreeing about what code 2 means -- worse than one consistently wrong table. Same shift here: there is no code 1, code 4 is the step-norm stop rather than CONVERGED_ITS, and -4 is DIVERGED_FUNCTION_NANORINF. The two objective-function divergence codes were missing, so those solves printed UNKNOWN(-13)/UNKNOWN(-14). test_1055 now pins BOTH copies to petsc4py's enum and to each other, so neither can drift alone. Verified the added assertions fail against a build carrying the old solver table. Underworld development team with AI support from Claude Code
Follow-up found and fixed in this PR: the table was duplicated, and only one copy was correctedMy own review note above ("nothing prevents the same drift in the compact map in 1: ("CONVERGED_FNORM_ABS", "||F|| < atol"), # no such SNES code
2: ("CONVERGED_FNORM_RELATIVE", ...), # actually FNORM_ABS
4: ("CONVERGED_ITS", "Maximum iterations reached"), # actually the step-norm stop
-4: ("DIVERGED_FNORM_NAN", ...), # actually FUNCTION_NANORINFIt also had no entries for -13/-14, so an objective-domain or objective-NaN divergence printed This matters because it is not a dead table. It feeds
Verified it fails without the fix, against a real build rather than by inspection: running the updated test against an environment carrying the old solver table gives The good news for Left deliberately unfixed: the two copies are still two copies. Collapsing them (the solver deriving names from Underworld development team with AI support from Claude Code |
The bug
solve_report's SNES converged-reason table is shifted by one against PETSc, and claims acode
1that PETSc does not define:CONVERGED_FNORM_ABSCONVERGED_FNORM_RELATIVECONVERGED_FNORM_RELATIVECONVERGED_SNORM_RELATIVECONVERGED_SNORM_RELATIVECONVERGED_ITSCONVERGED_ITSUNKNOWN_5Why the direction matters
A solve that stopped on the step norm — the weakest criterion, and what a stalled
viscoplastic solve reports — was labelled
CONVERGED_ITS. A genuine residual convergencewas labelled
CONVERGED_SNORM_RELATIVE.Reading a difficulty report is how a continuation driver decides whether a parameter
station is reachable, so a mislabelled convergence is exactly the kind of error that puts
false rescues on a regime map without anyone noticing.
Found while checking why a hard Drucker–Prager station was reporting
CONVERGED_ITSon asolve that had no iteration cap in play. It had actually converged on the step norm — a
different and much weaker claim.
Also fixed
DIVERGED_OBJECTIVE_DOMAIN(-13) andDIVERGED_OBJECTIVE_NANORINF(-14) were unmappedand surfaced as
UNKNOWN_n; -4 isDIVERGED_FUNCTION_NANORINFin PETSc, notDIVERGED_FNORM_NAN.How it survived
The table is hand-written on purpose — the module must import without the Cython
extension present — so it can drift. The existing test pinned it to itself:
The KSP table sitting next to it was checked against
petsc4pyand is correct. Only theSNES one was never pinned.
test_snes_reason_table_matches_petscnow checks both directions: every label matches theenum, and every reason PETSc can return is mapped — so a PETSc renumbering or a future
hand-edit fails the test instead of silently mislabelling solves.
Verified against
petsc4py3.25; the reason-table tests pass.Underworld development team with AI support from Claude Code