Skip to content

Commit 5b711c4

Browse files
Fix downstream tolerance failures: reuse J only, always rebuild W
- Disable Jacobian reuse for mass-matrix (DAE) problems to avoid order reduction from stale algebraic constraint derivatives (addresses @gstein3m's review comment) - Change reuse strategy from (false,false) to (false,true): reuse the cached Jacobian but always rebuild W = J − M/(dt·γ) with the current dtgamma. The Jacobian evaluation (finite-diff/AD) is the expensive part; W construction + LU factorization is comparatively cheap and keeps step control accurate. - Remove stale OrdinaryDiffEqNonlinearSolve from runtime [deps] in OrdinaryDiffEqRosenbrock (fixes Aqua stale dependencies QA test) Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 06d77a4 commit 5b711c4

2 files changed

Lines changed: 25 additions & 14 deletions

File tree

lib/OrdinaryDiffEqDifferentiation/src/derivative_utils.jl

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@ get_jac_reuse(cache) = hasproperty(cache, :jac_reuse) ? cache.jac_reuse : nothin
1212
_rosenbrock_jac_reuse_decision(integrator, cache, dtgamma) -> Union{Nothing, NTuple{2,Bool}}
1313
1414
Decide whether to recompute the Jacobian and/or W matrix for Rosenbrock methods.
15-
For W-methods (where `isWmethod(alg) == true`), implements CVODE-inspired reuse:
15+
For W-methods (where `isWmethod(alg) == true`) on non-DAE problems, implements
16+
CVODE-inspired Jacobian reuse:
1617
- Always recompute on first iteration
17-
- Recompute after step rejection (EEst > 1), since the old W wasn't good enough
18+
- Recompute after step rejection (EEst > 1), since the old J wasn't good enough
1819
- Recompute when gamma ratio changes too much: |dtgamma/last_dtgamma - 1| > 0.3
1920
- Recompute every `max_jac_age` accepted steps (default 50)
2021
- Recompute when u_modified (callback modification)
21-
- Otherwise reuse both J and W (including LU factorization). The LU is the
22-
expensive part, so we try the old W and only recompute if the step fails.
22+
- Otherwise reuse J but rebuild W from the cached J and current dtgamma.
23+
The Jacobian evaluation (finite-diff / AD) is the expensive part; W
24+
construction and LU factorization are comparatively cheap.
2325
24-
For strict Rosenbrock methods, returns `nothing` to delegate to `do_newJW`.
26+
Returns `nothing` (delegate to `do_newJW`) for strict Rosenbrock methods,
27+
linear problems, and mass-matrix (DAE) problems where stale Jacobians
28+
cause order reduction.
2529
"""
2630
function _rosenbrock_jac_reuse_decision(integrator, cache, dtgamma)
2731
alg = OrdinaryDiffEqCore.unwrap_alg(integrator, true)
@@ -43,6 +47,14 @@ function _rosenbrock_jac_reuse_decision(integrator, cache, dtgamma)
4347
return nothing
4448
end
4549

50+
# Mass matrix (DAE) problems: delegate to do_newJW.
51+
# Stale Jacobians cause order reduction for DAEs because the algebraic
52+
# constraint derivatives must remain accurate. See Steinebach (2024)
53+
# for W-method DAE order conditions.
54+
if integrator.f.mass_matrix !== I
55+
return nothing
56+
end
57+
4658
# First iteration: always compute J and W.
4759
if integrator.iter <= 1
4860
return (true, true)
@@ -102,9 +114,11 @@ function _rosenbrock_jac_reuse_decision(integrator, cache, dtgamma)
102114
return (true, true)
103115
end
104116

105-
# Reuse both J and W. The LU factorization is expensive, so try with
106-
# the old W and only recompute if the step is rejected (caught above).
107-
return (false, false)
117+
# Reuse J but rebuild W with the current dtgamma. Following CVODE's
118+
# approach: the Jacobian evaluation (finite-diff / AD) is expensive,
119+
# while reconstructing W = J − M/(dt·γ) and its LU is comparatively
120+
# cheap and keeps the step controller accurate.
121+
return (false, true)
108122
end
109123

110124
function calc_tderivative!(integrator, cache, dtd1, repeat_step)
@@ -893,10 +907,9 @@ function calc_rosenbrock_differentiation(integrator, cache, dtgamma, repeat_step
893907
jac_reuse.last_u_length = length(integrator.u)
894908
end
895909

896-
# Build and cache W, or reuse cached W (including LU factorization).
897-
# Reusing old W (with old dtgamma) mirrors IIP behavior: if the old W
898-
# is too inaccurate, the step will be rejected and EEst > 1 triggers
899-
# a fresh J+W computation on the retry.
910+
# Always rebuild W from the (possibly cached) J and the current dtgamma.
911+
# The Jacobian evaluation is the expensive part; W = J − M/(dt·γ) and
912+
# its factorization are comparatively cheap and keep step control accurate.
900913
if new_W
901914
W = J - mass_matrix * inv(dtgamma)
902915
if !isa(W, Number)

lib/OrdinaryDiffEqRosenbrock/Project.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1616
MuladdMacro = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221"
1717
OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8"
1818
OrdinaryDiffEqDifferentiation = "4302a76b-040a-498a-8c04-15b101fed76b"
19-
OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8"
2019
Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588"
2120
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
2221
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
@@ -28,7 +27,6 @@ Static = "aedffcd0-7271-4cad-89d0-dc628f76c6d3"
2827
[sources]
2928
OrdinaryDiffEqCore = {path = "../OrdinaryDiffEqCore"}
3029
OrdinaryDiffEqDifferentiation = {path = "../OrdinaryDiffEqDifferentiation"}
31-
OrdinaryDiffEqNonlinearSolve = {path = "../OrdinaryDiffEqNonlinearSolve"}
3230

3331
[compat]
3432
ADTypes = "1.16"

0 commit comments

Comments
 (0)