Skip to content

Commit 7a24684

Browse files
ChrisRackauckas-ClaudeChrisRackauckasnathanaelboschclaude
authored
Handle OrdinaryDiffEq v7 / DiffEqBase v7 / OrdinaryDiffEqCore v4 breaking changes (#410)
* Handle OrdinaryDiffEq v7 / DiffEqBase v7 / OrdinaryDiffEqCore v4 breaking changes Bump compats to the new major versions (matching CompatHelper PRs #407, #408, #409) and gate the OrdinaryDiffEqCore internals we extend so the package continues to build against both OrdinaryDiffEqCore v3 and v4. v4 removed a handful of internal hooks that ProbNumDiffEq relied on: - `OrdinaryDiffEqCore._default_dae_init!(integrator, prob, x, alg)` was deleted along with the per-solver DAE-init dispatch; initialization now routes through `_initialize_dae!(integrator, prob, alg::DefaultInit, x)` and the default algorithm switched from `BrownFullBasicInit` to `CheckInit`. Restore the prior behaviour for `AbstractEK` by overriding `_initialize_dae!` for our integrator type on v4. - `OrdinaryDiffEqCore.isstandard` / `ispredictive` traits were removed; PI control is now the default via `default_controller`, so the trait overrides are only needed on v3. - `integrator.EEst` was removed from the `ODEIntegrator` struct and replaced with `OrdinaryDiffEqCore.get_EEst` / `set_EEst!` accessors backed by `integrator.controller_cache`. Wrap both call sites in `perform_step!` with helpers that pick the right API per version. - `OrdinaryDiffEqCore._process_AD_choice` is gone on v4 (the `chunk_size` / `diff_type` / `standardtag` solver kwargs were removed in favour of `AutoForwardDiff(chunksize=…)` / `AutoFiniteDiff(fdtype=…)`). Fall back to an inlined equivalent so `EK1` / `DiagonalEK1` keep accepting the legacy kwargs on both versions. See the OrdinaryDiffEq v7 NEWS.md for the full list of breaking changes. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> * JuliaFormatter * Fix v4/v7 stack CI failures: scope reduction, RAT v4, Bool autodiff, DiffEqDevTools v4 Four distinct failures surfaced once CI started resolving the v4/v7 stack (which became possible after the gating deps registered in General). 1. UndefVarError: RadauIIA5. OrdinaryDiffEq v7's package scope reduction means `using OrdinaryDiffEq` no longer re-exports the full solver suite. Add `using OrdinaryDiffEqFIRK: RadauIIA5` in test/stiff_problem.jl and test/mass_matrix.jl, plus the dep UUID in test/Project.toml. 2. length(sol) returned the wrong value at test/solution.jl:29,96. RecursiveArrayTools v4 changed length(sol) from "number of timesteps" to prod(size(sol)) since AbstractVectorOfArray now subtypes AbstractArray. Switch to length(sol.u) per the v7 NEWS migration. 3. MethodError: prepare_ADType(::Bool, ...) when EK1 or DiagonalEK1 was constructed with the legacy autodiff=true or autodiff=false kwarg. The v4 fallback for _process_AD_choice in src/algorithms.jl only handled AutoForwardDiff and AutoFiniteDiff; the v3 OrdinaryDiffEqCore version it replaced also converted Bool to an ADType. Mirror that conversion so the Bool no longer propagates as the AD type parameter on the algorithm. 4. ArgumentError: Passing a Bool for verbose, raised by DiffEqDevTools v3's WorkPrecisionSet on test/diffeqdevtools.jl:79. DiffEqDevTools v4 removed the offending kwarg. Widen Project.toml compat to "2.46, 3, 4" so the resolver picks v4 on the v4/v7 stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Switch default DAE init to CheckInit; drop verbose=false from broken WP test Two changes: 1. src/algorithms.jl: replace the AbstractEK DAE-init override with one that routes through CheckInit on both stacks. Matches the OrdinaryDiffEq v7 / DifferentialEquations v8 default (errors on inconsistent ICs instead of silently correcting them). Users who want the old auto-fix behavior must now pass initializealg=BrownFullBasicInit() explicitly. On v4 the per-alg hook is gone and CheckInit is already the default, so the override is only needed on v3. Verified locally against test/mass_matrix.jl on the v4/v7 stack: both UniformScaling and Robertson mass-matrix tests pass with consistent ICs. 2. test/diffeqdevtools.jl: drop verbose=false from the two WorkPrecisionSet calls in the "is broken" testset. DiffEqDevTools v3 forwards verbose as a Bool which OrdinaryDiffEq v7 rejects (DEVerbosity / SciMLLogging required). DiffEqDevTools v4 removed the kwarg, but v4.0.0 is yanked in General, so we drop the kwarg test-side. On v3/v6 this only removes a deprecation warning the test was already eating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address code review feedback: fix AutoFiniteDiff regression, trim comments Two functional changes in src/algorithms.jl: - Drop the stray em-dash from the `_process_AD_choice` overview comment to comply with the project's no-em-dashes style. - Guard the `AutoFiniteDiff` branch of the v4 `_process_AD_choice` fallback to only rebuild the ADType when the user passed a non-default legacy `diff_type` kwarg. The previous version unconditionally rebuilt with `fdtype=diff_type`, which silently dropped a user's pre-configured `AutoFiniteDiff(fdtype=Val(:central))` (or similar) when they didn't also set the legacy `diff_type`. Mirrors v3's behavior. Comment trim across src/algorithms.jl and src/alg_utils.jl: the `_alg_autodiff` block, the `_process_AD_choice` overview, the Bool-branch inline comment, and the DAE-init block are all condensed without losing the migration rationale. Saves ~10 lines in the compat-shim region. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Nathanael Bosch <nathanael.bosch@epfl.ch> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d58e2f5 commit 7a24684

9 files changed

Lines changed: 105 additions & 33 deletions

File tree

Project.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ RecipesBaseExt = "RecipesBase"
5353
ADTypes = "1.16"
5454
ArrayAllocators = "0.3"
5555
BlockArrays = "1"
56-
DiffEqBase = "6.194"
56+
DiffEqBase = "6.194, 7"
5757
DiffEqCallbacks = "4"
58-
DiffEqDevTools = "2.46"
58+
DiffEqDevTools = "2.46, 3, 4"
5959
DocStringExtensions = "0.9.4"
6060
FastBroadcast = "0.3.5, 1"
6161
FillArrays = "1.9"
@@ -66,10 +66,10 @@ Kronecker = "0.5.4"
6666
LinearAlgebra = "1"
6767
MatrixEquations = "2"
6868
Octavian = "0.3.29"
69-
OrdinaryDiffEqCore = "3"
70-
OrdinaryDiffEqDifferentiation = "2"
71-
OrdinaryDiffEqRosenbrock = "1.23"
72-
OrdinaryDiffEqVerner = "1.10"
69+
OrdinaryDiffEqCore = "3, 4"
70+
OrdinaryDiffEqDifferentiation = "2, 3"
71+
OrdinaryDiffEqRosenbrock = "1.23, 2"
72+
OrdinaryDiffEqVerner = "1.10, 2"
7373
PSDMatrices = "0.5"
7474
PrecompileTools = "1.2.1"
7575
Printf = "1"

src/alg_utils.jl

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
# https://github.com/SciML/OrdinaryDiffEqCore.jl/blob/master/src/alg_utils.jl
44
############################################################################################
55

6-
OrdinaryDiffEqDifferentiation._alg_autodiff(::AbstractEK) = Val{true}()
6+
# v3 reaches autodiff settings via the private `_alg_autodiff`; v4 promoted
7+
# `alg_autodiff` to the public entry point and deleted `_alg_autodiff`. Define both:
8+
# the `_alg_autodiff` override is gated so v4 doesn't see it, and `alg_autodiff` is
9+
# harmlessly redefined on v3 (an existing function gets a new method).
10+
@static if isdefined(OrdinaryDiffEqDifferentiation, :_alg_autodiff)
11+
OrdinaryDiffEqDifferentiation._alg_autodiff(::AbstractEK) = Val{true}()
12+
end
13+
OrdinaryDiffEqDifferentiation.alg_autodiff(::AbstractEK) = ADTypes.AutoForwardDiff()
714
OrdinaryDiffEqDifferentiation.standardtag(::AbstractEK) = false
815
OrdinaryDiffEqDifferentiation.concrete_jac(::AbstractEK) = nothing
916

@@ -12,8 +19,11 @@ OrdinaryDiffEqDifferentiation.concrete_jac(::AbstractEK) = nothing
1219
OrdinaryDiffEqCore.isfsal(::AbstractEK) = false
1320

1421
for ALG in [:EK1, :DiagonalEK1]
15-
@eval OrdinaryDiffEqDifferentiation._alg_autodiff(alg::$ALG{CS,AD}) where {CS,AD} =
16-
alg.autodiff
22+
@static if isdefined(OrdinaryDiffEqDifferentiation, :_alg_autodiff)
23+
@eval OrdinaryDiffEqDifferentiation._alg_autodiff(alg::$ALG{CS,AD}) where {CS,AD} =
24+
alg.autodiff
25+
end
26+
@eval OrdinaryDiffEqDifferentiation.alg_autodiff(alg::$ALG) = alg.autodiff
1727
@eval OrdinaryDiffEqDifferentiation.alg_difftype(
1828
::$ALG{CS,AD,DiffType},
1929
) where {CS,AD,DiffType} =
@@ -35,9 +45,13 @@ OrdinaryDiffEqCore.isadaptive(::AbstractEK) = true
3545
OrdinaryDiffEqCore.alg_order(alg::AbstractEK) = num_derivatives(alg.prior)
3646
# OrdinaryDiffEqCore.alg_adaptive_order(alg::AbstractEK) =
3747

38-
# PI control is the default!
39-
OrdinaryDiffEqCore.isstandard(::AbstractEK) = false # proportional
40-
OrdinaryDiffEqCore.ispredictive(::AbstractEK) = false # not sure, maybe Gustafsson acceleration?
48+
# PI control is the default. On OrdinaryDiffEqCore v3 we have to explicitly set the
49+
# `isstandard`/`ispredictive` traits to false; on v4 those traits were removed and the
50+
# default is PI already (via `default_controller`), so we only opt in on the old version.
51+
@static if isdefined(OrdinaryDiffEqCore, :isstandard)
52+
OrdinaryDiffEqCore.isstandard(::AbstractEK) = false # proportional
53+
OrdinaryDiffEqCore.ispredictive(::AbstractEK) = false # not sure, maybe Gustafsson acceleration?
54+
end
4155

4256
# OrdinaryDiffEqCore.qmin_default(alg::AbstractEK) =
4357
# OrdinaryDiffEqCore.qmax_default(alg::AbstractEK) =

src/algorithms.jl

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,49 @@
33
########################################################################################
44
abstract type AbstractEK <: OrdinaryDiffEqCore.OrdinaryDiffEqAdaptiveAlgorithm end
55

6+
# `_process_AD_choice` normalizes the legacy `(autodiff, chunk_size, diff_type)`
7+
# kwargs into an `ADTypes.AbstractADType`. v3's OrdinaryDiffEqCore exports this
8+
# helper; v4 (OrdinaryDiffEq v7) removed it along with the legacy kwargs. We keep
9+
# accepting the kwargs on EK1/DiagonalEK1 by delegating to OrdinaryDiffEqCore on v3
10+
# and inlining an equivalent shim on v4.
11+
@static if isdefined(OrdinaryDiffEqCore, :_process_AD_choice)
12+
_process_AD_choice(autodiff, chunk_size, diff_type) =
13+
OrdinaryDiffEqCore._process_AD_choice(autodiff, chunk_size, diff_type)
14+
else
15+
function _process_AD_choice(autodiff, chunk_size, diff_type)
16+
ad = autodiff
17+
if ad isa Bool
18+
# Mirror v3: `true` → AutoForwardDiff(chunksize=chunk_size), `false` →
19+
# AutoFiniteDiff(fdtype=diff_type, dir=1). Without this a Bool would
20+
# propagate as EK1's AD type parameter and break `AbstractADType` dispatch.
21+
if ad
22+
cs_int = _unwrap_val(chunk_size)
23+
cs = cs_int == 0 ? nothing : cs_int
24+
ad = ADTypes.AutoForwardDiff(; chunksize=cs)
25+
else
26+
fdtype = diff_type isa Val ? diff_type : Val(diff_type)
27+
ad = ADTypes.AutoFiniteDiff(; fdtype=fdtype, dir=1)
28+
end
29+
elseif ad isa ADTypes.AutoForwardDiff
30+
cs_int = _unwrap_val(chunk_size)
31+
# If the user set an explicit chunk_size kwarg, fold it into the ADType.
32+
# Otherwise defer to whatever chunksize is already on `ad`.
33+
if cs_int != 0
34+
ad = ADTypes.AutoForwardDiff(; chunksize=cs_int, tag=ad.tag)
35+
end
36+
elseif ad isa ADTypes.AutoFiniteDiff
37+
# Only override the ADType's fdtype if the user passed a non-default
38+
# legacy `diff_type` kwarg; otherwise preserve their pre-configured
39+
# AutoFiniteDiff (matching v3's behavior).
40+
fdtype = diff_type isa Val ? diff_type : Val(diff_type)
41+
if fdtype !== Val(:forward)
42+
ad = ADTypes.AutoFiniteDiff(; fdtype=fdtype)
43+
end
44+
end
45+
return (ad, chunk_size, diff_type)
46+
end
47+
end
48+
649
# Tell SciMLBase that EK1/DiagonalEK1 use ForwardDiff on the model function,
750
# so that FunctionWrappersWrapper registers Dual-compatible wrappers.
851
function SciMLBase.forwarddiffs_model(alg::AbstractEK)
@@ -13,19 +56,21 @@ function SciMLBase.forwarddiffs_model(alg::AbstractEK)
1356
return false
1457
end
1558

16-
# Newer OrdinaryDiffEqCore dispatches _default_dae_init! by algorithm type, but only
17-
# extends it for its own implicit algorithm types in OrdinaryDiffEqNonlinearSolve.
18-
# Replicate the same behavior here so that DAE initialization (BrownFullBasicInit)
19-
# works for singular mass matrices, as it did before the refactor.
20-
function OrdinaryDiffEqCore._default_dae_init!(integrator, prob, x, alg::AbstractEK)
21-
initializealg = DiffEqBase.BrownFullBasicInit(integrator.opts.abstol)
22-
if applicable(OrdinaryDiffEqCore._initialize_dae!, integrator, prob, initializealg, x)
23-
OrdinaryDiffEqCore._initialize_dae!(integrator, prob, initializealg, x)
24-
else
25-
error(
26-
"`OrdinaryDiffEqNonlinearSolve` is not loaded, which is required for " *
27-
"DAE initialization with singular mass matrices. " *
28-
"To fix this, do `using OrdinaryDiffEqNonlinearSolve` or `using OrdinaryDiffEq`.",
59+
# DAE-initialization hook.
60+
#
61+
# v3's `_default_dae_init!` is only extended for OrdinaryDiffEq's own implicit
62+
# algorithms (in OrdinaryDiffEqNonlinearSolve), so without an override the EK
63+
# solvers hit a MethodError on DAE / singular-mass-matrix problems. Route through
64+
# `CheckInit` to match the OrdinaryDiffEq v7 default; users who want the previous
65+
# silent-fix behavior pass `initializealg = BrownFullBasicInit()` explicitly.
66+
# v4 dropped the hook and `CheckInit` is already its `DefaultInit` target.
67+
@static if isdefined(OrdinaryDiffEqCore, :_default_dae_init!)
68+
function OrdinaryDiffEqCore._default_dae_init!(integrator, prob, x, alg::AbstractEK)
69+
return OrdinaryDiffEqCore._initialize_dae!(
70+
integrator,
71+
prob,
72+
DiffEqBase.CheckInit(),
73+
x,
2974
)
3075
end
3176
end
@@ -226,7 +271,7 @@ struct EK1{CS,AD,DiffType,ST,CJ,PT,DT,IT,RT,CF} <: AbstractEK
226271
) where {PT,DT,IT,RT,CF} = begin
227272
ekargcheck(EK1; diffusionmodel, pn_observation_noise, covariance_factorization)
228273
AD_choice, chunk_size, diff_type =
229-
OrdinaryDiffEqCore._process_AD_choice(autodiff, chunk_size, diff_type)
274+
_process_AD_choice(autodiff, chunk_size, diff_type)
230275
new{
231276
_unwrap_val(chunk_size),
232277
typeof(AD_choice),
@@ -323,7 +368,7 @@ struct DiagonalEK1{CS,AD,DiffType,ST,CJ,PT,DT,IT,RT,CF} <: AbstractEK
323368
) where {PT,DT,IT,RT,CF} = begin
324369
ekargcheck(DiagonalEK1; diffusionmodel, pn_observation_noise, covariance_factorization)
325370
AD_choice, chunk_size, diff_type =
326-
OrdinaryDiffEqCore._process_AD_choice(autodiff, chunk_size, diff_type)
371+
_process_AD_choice(autodiff, chunk_size, diff_type)
327372
new{
328373
_unwrap_val(chunk_size),
329374
typeof(AD_choice),

src/perform_step.jl

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# On OrdinaryDiffEqCore v4+ the local error estimate `EEst` lives on `integrator.controller_cache`
2+
# and must be accessed through `get_EEst`/`set_EEst!`. On v3 it was a mutable field on the
3+
# integrator struct itself. These helpers paper over that difference.
4+
@static if isdefined(OrdinaryDiffEqCore, :set_EEst!)
5+
_set_EEst!(integ, val) = OrdinaryDiffEqCore.set_EEst!(integ, val)
6+
_get_EEst(integ) = OrdinaryDiffEqCore.get_EEst(integ)
7+
else
8+
_set_EEst!(integ, val) = (integ.EEst = val)
9+
_get_EEst(integ) = integ.EEst
10+
end
11+
112
# Called in the OrdinaryDiffEqCore.__init; All `OrdinaryDiffEqAlgorithm`s have one
213
function OrdinaryDiffEqCore.initialize!(
314
integ::OrdinaryDiffEqCore.ODEIntegrator,
@@ -100,8 +111,9 @@ function OrdinaryDiffEqCore.perform_step!(integ, cache::EKCache, repeat_step=fal
100111
cache.local_diffusion = estimate_local_diffusion(cache.diffusionmodel, integ)
101112
end
102113
if integ.opts.adaptive
103-
integ.EEst = compute_scaled_error_estimate!(integ, cache)
104-
if integ.EEst >= one(integ.EEst)
114+
_set_EEst!(integ, compute_scaled_error_estimate!(integ, cache))
115+
_EEst = _get_EEst(integ)
116+
if _EEst >= one(_EEst)
105117
return
106118
end
107119
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ LinearRegression = "92481ed7-9fb7-40fd-80f2-46fd0f076581"
1414
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
1515
ODEProblemLibrary = "fdc4e326-1af4-4b90-96e7-779fcce2daa5"
1616
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
17+
OrdinaryDiffEqFIRK = "5960d6e9-dd7a-4743-88e7-cf307b64f125"
1718
OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8"
1819
ParameterizedFunctions = "65888b18-ceab-5e60-b2b9-181511a3b968"
1920
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

test/diffeqdevtools.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ end
9191
numruns=2,
9292
maxiters=Int(1e7),
9393
timeseries_errors=false,
94-
verbose=false,
9594
)
9695
@test_nowarn WorkPrecisionSet(
9796
prob, abstols, reltols, setups;
@@ -101,6 +100,5 @@ end
101100
numruns=2,
102101
maxiters=Int(1e7),
103102
timeseries_errors=false,
104-
verbose=false,
105103
)
106104
end

test/mass_matrix.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ProbNumDiffEq
22
using OrdinaryDiffEq
3+
using OrdinaryDiffEqFIRK: RadauIIA5
34
using LinearAlgebra
45
using Test
56

test/solution.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ using ODEProblemLibrary: prob_ode_lotkavolterra
2626
@testset "Alg $Alg" for Alg in (EK0, EK1)
2727
sol = solve(prob, Alg())
2828

29-
@test length(sol) > 2
29+
@test length(sol.u) > 2
3030
@test length(sol.t) == length(sol.u)
3131
@test length(prob.u0) == length(sol.u[end])
3232

@@ -93,7 +93,7 @@ using ODEProblemLibrary: prob_ode_lotkavolterra
9393
@test samples isa Array
9494

9595
m, n, o = size(samples)
96-
@test m == length(sol)
96+
@test m == length(sol.u)
9797
@test n == length(sol.u[1])
9898
@test o == n_samples
9999

test/stiff_problem.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using ProbNumDiffEq
22
using Test
33
using OrdinaryDiffEq
4+
using OrdinaryDiffEqFIRK: RadauIIA5
45
using ODEProblemLibrary: prob_ode_vanderpol_stiff
56

67
prob = prob_ode_vanderpol_stiff

0 commit comments

Comments
 (0)