Skip to content

Nonlinear solver defaults: Newton with one cold Picard step, smooth yield law, declared plastic regularisation (#791) - #794

Merged
lmoresi merged 14 commits into
developmentfrom
bugfix/picard-warmup-791
Oct 12, 2026
Merged

lmoresi merged 14 commits into
developmentfrom
bugfix/picard-warmup-791

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Fixes #791. Changes the nonlinear solver defaults and adds a declared plastic regularisation.

What changes for a user

before after
consistent_jacobian (every solver class) False: Picard throughout True: Newton, with ONE automatic Picard step on a cold start
ViscoPlasticFlowModel yield law exact hard Min smooth: softmin, sqrt family, anchor "onset", δ = 0.1
Krylov restart PETSc's 30 100 (outer KSP of every class, Stokes velocity split)
solve(picard=N) under Newton raised NotImplementedError N real Picard steps, then Newton

The smooth default is exact on the viscous branch, so a model that never yields gives the viscous answer. It is exact in the plastic limit too. At the yield point it under-stresses by 4.5%, and its yielded tangent is at least −1e-4 of the viscous one. yield_mode="min" and consistent_jacobian=False remain as explicit choices.

The automatic Picard step is a real frozen-tangent step, not the former nrichardson sweep (#791). It is skipped:

  • for a ksponly solve;
  • for a flux whose coefficient does not depend on the unknowns;
  • on the rotated free-slip and fault-contact paths, which run their own loops. An explicit picard=N on the rotated path stages Picard, then Newton.

The step runs inside the solve's update hooks, wall-clock guard, estimate_difficulty cap and report. The Newton stage that follows stops at tolerance·‖F(u_cold)‖.

New

  • Parameters.plastic_rate_strengthening (η_reg) on ViscoPlastic, VEP and TI-VEP. The plastic viscosity becomes η_pl = τ_y/(2ε̇_II) + m·η_reg.

    • It floors the plastic viscosity AND the tangent along the flow. Without it, a yielded layer is a near-singular mechanism mode of the velocity block (notch: Rayleigh quotient 1.8e-7).
    • Its scale is the model's. There is no universal dimensionless value: a 1% overstress on the notch is 20 σ_y in a convective lid. The default is therefore 0 (no term).
    • A pressure-dependent yield stress under the consistent tangent without it warns.
  • solve(homotopy="rate_strengthening") ladders m from the viscous limit, read from the fields, down to exactly 1 (ViscoPlastic only). Any other homotopy string now raises.

  • newton_pressure_coupling=False: the Newton velocity block with the frozen pressure column. Far from the solution, the full tangent's d(η)/dp term makes the Schur complement indefinite (notch: J_vp 41% off −Bᵀ, 40/1968 eigenvalues change sign). The default stays True, which is benign in the basin of convergence.

  • consistent_jacobian="continuation": one solve, with α keyed on the residual relative to ‖F(u=0)‖. It backs off when F climbs and re-enters warm, with Picard iterations, on divergence.

  • solve_report.config records what actually ran (Solver introspection does not report the numerical configuration a solve ran with #806):

    • SNES, line search, KSP with its restart, and the fieldsplit sub-solvers;
    • the tangent (including warm-up steps and α);
    • for Stokes, the pressure gauge and the yield-law regularisation.

    The transcript's solve event carries the tangent, gauge and regularisation. A mean-pressure nullspace feeding a pressure-dependent viscosity warns once per solve.

Behaviour notes

  • Iteration counts move for every nonlinear solve. Results move by the smooth law near yield (up to 4.5% at the yield point), never on the unyielded branch.
  • solve_report.nl_its, ksp_its and history include the warm-up, and reduction is measured from the cold residual.
  • picard is now the LAST argument of the Scalar/Vector/MultiComponent solve(). Five subclasses pass _force_setup positionally.
  • Changing consistent_jacobian or newton_pressure_coupling after a solve recompiles the Jacobian on the next one.
  • homotopy=True runs the δ march under the model's anchor ("onset" by default). Pass anchor="yield" for an approach from above.

Benchmark (solver-stability rule)

Spiegelman notch, refinement 1 (two multigrid levels: too few to test the multigrid, and silent on the resolution trend, which is the open measurement), cold start from zero, tolerance 1e-6, cap 250 iterations, same build and fixture. Only the changed defaults differ. KSP iterations are the cost measure.

η_bg old defaults: exact Min, Picard, restart 30 new defaults: smooth (onset, δ 0.1), Newton + 1 Picard step, restart 100
1e24 DIVERGED_LINE_SEARCH, 43 its, 81 KSP, reduction 1.4e-2 converged, 144 its, 2,656 KSP
5e24 DIVERGED_LINE_SEARCH, 35 its, 125 KSP, reduction 9.0e-3 DIVERGED_MAX_IT, 251 its, 9,785 KSP, reduction 1.6e-3
5e24, cap 1000 DIVERGED_MAX_IT, 1,001 its, 50,544 KSP, reduction 1.54e-3

The hard case, on the new defaults, by the two routes the guides give:

5e24 result
consistent_jacobian="continuation", one solve converged, 115 its, 768 KSP
η_reg = 1e-3 η_bg, homotopy="rate_strengthening" 11 rungs, m 1e6 → 1 (two rungs retried), converged at m = 1: 441 its, 2,912 KSP in total

The old defaults stop on a line-search failure at a reduction of about 1e-2 on both cases. The new defaults converge the 1e24 case in one solve. On 5e24 a single Newton solve reaches 1.6e-3 by iteration 140 and then stalls: monotone, but 1.604e-3 at 150 and 1.543e-3 at 1000 (0.99995 per iteration, so more iterations do not help), at about 39 KSP per iteration (13–15 in the basin of convergence), because far from the solution the full tangent's linear solve is the expensive part. That case needs the blend or the declared ladder, as plasticity-solvers and nonlinear-solver say.

Tests

  • test_1068: the warm-up contract per tangent.
    • A warm-up step equals one frozen-tangent iteration on a boundary-driven box.
    • N steps converge to the same answer, and a warm solve takes none.
    • The Newton stage is anchored to the cold residual.
    • _force_setup is not read as picard.
    • A tangent change recompiles, and α is per solver.
    • A flux made nonlinear after a solve takes the cold step.
    • The partial tangent restores J_vp = −Bᵀ.
  • test_1069: the gauge record and warning.
    • The Krylov restart is checked on the live KSP, after a solve.
    • Switching to Drucker-Prager after a solve is seen by the record.
  • test_1071: η_reg floors the plastic viscosity and is recorded. Unstated, 0 and 0.0 are the same model and warn. The ladder runs and ends at m = 1. A misspelt homotopy raises. VEP carries the term and refuses the ladder.
  • test_1055: the default law pinned to its closed form to 1e-12.
  • tests/parallel/test_1081: the cold-start verdict is rank-uniform. The old per-rank norm disagreed at np=2 and np=3, and in a solve it hung the job.
  • test_1018: rotated free-slip keeps frozen-tangent coverage and stages an explicit picard under Newton.
  • Updated for the new defaults: 0104, 0201, 0641, 1057, 1059, 1066.
  • Full serial suite (tests/test_*.py without tier C, plus the serially-run parallel guard; test_0053_hang_watchdog excluded, test_0053 hang_watchdog: SIGSEGV inside CPython faulthandler dump_traceback during full tier_a runs (state-dependent) #793) on 6535935: 2,998 passed, 0 failed. tests/parallel/test_1081 passes at np=2 and np=3. The style gate and the test-coverage check pass.

Adversarial review

Two rounds: three reviewers on the branch, then one on the fix commit. The findings, and the attacks that failed, are in the review comment below. All blockers are fixed in this branch.

Open, not fixed here:

Merged with development (#823 tier 2, #835), 2026-10-11

Development's JIT now lowers kernels onto a graph of named quantities by default, with the previous route kept as jit_route = "expanded". The merge (2c6bc2f) takes the solver's route in _jacobian_source and keeps this branch's warm-up blend; details are in the review record, round 3.

The merge review found one blocker this branch's default exposes, fixed here:

Tests on 72f0bd8: the serial suite (test.sh batches, not tier C) passes on both JIT routes, 3,139 each, 0 failed; the parallel directory at np=4 passes apart from test_1063[ti], which is #826 and fails the same way on development.

Open from the merge review (for decision, not fixed here): solve(homotopy=True) ends the delta march at the power-mean law with delta = 9.8e-4 rather than the model's stated delta = 0.1; on the DP box the automatic warm-up takes 19-20 iterations against 9 with picard=-1.

Underworld development team with AI support from Claude Code

The Layer 1 design specifies the cold-start warm-up and solve(picard=N) as
Picard (frozen-coefficient) iterations: linear Stokes solves with the
viscosity held at the current state. The standard path instead ran SNES
nrichardson with no nonlinear preconditioner (x <- x - lambda F(x)), a
residual step with no linear solve, and not a Picard step.

The standard path now has the semantics the rotated free-slip path
already had:
- consistent_jacobian=False: every iteration uses the frozen tangent,
  so picard is satisfied by the solve itself.
- "continuation": picard=N makes the alpha=0 stage run max(N, natural
  stage-1 count) iterations, both measured against the original
  residual (stage 1 after the N-iteration block uses an absolute target,
  since PETSc's rtol restarts with each snes.solve call). Per-stage
  counts are recorded in _continuation_stages, reset every solve.
- True: picard>0 raises on a nonlinear residual (no frozen tangent is
  compiled) and is a no-op on a linear one.

The automatic cold-start warm-up is removed rather than repaired; it
was never a Picard step. When the rest state is exactly zero
(homogeneous essential BCs, no stress history) Newton's first iteration
is the Picard step (test_1068, < 1e-6). A boundary-driven or
stress-history problem does not have that property (first steps
measured 45% apart on a sheared box); a genuine Picard entry there is
"continuation" + picard=N. Docs, both solver skills and test_0201 are
updated to say so; the dead phase="picard" branch of
_warn_on_divergence is removed.

Behaviour note: solve(picard=N) under the default frozen tangent is now
a no-op (it previously ran N nrichardson sweeps). Five example scripts
use it that way; none raise.

Tests: test_1068 (7 tests, hard baselines). Targeted solver suites
69/69; tier_a 1714 passed, 0 failed (first revision). Notch benchmark,
cold Newton, before/after on the same base commit: KSP effort -18%
(delta=0.4, tol 1e-6, both converge) and -47% (tol 1e-8, both stall at
the same residual); delta=0.1 stalls identically on both.

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings September 26, 2026 03:24

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.

…rtial tangent, eta_reg, solve config

Mechanisms only. The solver defaults (consistent_jacobian False, exact Min yield,
Krylov restart 30) are unchanged by this commit; the next commit flips them.

Picard warm-up (#791). Under consistent_jacobian=True, solve(picard=N) now runs N
frozen-tangent steps and then Newton, in every solver class (Scalar, Vector,
MultiComponent, Stokes). The Jacobian is compiled once in the blended form
J_p + alpha (J_n - J_p); alpha is a constants[] atom, so switching costs no
recompile. A cold start takes ONE such step automatically, except for a ksponly
solve and a flux whose coefficient does not depend on the unknowns (on a linear
constrained annulus the extra step diverged a one-iteration solve: #817).

Continuation. consistent_jacobian="continuation" is one SNES solve with alpha keyed
on the residual relative to F_ref = ||F(u=0)||, so a warm start enters the ramp
where its residual puts it. Back-off when F climbs, warm re-entry with Picard
fallback on divergence.

Partial tangent. newton_pressure_coupling=False keeps the Newton velocity block and
takes the pressure column from the frozen flux. Far from the solution the
Drucker-Prager d(eta)/dp term makes the Schur complement indefinite (Spiegelman
notch: J_vp 41% off -B^T, 40/1968 eigenvalues change sign).

Declared regularisation. plastic_rate_strengthening (eta_reg) on ViscoPlastic, VEP
and TI-VEP: eta_pl -> tau_y/(2 edot_II) + m eta_reg. It floors the plastic
viscosity and its tangent modulus. Its scale is stated per model, because no
universal dimensionless value exists.
solve(homotopy="rate_strengthening") ladders m from the viscous limit (found from
the fields) down to exactly 1. A pressure-dependent yield law without it warns.

Solve report (#806). solve_report.config records the SNES/KSP/line-search settings
that actually ran (read from the live PETSc objects), Krylov restart, the tangent,
the regularisation, and the pressure gauge. The gauge entry says whether a nullspace
was attached and whether the rheology reads p. A nullspace projection feeding a
pressure-dependent viscosity warns. The transcript's solve event carries the
tangent and gauge.

Tests: test_1068 (warm-up contract, partial tangent), test_1069 (gauge record and
warning), test_1071 (eta_reg floor, warning, ladder ends at m = 1), test_0201
(no warm-up on a linear flux).

Underworld development team with AI support from Claude Code
… Krylov restart 100

Louis, 2026-10-05: "set the exact min to not be the default anymore ... the default
should be Picard only for that one cold solve first iteration."

- ViscoPlastic: yield_mode "softmin", smoother "sqrt", anchor "yield", delta 0.1
  (was the exact hard Min). The "yield" anchor keeps tau/tau_y = 1 at the yield
  point and the law >= the exact one; delta 0.1 stiffens unyielded material by at
  most ~3% just below yield. VEP gets anchor "yield" explicitly. yield_mode "min"
  with delta 0 stays available.
- consistent_jacobian = True (was False) in every solver class. A cold start takes
  one automatic frozen-tangent step (previous commit); a warm start takes none.
  False and "continuation" remain explicit choices.
- ksp_gmres_restart = 100 (PETSc default 30) on the outer KSP of every class and on
  the Stokes velocity split. On the notch a yielded layer gives the Newton velocity
  block a mechanism mode; FGMRES(30) stalls on it (rate 1.000), FGMRES(100)
  converges.

Tests that are about the old behaviour now pin it (test_1057's Picard baseline,
test_0104's stale-slot check, test_1068's hard-Min fixtures); tests that asserted
the old defaults assert the new ones (1055, 1059, 0641). Measured side effects on
the test boxes: the cold hard-Min yielding box Picard-throughout could not solve
converges under Newton + one Picard step; pure Newton converges on the driven box
once the kink is gone.

Design doc and the nonlinear-solver / plasticity-solvers guides updated.

Underworld development team with AI support from Claude Code
…docs guides the skills now link to

Underworld development team with AI support from Claude Code
…lve's contracts, onset anchor

Three adversarial reviewers (solver control flow; constitutive models and eta_reg; the
solve record, docs and tests) on the branch at a8aa68c.

Blockers fixed
- The cold-start verdict read u.vec.norm(), a LOCAL vector: ranks disagreed and the
  warm-up's rewire and extra snes.solve hung the job (np=2, measured). It is now a
  reduced any-nonzero test. tests/parallel/test_1081 gathers the verdicts (the old
  test disagrees across ranks at np=2 and np=3).
- Krylov restart 100 was written inside the tolerance/strategy setters, so a Stokes
  solver kept FGMRES(30). It is now set once at construction, as a managed option
  (outer KSP of every class, plus the Stokes velocity split). A user's own value is
  left alone; test_1069 pins it.
- `picard` had been inserted second in the Scalar/Vector/MultiComponent solve(). Five
  subclasses pass _force_setup positionally, which forced a warm-up and a blended
  compile on linear solves. It is now the last argument; test_1068 covers it.
- Under the new default, solve(picard=N) raised on rotated free-slip. An explicit
  picard now stages Picard then Newton on the blended Jacobian.
- The default yield anchor is "onset" (Louis's choice from the review). With "yield",
  every unyielded cell was 1/(1 - delta/2) stiffer (5% at delta 0.1) and the yielded
  tangent was negative (-0.029). test_1055 pins the closed-form law to 1e-12.
- The guide front matter had a YAML colon that dropped the nonlinear-solver guide.
  An uncommented except-pass was failing the style gate.

Should-fix items fixed
- One warm-up policy (_plan_picard_warmup) replaces four pasted blocks that had
  drifted: the cheap cold test comes first, and the symbolic facts are cached
  (_form_fact). A warm time step no longer pays 3.7 s for the linearity test.
- The warm-up runs inside _snes_solve_with_retries: update hooks, wall-clock guard,
  probe cap and report. Its iterations, linear solves and history count in the
  report, and the reduction is measured from the cold residual. Newton is anchored
  at tolerance x ||F(u_cold)||; without the anchor a near-linear box stagnated on
  SNORM. A failed warm-up warns.
- Changing consistent_jacobian or newton_pressure_coupling recompiles the Jacobian.
  Each solver has its own alpha: a shared container let one solver move another's
  tangent. Alpha is recorded only where it is live.
- Ladder: the entry is relative (1e3 x max(eta_ve/eta_reg)), and the onset is a masked
  numerator/denominator, so a zero or Piecewise eta_reg no longer produces zoo. A
  failed projection raises. Retries restart from the last converged rung, and NaN/inf
  is refused. Running out of max_steps warns. The probe objects are per solver with
  deterministic names; the code uses .array and uw.mpi. VEP gets a clean
  NotImplementedError.
- One zero test (_parameter_is_zero, is_zero): Float(0.0) == 0 is False in SymPy.
  The eta_reg Parameter and its term are each defined once. The m setter no longer
  recompiles (22x per rung).
- homotopy= accepts only False, True or "rate_strengthening". The ladder is
  exported from uw.systems.
- The gauge and DP warnings fire once per solve, from the user's frame. The DP warning
  needs a pressure-dependent YIELD stress under the consistent tangent. Rotated
  reports carry config, and the transcript carries regularisation.
- Guides and docstrings describe the new defaults, eta_reg and its ladder,
  newton_pressure_coupling (a new trap row), the solve record and the restart.
  Stale "default (Picard)" text is gone. test_1018 keeps the rotated frozen-tangent
  coverage. test_1071 asserts that the ladder ran.

Underworld development team with AI support from Claude Code
…tart, ladder exits

One reviewer on 8010d33.

- The cached symbolic facts (flux linearity, pressure in the rheology) survived a rebuild.
  A warm solve's plan never reads them, and the read after the build hit the old dict.
  So "solve viscous, then switch on Drucker-Prager" recorded pressure_in_rheology=False
  with no warning, and a flux made nonlinear skipped the cold Picard step. The cache is
  now dropped wherever a rewire or a setup completes. test_1068 and test_1069 cover it.
- test_1018 asserted the old NotImplementedError. It now asserts that picard=3 under
  Newton stages Picard, then Newton, and matches the essential-BC answer.
- The rotated path dropped the gauge and Drucker-Prager warnings. Both paths now emit
  them through one helper.
- KSPSetType on the single-field custom-P FMG route reset the restart to PETSc's 30,
  while the report said 100. The restart is re-applied to the live KSP, and the
  composed solver's multigrid route takes the default. test_1069 reads the restart
  from the live KSP after a solve.
- Ladder:
  - no early return skips the warning, and scale_big is validated;
  - a failure with no converged rung restores the starting fields and their
    has_solution;
  - a revert marks converged only when it goes back to a converged rung.
- The failed-warm-up warning comes from the caller's frame and names an expired
  deadline. The homotopy check is shared between the two solve() entry points.
  The δ march docstring says it runs under "onset" unless anchor="yield" is passed.

Underworld development team with AI support from Claude Code
…t failed, open items

Underworld development team with AI support from Claude Code
@lmoresi lmoresi changed the title Picard warm-up is a frozen-tangent step, not an nrichardson sweep (#791) Nonlinear solver defaults: Newton with one cold Picard step, smooth yield law, declared plastic regularisation (#791) Oct 6, 2026
@lmoresi

lmoresi commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — nonlinear solver defaults and the declared plastic regularisation (PR #794)

Branch bugfix/picard-warmup-791 vs development.

Method: two rounds.

  • Round 1 (at a8aa68c): three independent reviewers, one each on the solver control flow, the constitutive models and η_reg, and the solve record, docs and tests.
  • Round 2: one reviewer on the fix commit 8010d33.

Every reviewer was told to break the change and to verify each finding by probe. Probe scripts are in ~/+Simulations/review_794/. All findings below are fixed on the branch (8010d33, 6535935) unless listed as open.

Round 1 — blockers

  • Cold-start verdict was per rank (_solution_is_trivially_zero read u.vec.norm(), a LOCAL vector).

    • At np=2, with the initial guess non-zero in one corner, rank 0 took the warm-up and rank 1 did not. Rank 1 failed with MPI_ERR_BUFFER; rank 0 spun until killed.
    • The verdict is now a reduced any-nonzero test. tests/parallel/test_1081 checks it; the old verdict disagrees at np=2 and np=3.
  • Krylov restart 100 never took effect for Stokes. It was written inside the strategy setter, which construction never runs; the live KSP and the report both showed 30. It is now set once at construction as a managed option.

  • picard was inserted second in the Scalar/Vector/MultiComponent solve(). Five subclasses pass _force_setup positionally, so _force_setup=True became picard=1: a warm-up and a blended compile on a linear AdvDiffusion solve. picard is now the last argument.

  • solve(picard=N) raised on rotated free-slip under the new default. It now stages N Picard iterations, then Newton (power law: 17 its, error 1.2e-8 against the essential-BC answer).

  • The default yield anchor "yield", sqrt family, δ 0.1, had two defects:

    • every unyielded cell was 1/(1 − δ/2) stiffer: η/η_ve = 1.0499 at f → 0, and a box that never yields moved 4.75%;
    • the yielded tangent was negative: τ/τ_y peaks at 1.033 at f = 1.2, and dτ/dε̇ = −0.029 at f = 1.5.

    The maintainer chose sqrt / "onset" / δ 0.1: η(0) = 1, τ(f=1) = 0.955 τ_y, tangent ≥ −1e-4.

  • A YAML colon in the guide front matter dropped nonlinear-solver from uw.capabilities(); test_0030 failed 2 tests.

  • An uncommented except: pass in yield_continuation.py failed the Charter S4 style gate.

Round 1 — should-fix

  • Linearity test on every warm solve. The symbolic test ran before the cheap cold-start test: 3.7 s per warm step on a 16×8 VEP box, and 5.8 against 2.2 s/step overall. The cold test now runs first, and the facts are cached.
  • The warm-up ran outside the solve's contracts: before the update hooks and the wall-clock guard, outside the estimate_difficulty cap (cap 1 ran 2 iterations), and missing from the report (fnorm0 was 4.04e-2 against a cold F of 0.120). It now runs inside _snes_solve_with_retries.
  • Newton after the warm-up measured rtol from the post-warm-up residual, and stagnated on SNORM on near-linear boxes (2 its without the warm-up). It is now anchored at tolerance·‖F(u_cold)‖.
  • Changing the tangent after a solve did not recompile, so the report described a tangent that was not running (Poisson False → True: 9 its, against 3 for a fresh solver). α was one process-wide container shared by every solver. Both setters now mark a rewire, and α is unique per solver.
  • Ladder defects:
    • a retry restarted at the largest converged m (1e6), not the last good rung;
    • a zero or Piecewise η_reg gave zoo or stale zeros, and a ladder that did nothing reported converged=True;
    • the entry multiplier was absolute;
    • each rung recompiled (5.2 s against 0.23 s);
    • VEP failed with an AttributeError instead of a clean refusal;
    • the probe variable was named from id(solver), which differs between ranks.
  • Zero test spelled three ways. Float(0.0) == 0 is False in SymPy 1.14, so plastic_rate_strengthening = 0.0 compiled a zero term and suppressed the warning.
  • Warning hygiene. The gauge warning fired twice per solve from an internal frame. The Drucker-Prager warning fired under the frozen tangent and for a p-dependent η0 with a constant yield stress.
  • The record had gaps. Rotated reports carried no config. The transcript dropped the regularisation. Guides and docstrings still described the old defaults, and the new controls were undocumented.
  • Tests that could not fail. test_1069 asserted only that a "restart" key existed (it was 30). test_1071's string checks could not tell 0.05 from 0, and it never checked that the ladder ran. test_1018's rotated frozen-tangent coverage had silently become Newton.
  • Duplicated code. The warm-up policy was pasted 4 times and had drifted: "continuation" ignored picard=N in three classes.

Round 2 (on 8010d33)

  • blocker: the form-fact cache went stale across a rebuild. Viscous → Drucker-Prager, solved warm, recorded pressure_in_rheology=False with no warning. A flux made nonlinear skipped the cold step. The cache is now dropped wherever a rewire or a setup completes.
  • blocker: test_1018 still expected the removed NotImplementedError. It now asserts the staging.
  • should-fix: the rotated path discarded the warning notes.
  • should-fix: KSPSetType on the single-field FMG route reset the live restart to 30 while the report said 100. The restart is re-applied, and test_1069 now reads it from the live KSP.
  • should-fix: the ladder's early exits skipped its warning. A failed first rung marked non-solution fields as converged.
  • nit: the duplicated homotopy check, the warm-up warning frame, the δ-march anchor docstring.

Attacks that failed

  • Linearity guard: correct on 19 cases (Poisson k=1/k(T)/k(u), Stokes η=1/η(T)/η(|v|), VE, VP, DP, VEP, power law, units-aware viscous and VP, projections, AdvDiffusion, Navier-Stokes).
  • Warm loop: no warm-up on steps 2+. Nonlinear Poisson matches Picard-throughout to 7.7e-11 (3 vs 9 its).
  • Eisenstat-Walker: the warm-up iterate is identical with EW on and off.
  • newton_pressure_coupling=False with a p-independent rheology: identical iterations, dv = 0, ‖J_T − J_F‖ = 0.
  • Units: uw.quantity(1e20, "Pa*s") with η_ref 1e21 gives deep-yield η = 0.1005. The default 0 [Pa s] omits the term.
  • Ladder: ends at exactly 1.0 on a clean run, after a mid-ladder failure, and after a failure at m = 1. It refuses NaN through a separate flag reduction, and is bounded by max_steps.
  • Names: two VP models get distinct m_reg atoms. describe() names η_reg and m_reg.
  • Record: JSON-safe. A transcript round-trips through uw.Transcript. The snapshot creates no NPC or line search and prints nothing.
  • Probe cap: picard 2/5/3 with cap 3 gives 3 iterations. Tolerances are restored.
  • Anchor: never loosens a solve (boundary-driven DP, VP, n = 0.5 and 3).
  • Rank uniformity: the ladder runs at np=3. Every new collective is reached by every rank.
  • Restart: reaches the live KSP for default, fast, robust, gamg and refinement=2 FMG Stokes, and for plain, gamg and fmg Poisson. A user value is kept. The composed solver keeps its 200.

Open (not fixed on this branch)

  • Automatic cold Picard warm-up diverges a LINEAR constrained Stokes solve (Stokes_Constrained + multiplier free-slip) #817 is misdiagnosed. Stokes_Constrained fails on its second Newton iteration whatever the tangent:
    • with η(1 + 1e-6 ε̇_II): automatic warm-up DIVERGED_DTOL; picard=-1 DIVERGED_MAX_IT oscillating; consistent_jacobian=False identical;
    • the linearity guard only hides this for linear fluxes.
  • VEP under the Newton default. The guide recommends the frozen tangent for VEP, but test_1052's loading through yield converges under the new default (nl = 1 per step).
  • Retries in the record: with divergence retries, the report keeps the warm-up plus the last attempt (pre-existing behaviour for retries).
  • mpi4py reductions: _solution_is_trivially_zero and _masked_max_ratio reduce through uw.mpi.comm (mpi4py), which the library already does elsewhere. A PETSc global-Vec norm would serve.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

✅ Test Suite: success

…ad of unwrapping

_compute_flux_linearity unwrapped the whole flux and differentiated it by each
gradient symbol; _reads_pressure unwrapped the yield expression and searched
it. Unwrapping rebuilds the shared expression graph as a tree, and on the
Spiegelman notch (Drucker-Prager yield, residual strength, viscosity floor,
smooth yield law) the linearity test ran for hours, the pressure test for
minutes. Both now ask _reaches, which follows each UWexpression into its
contents and visits every node once; the linearity test differentiates only
the wrapped top level of the flux.

Underworld development team with AI support from Claude Code
Conflicts: up_G2/up_G3 keep #794's F1_for_up (newton_pressure_coupling) and
differentiate it with tier 1's diff_wrt_field; the reviews index keeps both
rows. The linearity check's top-level derivative also goes through
diff_wrt_field (the solver census in test_0023).

Underworld development team with AI support from Claude Code
Conflicts:
- petsc_generic_snes_solvers.pyx: _jacobian_source keeps this branch's
  warm-up blend (mode True with _picard_blend) and takes development's
  per-solver route for _jacobian_unwrap; the consistent_jacobian docstring
  keeps this branch's defaults (True, one cold Picard step) with tier 2's
  description of the Newton source (differentiate through the named
  quantities); the guard docstring names the frozen tangent, not the default.
- systems/solvers.py: Stokes.solve keeps development's _skip_stress_history
  and this branch's untyped homotopy (it also takes "rate_strengthening").
- test_0104: _build keeps the frozen tangent this branch pins and the route
  development's tests select.

Underworld development team with AI support from Claude Code
…t the default

The status line still read "opt-in, default off" from #258; the merge brings
tier 2's header into the same note, so the note now says where the default
changed and that the defaults it describes are #258's.

Underworld development team with AI support from Claude Code
…outes (#841)

The guard in _jacobian_unwrap added 1e-36 to the base of half-integer powers
only. SymPy merges a power of the bare strain-rate invariant into one that is
not a half-integer (sqrt(g)**(1/3) is g**(1/6)), so a law such as
eta = 1/(1 + edot**(1/3)) on stokes.Unknowns.Einv2 had a NaN Newton tangent at
rest on both routes, and the cold start raised PETSc error 77; the blended
warm-up kernel inherits it at alpha = 0. Under this branch's default tangent
that is every cold solve of such a law.

_jit_graph.is_fractional_power is now the rule on both routes: a non-integer
exponent on a base with free symbols. guard_half_integer_powers is renamed
guard_fractional_powers. In floating point the added 1e-36 vanishes against any
base above about 1e-20, so values away from rest are unchanged; only kernels
that contain such powers change text. The frozen tangent and the residual are
not routed through the guard.

test_1067 gains the regression (a cold Newton solve of that law reaches the
frozen tangent's solution on each route, and J at rest is finite); it raised
error 77 on both routes before this change. test_0022's reference rule follows
the guard.

Underworld development team with AI support from Claude Code
…ings

The ladder's P0 probe projection compiled on the process default route, so a
solver set to jit_route = "expanded" still computed its entry multiplier with
graph kernels. The probe now takes the solver's route.

_jacobian_source and consistent_jacobian: True returns the blend once a cold
start has planned its Picard step; alpha = 0 is bit-identical to Picard only
while the Newton blocks are finite; the Newton source is _jacobian_unwrap on
the solver's route.

Underworld development team with AI support from Claude Code
…835)

The conflict resolution, the blocker it exposed and fixed (#841), the probe
route and docstring fixes, three open items (the delta march's end state, the
warm-up's iteration cost on the DP box, the #827 banner), and the attacks
that failed.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

Adversarial review, round 3: the merge with development (2c6bc2f)

One reviewer attacked the seams between this branch's tangent machinery and development's two JIT routes, with probes on the installed build. The full record is in docs/reviews/2026-10/nonlinear-defaults-794-adversarial-review.md, round 3.

Fixed in this branch

  • blocker (Newton tangent is NaN at rest for a fractional (non-half) power of the strain-rate invariant: the Jacobian guard covers half-integer powers only #841, 4cdc00d): a fractional power of the bare invariant, eta = 1/(1 + Einv2**(1/3)), raised PETSc error 77 on a cold solve under the default tangent, on both routes. SymPy merges sqrt(g)**(1/3) into g**(1/6), and the guard covered half-integer powers only, so J(0) was NaN and the warm-up blend inherited it at alpha = 0 (0 * NaN). The guard now covers every non-integer exponent on a base with free symbols, on both routes. In floating point the added 1e-36 vanishes against any base above about 1e-20, so only states at rest see it.
  • should-fix (bf4c32f): the rate-strengthening probe projection compiled on the process default route; it now takes its solver's route.
  • should-fix (bf4c32f, 3eaa99b): docstrings and the design note that the merge made false (the blend under True, alpha = 0 exact only while the Newton blocks are finite, "default off").

Open, for decision

Attacks that failed

  • Every _jacobian_unwrap and getext call passes the solver's route. With the solver on "expanded", the Jacobian blocks hold no graph nodes and the C no uwt_ temporaries, for the default tangent, "continuation" and newton_pressure_coupling=False.
  • \alpha_{N}, the yield softness, m_reg and \eta_reg are constants[] slots on both routes; ramping them leaves the cache key unchanged.
  • A mid-run route switch keeps _picard_blend and recompiles the blend on the new route with the same answer. Form facts are structural (_reaches), so they cannot go stale with the route.
  • DP box, default law and tangent: 20 iterations on both routes, solutions equal to 2.2e-16 (v) and 7e-16 (p), J(0) finite and equal.
  • test_0104 under the default tangent: the ramp keeps its cache key and matches a fresh build to 1e-13, and the stale-slot check still raises on a warm solve, so the frozen-tangent pin hides nothing.

Tests on 72f0bd8: serial suite on both routes 3,139 passed, 0 failed (before the guard fix, the same suite failed only the two new test_1067 cases); tests/parallel at np=4, all pass except #826.

Underworld development team with AI support from Claude Code

@github-actions

Copy link
Copy Markdown

✅ Test Suite: success

@lmoresi
lmoresi merged commit c97eb9e into development Oct 12, 2026
5 checks passed
lmoresi added a commit that referenced this pull request Oct 12, 2026
Six conflicts in petsc_generic_snes_solvers.pyx, four in ddt.py, and one
each in __init__.py, model.py and two test files. Most were placement
collisions — this branch adds adjoint members where development added
unrelated ones — and keep both sides. Three were not:

- _jacobian_unwrap: development's signature (expr, route=None) for the
  JIT shared-graph route (#836) supersedes this branch's one-argument
  version, which is dropped. The two new adjoint helpers beside it,
  _transpose_kernels and _solve_transposed, are kept.
- The cold-start warm-up: development's _plan_picard_warmup /
  _snes_solve_with_retries (#794, the Newton default with one automatic
  frozen-tangent step) replaces this branch's hand-rolled nrichardson
  sweep entirely. Taking both would have applied the policy twice.
- The consistent_jacobian docstring takes development's text, which
  states the current default, plus one sentence this branch needs: the
  consistent tangent is the matrix the adjoint transposes.

Two test failures the merge brings to light, both the tests working:

- test_every_history_scheme_declares_a_verdict caught
  ForwardSemiLagrangian, a scheme development added after this branch
  forked, with no adjoint verdict. It now refuses, and says why: the
  forward trace is differentiable in the velocity, but the per-cell
  least-squares fit of the arrivals is not materialised as an operator,
  and which cell an arrival lands in is piecewise constant in it.
- test_a_swarm_step_that_loses_particles_refuses_with_the_count set
  return_coords_to_bounds = None to switch the restore off. On
  development None selects the general facet restore instead, so every
  particle was returned (972 -> 972, measured) and the fixture no longer
  showed the effect it tests. Switched off is now an identity closure,
  and the step is partial (972 -> 482) so the refusal is read off a
  change of dimension rather than an empty swarm.

test_1063_constrained_traction, the one real failure on this PR's last
CI run (SNES reason -3), passes on the merge.

36 adjoint tests pass; test_0030_capability_guides, test_0641 and
test_1057 pass.

Underworld development team with AI support from Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E87Q7KrpapxeQiLD1RiNXv
@lmoresi
lmoresi deleted the bugfix/picard-warmup-791 branch October 12, 2026 21:03
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