fix(solve-report): honest rotated-path reports, KSP reason namespace, resume-anchor lifecycle#437
Conversation
… anchor lifecycle (PR #377 review) Fixes the three MAJOR and three MINOR findings from the post-merge adversarial review of PR #377: 1. _capture_rotated_report now honours both rotated result dicts: the nonlinear loop's ksp_its LIST (summed), nonlinear_iterations, and the outer converged verdict. Previously a TypeError on the list was swallowed by a blanket except, leaving solve_report = None after every nonlinear rotated solve. The blanket swallow is gone: a malformed dict now raises (the dicts and the reader are a contract; the swallow is what masked the breakage). 2. KSP converged-reason codes get their own table (KSP_ prefix) — they were rendered with SNES names that share integers but mean different outcomes (e.g. -3: KSP DIVERGED_MAX_IT vs SNES DIVERGED_LINEAR_SOLVE). A test pins the table against petsc4py's enum. 3. Both rotated paths report a real residual: the linear solve computes the true rotated residual (preonly/LU never computes a norm; iterative norms can be preconditioned), the nonlinear loop returns its final/initial |F|. 4. estimate_difficulty raises NotImplementedError with rotated free-slip BCs (that path solves outside self.snes, so the cap and anchor cannot reach it — it previously ran unbounded and returned None), and rejects zero_init_guess / divergence_retries kwargs that would break the probe contract. 5. Anchor lifecycle: a CONVERGED probe clears the resume anchor (a warm first call on a converged state previously armed an unreachable target at tolerance*|F_converged| and ground extra iterations with a misleading exit), and _reset clears it (a torn-down solver is a different problem). 6. Report capture moved into the finally of _snes_solve_with_retries: a solve that raises now leaves an honest report of the failed attempt instead of the previous solve's converged one. Tests: 5 new cases in test_1055 (rotated linear + nonlinear reports, rotated / kwarg rejection, converged-warm anchor hygiene, KSP table pinned to petsc4py). Underworld development team with AI support from Claude Code
Verification: the original attack probes, rerun against this fixThe adversarial review of PR #377 left its probe scripts; rerunning the three that demonstrated the MAJOR findings, unmodified, against this branch's build: p2 (rotated reports) — linear: p3c (anchor lifecycle) — the script now crashes formatting the anchor because it is p5 (stale report on raised solve) — after an injected mid-solve failure: p4 (np2 parallel) — plain / bounded / resume / rotated-annulus reports all rank-consistent, anchors consistent, resume chain still terminates at the anchored point ( Gate: level_1+tier_a 479 passed, 0 failed in the worktree; test_1018 + test_1019 21/21. Underworld development team with AI support from Claude Code |
Summary
Quick-deploy fixes for the findings of the post-merge adversarial review of PR #377 (posted on that PR). All three MAJOR and all three MINOR findings are addressed; the NOTE-level items are documented in the review thread and the two probe-contract ones are guarded here.
What was broken (all demonstrated live in the review)
solve_report = None—_capture_rotated_reportread the nonlinear dict'sksp_its(a per-Newton list) as an int; theTypeErrorwas swallowed by a blanketexcept Exception, so a successful 7-iteration solve recorded nothing.estimate_difficultyon a rotated solver silently ignored its cap — the rotated dispatch returns before_snes_solve_with_retries, so amax_nl_its=1probe ran the full unbounded Newton loop and returnedNone.tolerance*‖F_converged‖, and no event (convergence, physics change, solver rebuild) ever cleared it.-3is KSPDIVERGED_MAX_ITbut SNESDIVERGED_LINEAR_SOLVE); a diagnostic that mislabels failure modes is worse than none.fnormalways NaN — no residual in the result dict).The fixes
_capture_rotated_reporthandles both rotated result dicts (list vs intksp_its,nonlinear_iterations, outerconvergedverdict) and renders reasons with a new KSP-namespace table insolve_report.py(names carry aKSP_prefix so report strings are unambiguous). The blanket swallow is removed — the dicts and the reader are a contract, and the swallow is what masked the breakage.‖·û − b̂‖explicitly (one matvec — preonly/LU never computes a norm and iterative norms can be preconditioned); the nonlinear loop returns its final/initial‖F̂‖.estimate_difficultyraisesNotImplementedErrorwith rotated free-slip BCs, andTypeErroronzero_init_guess/divergence_retrieskwargs (a capped probe endsDIVERGED_MAX_ITby design — retrying on it would run multiples of the advertised cap)._resetclears it (a torn-down solver is a different problem).finallyof_snes_solve_with_retries: a raising solve leaves an honest report of the failed attempt._capture_solve_reportnow state their sanctioned failure mode.Validation
test_1055_solve_report.py: 10/10 — including 5 new cases: rotated linear report (KSP namespace, finite residual), rotated nonlinear report (nl/ksp counts match the result dict, outer verdict honoured), rotated + kwarg rejection, converged-warm anchor hygiene (no grinding, no armed anchor), and the KSP table pinned against petsc4py'sConvergedReasonenum.test_1018_rotated_freeslip.py+test_1019_boundary_flux.py: 21/21 (rotated solve tail touched).level_1 and tier_agate: 479 passed, 0 failed (17 skipped, 1 xfail).Review provenance
Review thread with probe evidence: #377 (post-merge adversarial review comment). Note for PR #418: this PR adds
"rnorm"/"rnorm0"keys to the rotated result dicts — the #418 rebase should preserve them.Underworld development team with AI support from Claude Code