Skip to content

adjoint_solve inherits the forward solve's INEXACT Newton tolerance: every sensitivity on an iteratively-solved problem is silently short by 15-20% #769

Description

@lmoresi

What

adjoint_solve reuses the SNES's own KSP:

ksp = self.snes.getKSP()
ksp.setOperators(J, P)
ksp.solve(b, x)

so it inherits the forward problem's linear tolerance. A Newton solve's linear solves
are deliberately inexact — the Stokes tolerance property enables Eisenstat–Walker
(snes_ksp_ew), which re-picks the outer ksp_rtol every step and starts from PETSc's
default rtol0 = 0.3. That is correct for a forward step, because the outer Newton
iteration corrects the error.

mu has no outer iteration. The adjoint solve's error goes straight into
sensitivity(), as a fixed fraction — independent of any finite-difference step used
to check it, which is exactly what makes it look like a modelling result rather than a bug.

The KSP still reports CONVERGED, because it did converge — to 0.3.

Measured

Shear-thinning Stokes (eta = eta_0/(1 + edot/edot_0)), 4 Newton iterations, J = 1/2 int edot_II^2, dJ/d(eta_0) against central finite differences. Forced onto an iterative KSP
(fgmres, pc_type none) so the tolerance binds:

adjoint KSP rtol its final ‖r‖ ratio adjoint/FD
3e-1 (EW's default) 7 4.77e-03 0.845701
1e-3 30 1.96e-05 0.999119
1e-6 500 3.29e-08 0.999078
1e-10 500 3.29e-08 0.999078

On the Spiegelman notch's viscosity floor (fieldsplit-Schur multigrid, 1e3 viscosity
contrast) the same defect reads 0.8088 / 0.8062 / 0.8040 at FD steps of 10 / 3 / 1 per
cent — step-independent, as predicted.

Why no test caught it

A small problem's KSP is DIRECT — 1 iteration, residual 1e-15 — so no tolerance binds
and the adjoint is exact whatever the setting. Every existing adjoint test, and five
purpose-built reproducers (shear-thinning; smooth_max floor; pressure-dependent
viscosity; Drucker–Prager + floor; P0 discontinuous pressure) all returned ratios of
0.9986–1.0144 for this reason. The defect needs an iteratively solved problem, which is
every problem anyone actually runs.

And the user cannot work around it

Setting ksp_rtol does not stick, because Eisenstat–Walker overwrites it every step. Five
runs requesting rtol between 1e-12 and 3e-1 all reported rtol 3.00e-01. The tolerance
property documents this for the forward path ("to steer the linear solve via ksp_rtol you
must first switch snes_ksp_ew off"); nothing said it applies to the adjoint, where it is
not a tuning preference but a correctness requirement.

Fix

adjoint_solve(rhs, target=None, rtol=None) now pins the transpose solve:
snes.setUseEW(False), ksp.setTolerances(rtol=_ADJOINT_KSP_RTOL, atol=1e-50, max_it>=2000), and restores the KSP's saved tolerances and EW state in a finally — the
KSP belongs to the SNES and is reused by the next forward solve, so leaving it pinned would
silently make every subsequent Newton step solve far harder than the inexact-Newton design
intends. Default _ADJOINT_KSP_RTOL = 1e-10, overridable per call.

After the fix, the same iterative rig gives 0.999078 with the forward tolerance left at
0.3, and the direct-solve path is unchanged.

Note for anyone who has used this API

Any sensitivity or inversion gradient taken on an iteratively-solved problem before this
fix is short by a fixed fraction — order 15-20% on the cases measured. Gradient directions
are much less affected than magnitudes, so an inversion may well have converged anyway,
but Taylor tests and FD cross-checks on real problems would have failed at the few-per-cent
level and been attributed to discretisation.

Activity

  1. lmoresi commented on Oct 7, 2026

    @lmoresi
    MemberAuthor

    Not reproducible on development (7c9cbbfd) — the adjoint API is not there:

    uw.adjoint present:        False
    Stokes adjoint API:        []
    files with 'adjoint' in the path: 0
    

    Only scattered references remain (one line in petsc_generic_snes_solvers.pyx, five in mcp/__init__.py, eight in transcript_query.py), not the implementation. So this defect lives in unmerged work — feature/discrete-adjoint (#744, currently conflicting and red), feature/adjoint-rotated-bc (#751, based on #744), or scratch/adjoint-merge.

    That is why it has sat untouched: there is nothing on the main line to fix. It is not fixed-in-PR — the opposite. It has to be fixed on the branch before that branch lands, or it lands with the defect.

    Checked in the untouched-issue triage. Blocked behind #744, which needs its conflicts resolved and a CI re-run (its red check is test_1063_constrained_traction, whose timeout #814 addressed when it merged on 2026-10-06).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions