Repository navigation
Nonlinear solver defaults: Newton with one cold Picard step, smooth yield law, declared plastic regularisation (#791) - #794
Conversation
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
…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
Adversarial review — nonlinear solver defaults and the declared plastic regularisation (PR #794)Branch Method: two rounds.
Every reviewer was told to break the change and to verify each finding by probe. Probe scripts are in Round 1 — blockers
Round 1 — should-fix
Round 2 (on 8010d33)
Attacks that failed
Open (not fixed on this branch)
|
|
✅ 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
…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
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 Fixed in this branch
Open, for decision
Attacks that failed
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 Underworld development team with AI support from Claude Code |
|
✅ Test Suite: success |
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
Fixes #791. Changes the nonlinear solver defaults and adds a declared plastic regularisation.
What changes for a user
consistent_jacobian(every solver class)False: Picard throughoutTrue: Newton, with ONE automatic Picard step on a cold startViscoPlasticFlowModelyield lawMinsoftmin, sqrt family, anchor"onset", δ = 0.1solve(picard=N)under NewtonNotImplementedErrorThe 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"andconsistent_jacobian=Falseremain as explicit choices.The automatic Picard step is a real frozen-tangent step, not the former nrichardson sweep (#791). It is skipped:
ksponlysolve;picard=Non the rotated path stages Picard, then Newton.The step runs inside the solve's update hooks, wall-clock guard,
estimate_difficultycap and report. The Newton stage that follows stops attolerance·‖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.solve(homotopy="rate_strengthening")laddersmfrom the viscous limit, read from the fields, down to exactly 1 (ViscoPlastic only). Any otherhomotopystring 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 staysTrue, 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.configrecords what actually ran (Solver introspection does not report the numerical configuration a solve ran with #806):gaugeand the yield-lawregularisation.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
solve_report.nl_its,ksp_itsandhistoryinclude the warm-up, andreductionis measured from the cold residual.picardis now the LAST argument of the Scalar/Vector/MultiComponentsolve(). Five subclasses pass_force_setuppositionally.consistent_jacobianornewton_pressure_couplingafter a solve recompiles the Jacobian on the next one.homotopy=Trueruns the δ march under the model's anchor ("onset"by default). Passanchor="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.
DIVERGED_LINE_SEARCH, 43 its, 81 KSP, reduction 1.4e-2DIVERGED_LINE_SEARCH, 35 its, 125 KSP, reduction 9.0e-3DIVERGED_MAX_IT, 251 its, 9,785 KSP, reduction 1.6e-3DIVERGED_MAX_IT, 1,001 its, 50,544 KSP, reduction 1.54e-3The hard case, on the new defaults, by the two routes the guides give:
consistent_jacobian="continuation", one solvehomotopy="rate_strengthening"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-solversandnonlinear-solversay.Tests
test_1068: the warm-up contract per tangent._force_setupis not read aspicard.J_vp = −Bᵀ.test_1069: the gauge record and warning.test_1071: η_reg floors the plastic viscosity and is recorded. Unstated,0and0.0are the same model and warn. The ladder runs and ends at m = 1. A misspelthomotopyraises. 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 explicitpicardunder Newton.tests/test_*.pywithout tier C, plus the serially-run parallel guard;test_0053_hang_watchdogexcluded, 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_1081passes 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:
test_1052converges under the new default.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_sourceand 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:
1/(1 + Einv2**(1/3))) had a NaN Newton tangent at rest on both routes, so the cold start raised PETSc error 77. The guard in_jacobian_unwrapnow covers every non-integer exponent (Closes 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).test_1067gains the regression; it failed on both routes before the change.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 withpicard=-1.Underworld development team with AI support from Claude Code