_adjoint_support documents that an unconverged solve is refused ("a linearisation about a state the solve never reached is not the adjoint of anything"), caught after the fact by _record_solve_outcome. That catch is on the recorded/transcript path. A direct solver.solve() followed by solver.gradient(...) is not refused.
Reproduced on scratch/adjoint-merge (82e697b): softmin viscoplastic box, snes_max_it = 2.
forward reason -5 # DIVERGED_MAX_IT
gradient RETURNED {eta_min: 0.2197} # converged answer: -0.1423 (FD agrees to 4e-8)
Wrong sign, no warning. Script: ~/+Simulations/adjoint_note_checks/unconverged_refusal.py.
UWTN 2026-020 (draft) currently states that "the library refuses to take the adjoint of a solve that did not converge", so either the guard moves into adjoint_solve/gradient (check snes.getConvergedReason() > 0 on the solver that did the solving) or the note's sentence goes. The first seems right: the note's argument — a multiplier is undefined off the constraint manifold — is exactly why this should be an error rather than a number.
_adjoint_supportdocuments that an unconverged solve is refused ("a linearisation about a state the solve never reached is not the adjoint of anything"), caught after the fact by_record_solve_outcome. That catch is on the recorded/transcript path. A directsolver.solve()followed bysolver.gradient(...)is not refused.Reproduced on
scratch/adjoint-merge(82e697b): softmin viscoplastic box,snes_max_it = 2.Wrong sign, no warning. Script:
~/+Simulations/adjoint_note_checks/unconverged_refusal.py.UWTN 2026-020 (draft) currently states that "the library refuses to take the adjoint of a solve that did not converge", so either the guard moves into
adjoint_solve/gradient(checksnes.getConvergedReason() > 0on the solver that did the solving) or the note's sentence goes. The first seems right: the note's argument — a multiplier is undefined off the constraint manifold — is exactly why this should be an error rather than a number.