Skip to content

Solver configuration audit: reachability and silent fallback (report) - #485

Merged
lmoresi merged 3 commits into
developmentfrom
docs/solver-config-audit
Jul 30, 2026
Merged

Solver configuration audit: reachability and silent fallback (report)#485
lmoresi merged 3 commits into
developmentfrom
docs/solver-config-audit

Conversation

@lmoresi

@lmoresi lmoresi commented Jul 30, 2026

Copy link
Copy Markdown
Member

Diagnostic report, no code changes. Baseline development @ 04379583.

docs/reviews/2026-07/solver-configuration-reachability-audit.md, with the harness and raw results under ~/+Simulations/solver_reachability_audit/.

Why

Five defects of the same shape were found independently and by accident over a few weeks — #276/#478, #467, #468, #477, #425. Every one degrades to something that succeeds: GAMG converges, richardson converges, the solve returns a correct answer. So none presented as a failure. They presented later as "this is a bit slow", or as a sweep returning identical rows, which reads as "this variable does not matter" rather than "this variable was never set". That cost a twelve-cell sweep on #477 and a wrong premise in #471.

That rate says the next one is found by looking rather than by waiting.

Method

Per solver family: set a distinctive value for each managed option, solve() twice, pc.setUp(), then read the live PETSc objects — plus a defaults arm as control, because "reachable" is meaningless if the override happened to match the default. Findings are ranked by how invisible the failure is, not by the size of the substitution.

Findings

finding status
F-1 MG bundle silently overwrote user settings; explicit preconditioner="fmg" was worse than the default because it bypassed the override latch fixed in #471
F-2 geometric MG unreachable for every single-field solver; "auto" warns about nothing #478
F-3 Stokes and Stokes_Constrained disagree about which options are reachable, in opposite directions #483
F-4 snes_rtol is owned by design but looks settable #483
F-5 the #477 fix is committed but unpushed; #471 and #475 collide on the stale-key list raised on #475
F-6 10 of 12 preconditioner fallbacks leave no readable state — a warning is not observability #484, first instance done in #471
F-7 the barycentric→RBF retry silently switches to a dense transfer #484
F-8 two silent swallows can skip correctness guards #484

Scalar and vector solver families came back clean — every option reachable. The whole defect class is localised to the saddle-point path, which makes the follow-up work much smaller than "audit everything".

Two things worth carrying forward

The report records both, because both cost a cycle:

  • The ownership record must be keyed by the globally-qualified option name. The solver writes through a Solver_N_ prefixed view while custom_mg._configure_pcmg reads the global database with the live PC's full prefix. An unqualified record made every key look user-owned there, and the bundle silently stopped applying.
  • The defaults arm caught that, not a test. Second ownership regression in two sessions caught by the arm that was supposed to be unchanged. Never run an ownership change without one.

Known gap in the method

The audit checked option keys for reachability. It did not check the solver's own properties for being accepted-and-inert — solver.strategy validated three values and configured one, and it took @lmoresi pointing at it. penalty and saddle_preconditioner are the obvious remaining candidates and want a short targeted pass.

Underworld development team with AI support from Claude Code

lmoresi added 2 commits July 30, 2026 07:53
…llback

Diagnostic audit against `development` at 0437958. No code changes.

Five defects of the same shape were found independently over the last few weeks
(#276/#478, #467, #468, #477, #425). Every one degrades to something that
SUCCEEDS, so none presented as a failure — they presented later as "this is a bit
slow", or as a sweep returning identical rows, which reads as "this variable does
not matter" rather than "this variable was never set". This audit looks for the
rest, in two directions: reachability (the user writes a setting and something
else runs) and silent substitution (the code chooses otherwise and nothing
observable says so). Findings are ranked by how INVISIBLE the failure is, not by
the size of the substitution.

Method: per family, set distinctive values, solve TWICE, pc.setUp(), read the LIVE
PETSc objects, with a defaults arm as the control so a "reachable" verdict cannot
be an accident of the override matching the default.

Headline findings:

- F-1 (new) The MG bundle silently overwrites user settings, and asking more
  explicitly makes it worse. The override latch keys on `pc_type` ALONE, so nine
  of the bundle's ten keys are discarded unless the user also sets the tenth —
  and `preconditioner="fmg"` bypasses the latch entirely. Measured all three
  ways. Belongs in #471, which makes the bundle a single owner and would
  otherwise entrench it.
- F-3 (new) Stokes and Stokes_Constrained disagree about which options are
  reachable, in OPPOSITE directions: `ksp_rtol` is settable on one and not the
  other, the fieldsplit rtols the reverse. Scalar and vector families are clean,
  so the whole class is localised to the saddle-point path.
- F-5 (new) The #477 fix is committed but UNPUSHED — three commits on
  feature/regime-diagram not on origin, so PR #475 does not show them and an
  auditor rediscovers a solved problem. It covers snes_max_it and the two
  fieldsplit rtols; it does NOT cover the MG bundle keys, which are a build-time
  rather than a solve-time clobber. Also records the #471/#475 collision on the
  stale-key list: #471's derivation subsumes #475's patch, so whichever lands
  second takes the derivation rather than merging the lists.
- F-6 (new) Of twelve substitution points in the config surface, only two leave
  readable state — `velocity_pc` and `schur_pre`, both added in #465, and the only
  reason #467 was findable. A warning is not observability: warnings are filtered
  and deduplicated, state can be asserted on in a test. There is no public way to
  ask a solver which preconditioner it actually got, or why.
- F-4 / F-7 / F-8 `snes_rtol` is owned but looks settable (decide-and-document);
  the barycentric->RBF retry silently switches to a DENSE transfer; two silent
  swallows can skip correctness guards.

Each finding is routed to the PR it belongs in rather than to a new one.

Harness and raw results: ~/+Simulations/solver_reachability_audit/ with a README.

Underworld development team with AI support from Claude Code
…483/#484

F-1 is fixed in #471 (5a390ef). F-3/F-4 filed as #483, F-6/F-7/F-8 as #484, F-5
raised on #475. Adds the two lessons from implementing F-1: the ownership record
must be keyed by the globally-qualified option name (an unqualified record made the
bundle silently stop applying on the custom-P route), and the defaults arm is what
caught that — the second ownership regression in two sessions caught by the arm
that was supposed to be unchanged.

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings July 30, 2026 06:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown

🔍 Architectural Review Checklist

Thank you for submitting an architectural review! Reviewers should validate:

Design & Architecture

  • Design rationale is clear and well-justified
  • Trade-offs are documented with alternatives considered
  • System architecture is comprehensible
  • Integration points are clearly identified

Implementation

  • Implementation matches documented design
  • Code quality meets project standards
  • Breaking changes are identified and justified
  • Backward compatibility is properly addressed

Testing & Validation

  • Testing strategy is adequate for the changes
  • Test coverage is sufficient
  • Edge cases are properly covered
  • Performance impact has been assessed

Documentation

  • Known limitations are clearly documented
  • Benefits are quantified with metrics
  • User-facing changes are documented
  • Migration guide provided (if needed)

Review Process: See CODE-REVIEW-PROCESS.md

Approval: This PR merge = Review formally approved

@github-actions

Copy link
Copy Markdown

Test Suite: success

Phases 1 and 2 checked option KEYS and fallback BRANCHES and missed
solver.strategy, which validated three values and configured one. Phase 3 covers
that third axis: for each solve-configuring property, set two distinct values and
check whether round-trip, options, J/Pmat, |u|, |p| or iteration counts move.

The transferable finding is that the instrument was wrong three times first, each
time confidently:

1. options compared across different solver prefixes, so every pair 'differed' and
   everything read live — including the negative control, which is what caught it;
2. options read BEFORE the solve, when most configuration is written at _build,
   which reported strategy and preconditioner inert when they are not;
3. blind observables — a pressure nullspace need not move the velocity, a
   preconditioner flag need not move the answer. Three of four 'still inert'
   verdicts were artefacts of measuring |u| alone.

Once sound it independently rediscovered two known defects on development
(strategy, and preconditioner on single-field solvers = #478), which is what makes
its negative results worth anything.

Cleared: tolerance, penalty, saddle_preconditioner, preconditioner,
consistent_jacobian (live only on a NONLINEAR problem, 10 -> 5 nonlinear its),
petsc_use_nullspace, petsc_use_pressure_nullspace (live on |p|, invisible in |u|),
constant_nullspace, petsc_use_constant_nullspace, smoothing, smoothing_length.

One unresolved: multiplier_schur_pc is read but moved no observable at constant
viscosity or at eta contrast 1e4/1e6 — including an identically-failing solve.
Recorded as verify-or-remove rather than as a dead flag.

Also records a property that gets it right: constant_nullspace refuses an invalid
combination naming the offending boundaries and the remedy.

Underworld development team with AI support from Claude Code
@github-actions

Copy link
Copy Markdown

Test Suite: success

@lmoresi
lmoresi merged commit 128797f into development Jul 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants