Skip to content

Commit 88ef1ad

Browse files
Compose step-size knobs into CommonControllerOptions; replace DummyController for BDF/JVODE
In v7, `qmin` (alongside `qmax`, `gamma`, `beta1/beta2`, `qsteady_*`, `qoldinit`) moved off `DEOptions` and onto the controller object. The out-of-domain rejection path in `handle_step_rejection!` was still reaching for the old `integrator.opts.qmin`, which throws on the v7 `DEOptions` struct — only the legacy DelayDiffEq constructor still mentions it. The same was true of `integrator.opts.failfactor` in `post_newton_controller!`. Reported in NumericalMathematics/PositiveIntegrators.jl#194 (comment) Rather than papering over with a one-off `hasfield` walk, this lifts the standard step-size knobs into a composable building block and retires the `DummyController` workaround that BDF / Nordsieck were using to keep the knobs on their algorithm structs. A new `CommonControllerOptions{T}` struct holds `qmin`, `qmax`, `qmax_first_step`, `gamma`, `qsteady_min`, `qsteady_max`, `failfactor` — the seven scalars the integrator-level paths actually read. A single type parameter `T` keeps the type signatures simple even if more knobs are added later. All fields default to `nothing`; algorithm-specific defaults are filled in by `resolve_basic` at `setup_controller_cache` time. Concrete controllers (`IController`, `PIController`, `PIDController`, `PredictiveController`, `ExtrapolationController`, `KantorovichTypeController`, plus the new `BDFController` and `JVODEController`) all embed a `CommonControllerOptions` as `controller.basic`. Seven generic accessors — `get_qmin`, `get_qmax`, `get_qmax_first_step`, `get_gamma`, `get_qsteady_min`, `get_qsteady_max`, `get_failfactor` — dispatch on `cache::AbstractControllerCache` and read through `cache.controller.basic`. `CompositeControllerCache` overrides each one to delegate to the active sub-cache. `DummyControllerCache` keeps an alg-field fallback for any SDE algorithm still using it. `integrator.opts.qmin` → `get_qmin(integrator)`, `integrator.opts.failfactor` → `get_failfactor(integrator)`. Same in the BDF post-Newton paths. QNDF/FBDF/DFBDF used to keep `qmax`, `qsteady_min`, `qsteady_max` as fields on the algorithm struct itself, with a `DummyController` hard-wired into `default_controller`. The stepsize logic read `alg.qmax` / `alg.qsteady_min` / `alg.qsteady_max` directly, plus a hard-coded `zₛ = 1.2` magic-number gamma, so the controller surface was unsettable. `BDFController` embeds `CommonControllerOptions` and has a cache that delegates back to alg-level dispatch (the existing BDF order-selection logic is left intact). The hard-coded `zₛ = 1.2` is now `get_gamma(integrator)`. `default_controller(QT, alg::Union{QNDF, FBDF, DFBDF})` threads `alg.qmax` / `alg.qsteady_min` / `alg.qsteady_max` through to the controller, so existing usage like `QNDF(qmax = 20)` keeps working. Users can now also pass `controller = BDFController(qmin = …, gamma = …)` to set knobs that previously had no surface (incl. `qmin` and `gamma`). BDF-tuned per-algorithm defaults (`qmax = 5//1`, `qsteady_min = 9//10`, `qsteady_max = 12//10`, `gamma = 12//10`) are encoded as `qmax_default(::QNDF)` / `gamma_default(::QNDF)` etc. Same pattern. `setη!` / `chooseη!` / `step_accept_controller!(::JVODE)` now read `get_qmin(integrator)` / `get_qmax(integrator)` / `get_qsteady_*(integrator)` instead of `alg.qmin` etc. - `IController` / `PIController` / `PredictiveController` / `PIDController` shed their flat `qmin/qmax/...` fields and embed `CommonControllerOptions` instead. PI-specific knobs (`beta1`, `beta2`, `qoldinit`) and PID-specific knobs (`beta`, `accept_safety`, `limiter`) stay on the controller alongside `basic`. - `ExtrapolationController` and `KantorovichTypeController` likewise embed `CommonControllerOptions`. Their stepsize logic reads `get_qmax(integrator)` / `get_qmin(integrator)` rather than direct field access. `test/runtests.jl` walks transitive `[sources]` dependencies and `Pkg.develop`s them. Pre-seed the `developed` set with the active project so a `[sources]` entry that points back to it (e.g. via the umbrella `OrdinaryDiffEq`'s transitive sources) is skipped — `Pkg.develop` cannot develop the active project itself, and that error was the "package X has the same name or UUID as the active project" failure across the sublibrary CI matrix. Reproducer (`isoutofdomain` predicate that fires once on the first proposed step) plus a smoke test of every controller path (default `solve`, user-supplied `BDFController`, `CommonControllerOptions` construction, controller-composition invariants) — 21/21 pass on Julia 1.12. - On master (without this fix): all algorithms error out — accessing `integrator.opts.qmin` throws because the v7 `DEOptions` struct doesn't have the field. - With this fix: `Tsit5` / `Vern7` / `Rosenbrock23` / `FBDF` / `QNDF` all complete the isout-rejection problem successfully, and `BDFController(qmax = 3)` is honored end-to-end. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent 9b8e7e1 commit 88ef1ad

14 files changed

Lines changed: 573 additions & 254 deletions

File tree

lib/ImplicitDiscreteSolve/src/controller.jl

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,48 @@ The baseline algorithm has been derived in Peter Deuflhard's book "Newton Method
2525
Nonlinear Problems" in Section 5.1.3 (Adaptive pathfollowing algorithms). Please note
2626
that some implementation details deviate from the original algorithm.
2727
"""
28-
Base.@kwdef struct KantorovichTypeController{T} <: AbstractController
28+
struct KantorovichTypeController{B <: OrdinaryDiffEqCore.CommonControllerOptions, T} <: AbstractController
29+
basic::B
2930
Θmin::T
3031
p::Int64
31-
Θreject::T = 0.95
32-
Θbar::T = 0.5
33-
γ::T = 0.95
34-
qmin::T = 1 / 5
35-
qmax::T = 5.0
36-
strict::Bool = true
32+
Θreject::T
33+
Θbar::T
34+
γ::T
35+
strict::Bool
36+
end
37+
38+
function KantorovichTypeController(;
39+
Θmin, p, Θreject = 0.95, Θbar = 0.5, γ = 0.95,
40+
qmin = 1 // 5, qmax = 5, strict = true,
41+
kwargs...,
42+
)
43+
T = promote_type(typeof(Θmin), typeof(Θreject), typeof(Θbar), typeof(γ))
44+
basic = OrdinaryDiffEqCore.CommonControllerOptions(; qmin, qmax, kwargs...)
45+
return KantorovichTypeController{typeof(basic), T}(
46+
basic, T(Θmin), Int64(p), T(Θreject), T(Θbar), T(γ), strict,
47+
)
3748
end
3849

3950
mutable struct KantorovichTypeControllerCache{T, E} <: AbstractControllerCache
40-
controller::KantorovichTypeController{T}
51+
controller::KantorovichTypeController{OrdinaryDiffEqCore.CommonControllerOptions{T}, T}
4152
EEst::E
4253
end
4354

4455
function OrdinaryDiffEqCore.default_controller(
4556
QT, alg::IDSolve,
4657
)
47-
return KantorovichTypeController{QT}(; Θmin = QT(1 // 8), p = 1)
58+
return KantorovichTypeController(; Θmin = QT(1 // 8), p = 1)
4859
end
4960

50-
function OrdinaryDiffEqCore.setup_controller_cache(alg, cache, controller::KantorovichTypeController{T}, ::Type{E}) where {T, E}
51-
return KantorovichTypeControllerCache{T, E}(
52-
controller,
53-
oneunit(E),
61+
function OrdinaryDiffEqCore.setup_controller_cache(alg, cache, controller::KantorovichTypeController, ::Type{E}) where {E}
62+
QT = OrdinaryDiffEqCore._resolved_QT(controller.basic)
63+
basic = OrdinaryDiffEqCore.resolve_basic(controller.basic, alg, QT)
64+
resolved = KantorovichTypeController{typeof(basic), QT}(
65+
basic, QT(controller.Θmin), controller.p,
66+
QT(controller.Θreject), QT(controller.Θbar), QT(controller.γ), controller.strict,
5467
)
68+
T = QT
69+
return KantorovichTypeControllerCache{T, E}(resolved, oneunit(E))
5570
end
5671

5772
function OrdinaryDiffEqCore.stepsize_controller!(
@@ -62,7 +77,8 @@ function OrdinaryDiffEqCore.stepsize_controller!(
6277

6378
# Adapt dt with a priori estimate (Eq. 5.24)
6479
(; Θks) = integrator.cache
65-
(; Θbar, γ, Θmin, qmin, qmax, p) = controller
80+
(; Θbar, γ, Θmin, p) = controller
81+
(; qmin, qmax) = controller.basic
6682

6783
Θ₀ = length(Θks) > 0 ? max(first(Θks), Θmin) : Θmin
6884
q = clamp* (g(Θbar) / (g(Θ₀)))^(1 / p), qmin, qmax)
@@ -84,7 +100,8 @@ function OrdinaryDiffEqCore.step_reject_controller!(
84100

85101
# Shorten dt according to (Eq. 5.24)
86102
(; Θks) = integrator.cache
87-
(; Θbar, Θreject, γ, Θmin, qmin, qmax, p) = controller
103+
(; Θbar, Θreject, γ, Θmin, p) = controller
104+
(; qmin, qmax) = controller.basic
88105
for Θk in Θks
89106
if Θk > Θreject
90107
q = clamp* (g(Θbar) / g(Θk))^(1 / p), qmin, qmax)

lib/OrdinaryDiffEqBDF/src/OrdinaryDiffEqBDF.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ import OrdinaryDiffEqCore: alg_order, calculate_residuals!,
1414
constvalue, isadaptive, error_constant,
1515
has_special_newton_error,
1616
trivial_limiter!,
17-
issplit, qsteady_min_default, qsteady_max_default,
17+
issplit, qmin_default, qmax_default, gamma_default,
18+
qsteady_min_default, qsteady_max_default,
1819
get_current_alg_order, get_current_adaptive_order,
1920
stepsize_controller!,
2021
step_accept_controller!,
2122
step_reject_controller!, post_newton_controller!,
23+
accept_step_controller, get_EEst, set_EEst!,
24+
setup_controller_cache, get_qmax, get_gamma, get_qsteady_min, get_qsteady_max,
25+
get_failfactor, CommonControllerOptions, resolve_basic, _resolved_QT,
26+
AbstractControllerCache,
2227
DAEAlgorithm, _unwrap_val, DummyController,
2328
get_fsalfirstlast, generic_solver_docstring, _ad_chunksize_int, _ad_fdtype, _fixup_ad,
2429
_ode_interpolant, _ode_interpolant!, has_stiff_interpolation,

lib/OrdinaryDiffEqBDF/src/controllers.jl

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,72 @@
1+
"""
2+
BDFController(; qmin, qmax, qsteady_min, qsteady_max, gamma, qmax_first_step,
3+
failfactor)
4+
5+
Step-size controller for the variable-order BDF family (`QNDF`, `FBDF`,
6+
`DFBDF`). Composes the standard step-size knobs via [`CommonControllerOptions`](@ref);
7+
the adaptive logic is integrated into the algorithm itself, so the cache
8+
falls through to alg-level dispatch (the same way the legacy
9+
`DummyControllerCache` did) but exposes the knobs as a real, settable
10+
controller. Pass it explicitly with `solve(prob, alg; controller = BDFController(...))`,
11+
or rely on the default constructed by `default_controller(QT, alg)`.
12+
"""
13+
struct BDFController{B <: CommonControllerOptions} <: AbstractController
14+
basic::B
15+
end
16+
17+
BDFController(; kwargs...) = BDFController(CommonControllerOptions(; kwargs...))
18+
19+
BDFController(alg; kwargs...) = BDFController(Float64, alg; kwargs...)
20+
BDFController(::Type{QT}, alg; kwargs...) where {QT} =
21+
BDFController(resolve_basic(CommonControllerOptions(; kwargs...), alg, QT))
22+
23+
mutable struct BDFControllerCache{T, E, C} <: AbstractControllerCache
24+
controller::BDFController{CommonControllerOptions{T}}
25+
cache::C
26+
EEst::E
27+
end
28+
29+
function setup_controller_cache(
30+
alg::Union{QNDF, FBDF, DFBDF}, cache, controller::BDFController, ::Type{E},
31+
) where {E}
32+
QT = _resolved_QT(controller.basic)
33+
basic = resolve_basic(controller.basic, alg, QT)
34+
resolved = BDFController(basic)
35+
return BDFControllerCache{QT, E, typeof(cache)}(resolved, cache, oneunit(E))
36+
end
37+
38+
# The BDF stepsize/accept/reject logic lives at the algorithm level — the
39+
# controller cache just delegates back, mirroring how DummyControllerCache did.
40+
@inline OrdinaryDiffEqCore.stepsize_controller!(integrator, ::BDFControllerCache, alg) =
41+
stepsize_controller!(integrator, alg)
42+
@inline OrdinaryDiffEqCore.step_accept_controller!(integrator, ::BDFControllerCache, alg, q) =
43+
step_accept_controller!(integrator, alg, q)
44+
@inline OrdinaryDiffEqCore.step_reject_controller!(integrator, ::BDFControllerCache, alg) =
45+
step_reject_controller!(integrator, alg)
46+
@inline OrdinaryDiffEqCore.post_newton_controller!(integrator, ::BDFControllerCache, alg) =
47+
post_newton_controller!(integrator, alg)
48+
@inline OrdinaryDiffEqCore.accept_step_controller(
49+
integrator, cache::BDFControllerCache, alg,
50+
) = get_EEst(cache) <= 1
51+
52+
# Per-algorithm defaults for `CommonControllerOptions` knobs that don't match the
53+
# generic IController defaults. These match the historical alg-struct kwargs
54+
# `QNDF(qmax=5//1, qsteady_min=9//10, qsteady_max=12//10)` etc. and the formerly
55+
# hard-coded `zₛ = 1.2` step-size safety factor in the BDF stepsize logic.
56+
qmax_default(::Union{QNDF, FBDF, DFBDF}) = 5 // 1
57+
qsteady_min_default(::Union{QNDF, QNDF1, QNDF2, FBDF, DFBDF}) = 9 // 10
58+
qsteady_max_default(::Union{QNDF, QNDF1, QNDF2, FBDF, DFBDF}) = 12 // 10
59+
gamma_default(::Union{QNDF, FBDF, DFBDF}) = 12 // 10
60+
161
function default_controller(QT, alg::Union{QNDF, FBDF, DFBDF})
2-
return DummyController()
62+
# Thread the alg-level kwargs through to the CommonControllerOptions so that
63+
# `QNDF(qmax = 20)` keeps working (qmax = 20 ends up on the controller).
64+
return BDFController(
65+
QT, alg;
66+
qmax = alg.qmax,
67+
qsteady_min = alg.qsteady_min,
68+
qsteady_max = alg.qsteady_max,
69+
)
370
end
471

572
# QNDF
@@ -18,15 +85,15 @@ function step_accept_controller!(integrator, cache::Union{QNDFCache, QNDFConstan
1885
cache.consfailcnt = 0
1986
cache.nconsteps += 1
2087
if iszero(OrdinaryDiffEqCore.get_EEst(integrator))
21-
return integrator.dt * get_current_qmax(integrator, alg.qmax)
88+
return integrator.dt * get_current_qmax(integrator, get_qmax(integrator))
2289
else
2390
est = OrdinaryDiffEqCore.get_EEst(integrator)
2491
estₖ₋₁ = cache.EEst1
2592
estₖ₊₁ = cache.EEst2
2693
h = integrator.dt
2794
k = cache.order
2895
prefer_const_step = cache.nconsteps < cache.order + 2
29-
zₛ = 1.2 # equivalent to integrator.opts.gamma
96+
zₛ = get_gamma(integrator)
3097
zᵤ = 0.1
3198
Fᵤ = 10
3299
expo = 1 / (k + 1)
@@ -86,7 +153,7 @@ function step_accept_controller!(integrator, cache::Union{QNDFCache, QNDFConstan
86153
return integrator.dt
87154
end
88155
end
89-
if q <= alg.qsteady_max && q >= alg.qsteady_min
156+
if q <= get_qsteady_max(integrator) && q >= get_qsteady_min(integrator)
90157
return integrator.dt
91158
end
92159
return integrator.dt / q
@@ -100,7 +167,7 @@ function bdf_step_reject_controller!(integrator, cache, EEst1)
100167
if cache.consfailcnt > 1
101168
h = h / 2
102169
end
103-
zₛ = 1.2 # equivalent to integrator.opts.gamma
170+
zₛ = get_gamma(integrator)
104171
expo = 1 / (k + 1)
105172
z = zₛ * ((OrdinaryDiffEqCore.get_EEst(integrator))^expo)
106173
F = inv(z)
@@ -154,7 +221,7 @@ function post_newton_controller!(integrator, cache::Union{FBDFCache, FBDFConstan
154221
if cache.order > 1 && cache.nlsolver.nfails >= 3
155222
cache.order -= 1
156223
end
157-
integrator.dt = integrator.dt / integrator.opts.failfactor
224+
integrator.dt = integrator.dt / get_failfactor(integrator)
158225
cache.consfailcnt += 1
159226
cache.nconsteps = 0
160227
return nothing
@@ -299,7 +366,7 @@ function stepsize_controller!(
299366
end
300367

301368
if iszero(terk)
302-
q = inv(get_current_qmax(integrator, alg.qmax))
369+
q = inv(get_current_qmax(integrator, get_qmax(integrator)))
303370
else
304371
# CVODE-style step size formula: eta = 1 / (BIAS2 * dsm)^(1/(k+1))
305372
# where dsm = terk / (alpha0 * (k+1)) and alpha0 is the BDF leading coefficient.
@@ -320,7 +387,7 @@ function step_accept_controller!(
320387
q
321388
) where {max_order}
322389
cache.consfailcnt = 0
323-
if q <= alg.qsteady_max && q >= alg.qsteady_min
390+
if q <= get_qsteady_max(integrator) && q >= get_qsteady_min(integrator)
324391
q = one(q)
325392
end
326393
cache.nconsteps += 1
@@ -348,7 +415,7 @@ function post_newton_controller!(integrator, cache::Union{DFBDFCache, DFBDFConst
348415
if cache.order > 1 && cache.nlsolver.nfails >= 3
349416
cache.order -= 1
350417
end
351-
integrator.dt = integrator.dt / integrator.opts.failfactor
418+
integrator.dt = integrator.dt / get_failfactor(integrator)
352419
cache.consfailcnt += 1
353420
cache.nconsteps = 0
354421
return nothing
@@ -465,7 +532,7 @@ function stepsize_controller!(
465532
cache.order = k
466533
end
467534
if iszero(terk)
468-
q = inv(get_current_qmax(integrator, alg.qmax))
535+
q = inv(get_current_qmax(integrator, get_qmax(integrator)))
469536
else
470537
# CVODE-style step size formula matching FBDF change
471538
alpha0 = cache.bdf_coeffs[k, 1]
@@ -483,7 +550,7 @@ function step_accept_controller!(
483550
q
484551
) where {max_order}
485552
cache.consfailcnt = 0
486-
if q <= alg.qsteady_max && q >= alg.qsteady_min
553+
if q <= get_qsteady_max(integrator) && q >= get_qsteady_min(integrator)
487554
q = one(q)
488555
end
489556
cache.nconsteps += 1

lib/OrdinaryDiffEqCore/src/alg_utils.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,16 @@ qsteady_max_default(alg) = 1
468468
qsteady_max_default(alg::OrdinaryDiffEqAdaptiveImplicitAlgorithm) = 6 // 5
469469
# But don't re-use Jacobian if not adaptive: too risky and cannot pull back
470470
qsteady_max_default(alg::OrdinaryDiffEqImplicitAlgorithm) = isadaptive(alg) ? 1 // 1 : 0
471+
472+
# qmax_first_step is the upper bound on dt growth on the very first accepted
473+
# step — see https://github.com/SciML/DifferentialEquations.jl/issues/299.
474+
# 10000 mirrors the historical Sundials CVODE behavior.
475+
qmax_first_step_default(alg) = 10000
476+
477+
# failfactor is the post-Newton-failure dt shrink factor used by
478+
# `post_newton_controller!`. Default of 2 matches the historical
479+
# `integrator.opts.failfactor` default.
480+
failfactor_default(alg) = 2
471481
#TODO
472482
#SciMLBase.nlsolve_default(::QNDF, ::Val{κ}) = 1//2
473483

0 commit comments

Comments
 (0)