Skip to content

Fix instability Jacobian diagnostic on in-place problems with an analytic Jacobian - #4191

Draft
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix-4155-instability-jacobian-diagnostic
Draft

Fix instability Jacobian diagnostic on in-place problems with an analytic Jacobian#4191
ChrisRackauckas-Claude wants to merge 2 commits into
SciML:masterfrom
ChrisRackauckas-Claude:fix-4155-instability-jacobian-diagnostic

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Please ignore until reviewed by @ChrisRackauckas.

Closes #4155

What changed and why

get_fresh_jacobian unconditionally delegated to the out-of-place calc_J, which calls f.jac(uprev, p, t). On an in-place problem with an analytic jac!(J, u, p, t) that throws, so the instability diagnostic replaced the retcode = Unstable report with a FunctionWrappersWrappers.NoFunctionWrapperFoundError. Mutable caches now route through calc_J! into a scratch copy of cache.J (constant caches keep using calc_J), preserving the existing njacs restore in both. Separately, the whole Jacobian-analysis block in log_numerical_instability is now guarded: this is best-effort reporting on an already-failed solve, so a failure there degrades to a Jacobian analysis unavailable: <reason> line and the instability warning still prints.

Branch check on Rosenbrock and BDF

The #rosenbrock branch keys off _get_W(integrator), i.e. integrator.W — that field is the SDE noise process, which is nothing for a pure ODE, not the W-matrix. So that branch never fires for an ODE solve and Rosenbrock caches (which do have cache.J) fall through to the get_fresh_jacobian branch. Rodas5P was therefore broken by this bug too, which the tests confirm below. The BDF branch (cache.nlsolver.cache.J) reads an existing matrix and was genuinely unaffected — FBDF passes before and after. I left the dead W.J branch alone rather than widening the diff; it looks like a separate latent bug worth its own issue.

Verification

Base: 9d23061 at the time of the reproduction, rebased onto 4cb8020. Julia 1.12.6.

Failing before (src/ changes stashed, tests kept)

$ git stash push -- lib/
$ julia +1.12 --project=. test/InterfaceIII/instability_diagnostics.jl

in-place analytic Jacobian: Error During Test at test/InterfaceIII/instability_diagnostics.jl:32
  Got exception outside of a @test
  No matching function wrapper was found!
Rodas5P reads its stored Jacobian: Error During Test at test/InterfaceIII/instability_diagnostics.jl:43
  Got exception outside of a @test
  No matching function wrapper was found!
diagnostic does not inflate njacs: Error During Test at test/InterfaceIII/instability_diagnostics.jl:51
  Got exception outside of a @test
  No matching function wrapper was found!
a failing diagnostic still reports the instability: Error During Test at test/InterfaceIII/instability_diagnostics.jl:60
  Got exception outside of a @test
  No matching function wrapper was found!
ERROR: LoadError: Some tests did not pass: 13 passed, 0 failed, 4 errored, 0 broken.

Test Summary:                                        | Pass  Error  Total   Time
Instability diagnostics                              |   13      4     17  49.6s
  full diagnostic without an analytic Jacobian       |    5             5  13.6s
  in-place analytic Jacobian                         |           1      1   7.8s
  out-of-place analytic Jacobian                     |    4             4   4.7s
  Rodas5P reads its stored Jacobian                  |           1      1   9.2s
  FBDF reads its stored Jacobian                     |    4             4   8.6s
  diagnostic does not inflate njacs                  |           1      1   3.2s
  a failing diagnostic still reports the instability |           1      1   1.7s

Passing after

$ git stash pop
$ julia +1.12 --project=. test/InterfaceIII/instability_diagnostics.jl

Test Summary:           | Pass  Total   Time
Instability diagnostics |   28     28  50.0s
exit=0

The guard alone, isolated

With the dispatch fix applied but the try/catch guard reverted, only the guard test fails — confirming the two parts are independently load-bearing:

$ julia +1.12 --project=. test/InterfaceIII/instability_diagnostics.jl

a failing diagnostic still reports the instability: Error During Test at test/InterfaceIII/instability_diagnostics.jl:60
  Got exception outside of a @test
  deliberate diagnostic failure
ERROR: LoadError: Some tests did not pass: 23 passed, 0 failed, 1 errored, 0 broken.

Test Summary:                                        | Pass  Error  Total   Time
  full diagnostic without an analytic Jacobian       |    5             5  10.2s
  in-place analytic Jacobian                         |    4             4   3.7s
  out-of-place analytic Jacobian                     |    4             4   3.4s
  diagnostic does not inflate njacs                  |    2             2   0.7s
  a failing diagnostic still reports the instability |           1      1   3.0s

The diagnostic still works

The no-analytic-Jacobian case still emits all three sections, and the in-place analytic-Jacobian case now emits the identical report instead of throwing:

┌ Warning: Verbosity toggle: dt_epsilon
│  At t=1.0000038457234066, dt was forced below floating point epsilon 2.220446049250313e-16. Aborting. ...
│
│ Diagnostics:
│
│ State Analysis:
│   u[1] = 2.185e+15 has grown >1e6× its initial value
│
│ Jacobian Analysis:
│   row(s) [1] have unusually large entries (e.g. J[1,1] = 2.942e+15), suggesting a singularity in those equation(s)
│   column(s) [1] have unusually large entries, suggesting those state component(s) are diverging
│
│ Error Analysis:
│   step error estimate EEst = 2.278 (a step is accepted when EEst <= 1)
│   largest contributors to EEst = internalnorm(atmp), ...
│     atmp[1] = 2.278, u[1] = 2.185e+15, uprev[1] = 1.471e+15
└ @ SciMLBase .../SciMLBase/YjW4D/src/integrator_interface.jl:1017
retcode = Unstable

Test groups, format, spelling

$ GROUP=InterfaceIII julia +1.12 --project -e 'using Pkg; Pkg.test()'
Test Summary:              | Pass  Total     Time
Derivative Utilities Tests |   11     11  3m54.5s
Test Summary: | Pass  Total   Time
stats Tests   |   14     14  48.1s
Test Summary:  | Pass  Total     Time
No Index Tests |   24     24  2m37.2s
Test Summary:               | Pass  Total     Time
Events + DAE addsteps Tests |    5      5  2m12.9s
Test Summary: | Pass  Broken  Total     Time
Units Tests   |   52       4     56  4m15.6s
Test Summary:                 | Pass  Total     Time
Instability Diagnostics Tests |   28     28  1m46.0s
Test Summary:     | Pass  Total     Time
DEVerbosity Tests |  156    156  1m45.0s
exit=0

$ GROUP=QA julia +1.12 --project -e 'using Pkg; Pkg.test()'
public API has docstrings: Test Failed at SciMLTesting.jl:1205
  Expression: isempty(undocumented)
   Evaluated: isempty([:SciMLBase, :SciMLLogging])
Test Summary:                           | Pass  Fail  Total   Time
Quality Assurance Tests                 |   87     1     88  14.9s
exit=1

Runic 1.5.1 and typos are clean on every touched file.

What I did NOT verify

  • GPU, Downstream, ModelingToolkit, and ODEInterfaceRegression groups.
  • The docs build — this PR adds no public API and no rendered docstring.
  • The SDE path through StochasticDiffEq, where integrator.W is an actual noise process.
  • Sparse and structured jac_prototypes through the new copy(cache.J) scratch. copy was chosen over similar/zero precisely to preserve sparsity structure, and calc_J! already runs prepare_sparse_jac! for CSC prototypes, but I have no test covering an unstable sparse-Jacobian solve.

Worth pushing back on

  • copy(cache.J) allocates a fresh Jacobian on the failure path. Writing into cache.J directly would avoid it, since the cache is discarded immediately after; I chose the copy so the diagnostic has no side effect on solver state.
  • The guard is a try/catch around _analyze_instability_jacobian only, rethrowing InterruptException. It does not wrap the state or error-estimate analysis.
  • The W.J dead-branch finding above — I did not change it, but someone should decide whether it was meant to read the W-matrix.

The QA failure is pre-existing on master and unrelated to this PR. I reran GROUP=QA on a detached checkout of 4cb8020 with no changes and got the identical isempty([:SciMLBase, :SciMLLogging]) failure (87 passed, 1 failed, 16.4s). A separate agent is bisecting it.

Logging is added to [extras]/[targets] because Pkg.test's restricted LOAD_PATH does not expose unlisted stdlibs, and the new test uses Test.TestLogger + with_logger to capture the warning text.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

CI note: PositiveIntegrators.jl/Downstream/1.10 fails in 20s with ERROR: LoadError: Unsatisfiable requirements detected for package OrdinaryDiffEqRosenbrock [43230ef6]. That is a resolver failure, not a test failure, and it is pre-existing — the same job is failure on the base commit 4cb8020:

$ gh api repos/SciML/OrdinaryDiffEq.jl/commits/4cb8020/check-runs --jq '.check_runs[] | select(.name|test("PositiveIntegrators")) | "\(.name) \(.conclusion)"'
PositiveIntegrators.jl/Downstream/1.10 failure

Nothing in this PR touches a compat bound. Format and spelling are green:

Runic / Runic Format Check                    pass  1m4s
runic-suggestions / Runic Suggestions         pass  1m1s
Spell Check with Typos / Spell Check with Typos  pass  6s

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Second CI note. Every red check so far is pre-existing, none of it touches this change:

Check Status here Pre-existing?
PositiveIntegrators.jl/Downstream/1.10 fail (20s, resolver) yes — failure on base 4cb8020
downgrade-sublibraries / test (lib/{OrdinaryDiffEqBDF,OrdinaryDiffEqFIRK,OrdinaryDiffEqExtrapolation,ImplicitDiscreteSolve}) fail yes — all four are failure on base 4cb8020, alongside ~20 other downgrade jobs
tests / InterfaceV (julia lts) fail yes — Illegal conversion of a JLArray to a Ptr in getrf!; failure on #4176, #4158 and #4148 too

The GROUP=QA failure I reported locally (isempty([:SciMLBase, :SciMLLogging])) is also pre-existing and external: it is a SciMLTesting v2.6.1→v2.6.2 change, fixed upstream in SciML/SciMLTesting.jl#45 but not yet registered. Tracked in #4194.

Green so far and relevant to this PR: Runic, runic-suggestions, Spell Check, sublibrary-ci / lib/OrdinaryDiffEqCore / Julia lts / Tests - Core, tests / ModelingToolkit (julia lts). tests / InterfaceIII — the group carrying the new tests — is still running on all three Julia versions.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Third CI note — the group this PR actually changes is green on all three Julia versions:

tests / InterfaceIII (julia 1,   ubuntu-latest)  pass
tests / InterfaceIII (julia lts, ubuntu-latest)  pass
tests / InterfaceIII (julia pre, ubuntu-latest)  pass

Also green: Documentation / Build and Deploy Documentation, sublibrary-ci / lib/OrdinaryDiffEqDifferentiation [QA], sublibrary-ci / lib/OrdinaryDiffEqCore / Julia lts / Tests - Core, Runic, runic-suggestions, Spell Check, plus InterfaceI/II/IV, AlgConvergence_I/III, Regression_I, ModelingToolkit and Downstream.

Remaining reds, all verified pre-existing:

`get_fresh_jacobian` unconditionally delegated to the out-of-place `calc_J`,
so on an in-place problem with an analytic `jac!` the diagnostic called
`f.jac(uprev, p, t)` and threw instead of reporting the instability. Route
mutable caches through `calc_J!` into a scratch copy of `cache.J`, and guard
the whole Jacobian-analysis block so a diagnostic failure degrades to
"Jacobian analysis unavailable" rather than converting a `retcode = Unstable`
return into a thrown exception.

Closes SciML#4155

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude
ChrisRackauckas-Claude force-pushed the fix-4155-instability-jacobian-diagnostic branch from 96fa972 to 728b074 Compare August 9, 2026 10:25
@ChrisRackauckas

Copy link
Copy Markdown
Member

@Shreyas-Ekanathan

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.

Instability logging throws on in-place problems with an analytic Jacobian: get_fresh_jacobian calls out-of-place calc_J

2 participants