Skip to content

Commit 226dfbb

Browse files
Add GlobalDefectCorrection global error estimation and control
Implements solving-for-the-correction (SciML/GlobalDiffEq.jl#3 family; Zadunaisky 1976, Dormand-Duckers-Prince 1984, Dormand-Lockyer- McGorrigan-Prince 1989, validity under adaptive stepping by Calvo-Higham-Montijano-Randez 1996): the nonlinear companion ODE ε' = f(P + ε) - P' driven by the dense-output defect, requiring no Jacobian. Same estimator and gtol-control API as GlobalErrorTransport via the shared companion machinery. Verified locally on Lotka-Volterra vs a 1e-13 reference: estimate to true-error ratios 0.998-1.000 at tolerances 1e-4/1e-6, and gtol=1e-7 control yields 9.9e-9 final error. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent 319106a commit 226dfbb

6 files changed

Lines changed: 153 additions & 11 deletions

File tree

docs/src/globalerrorcontrol/GlobalDiffEq.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,32 @@ using GlobalDiffEq
2525
- To *control* the endpoint global error to a tolerance `gtol`, wrap any
2626
adaptive solver in [`GlobalErrorTransport`](@ref) (linearized
2727
error-transport equation, Jacobian-vector products via automatic
28-
differentiation) or [`GlobalAdjoint`](@ref) (adjoint-based, for endpoint
29-
functionals; requires SciMLSensitivity and QuadGK to be loaded). Each
30-
solves the problem, estimates the endpoint global error, and tightens the
31-
local tolerances until the requested global tolerance is met.
28+
differentiation), [`GlobalDefectCorrection`](@ref) (solving for the
29+
correction; no Jacobian needed), or [`GlobalAdjoint`](@ref) (adjoint-based,
30+
for endpoint functionals; requires SciMLSensitivity and QuadGK to be
31+
loaded). Each solves the problem, estimates the endpoint global error, and
32+
tightens the local tolerances until the requested global tolerance is met.
3233
- [`GlobalRichardson`](@ref) wraps any fixed-step method in global Richardson
3334
extrapolation over whole solves, interpreting `abstol` and `reltol` as
3435
global tolerances. It is the most robust and most expensive option.
3536

36-
For example, solving while tracking the global error along the trajectory:
37+
For example, solving with a controlled endpoint global error of `1e-8`:
3738

3839
```julia
39-
using GlobalDiffEq
40+
using GlobalDiffEq, OrdinaryDiffEqTsit5
4041

4142
function lorenz!(du, u, p, t)
4243
du[1] = 10.0(u[2] - u[1])
4344
du[2] = u[1] * (28.0 - u[3]) - u[2]
4445
du[3] = u[1] * u[2] - (8 / 3) * u[3]
4546
end
4647
prob = ODEProblem(lorenz!, [1.0; 0.0; 0.0], (0.0, 10.0))
48+
sol = solve(prob, GlobalDefectCorrection(Tsit5(); gtol = 1.0e-8))
49+
```
50+
51+
or solving while tracking the global error along the trajectory:
52+
53+
```julia
4754
sol = solve(prob, GLEE35(); abstol = 1.0e-8, reltol = 1.0e-8)
4855
errs = global_error_estimate(sol) # global error estimate at every sol.t
4956
```
@@ -63,6 +70,7 @@ global_error_estimate
6370
```@docs
6471
GlobalRichardson
6572
GlobalErrorTransport
73+
GlobalDefectCorrection
6674
GlobalAdjoint
6775
adjoint_error_estimate
6876
```

lib/GlobalDiffEq/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "GlobalDiffEq"
22
uuid = "1d72d19b-84cc-4cb7-a099-7cbdb9ccc67c"
33
authors = ["Chris Rackauckas <accounts@chrisrackauckas.com> and contributors"]
4-
version = "1.6.0"
4+
version = "1.7.0"
55

66
[deps]
77
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"

lib/GlobalDiffEq/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ provides:
2020
- `GlobalErrorTransport`: global error estimation and `gtol`-based control by
2121
integrating the linearized error-transport equation driven by the
2222
dense-output defect (Shampine 1986, Berzins 1988, Lang-Verwer 2007).
23+
- `GlobalDefectCorrection`: global error estimation and control by solving
24+
for the correction (Zadunaisky 1976, Dormand-Duckers-Prince 1984/1989),
25+
requiring no Jacobian.
2326
- `GlobalAdjoint`: adjoint-based a posteriori endpoint error estimation and
2427
control (Cao and Petzold 2004), available as a package extension when
2528
SciMLSensitivity and QuadGK are loaded.

lib/GlobalDiffEq/src/GlobalDiffEq.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ include("richardson.jl")
1818
include("adjoint.jl")
1919
include("companion.jl")
2020
include("transport.jl")
21+
include("correction.jl")
2122
include("glee/tableaus.jl")
2223
include("glee/algorithms.jl")
2324
include("glee/solve.jl")
2425
include("glee/caches.jl")
2526
include("glee/perform_step.jl")
2627

2728
export GlobalAdjoint, GlobalRichardson, adjoint_error_estimate
28-
export GlobalErrorTransport
29+
export GlobalDefectCorrection, GlobalErrorTransport
2930
export GLEE23, GLEE24, GLEE35, MM5GEE, global_error_estimate
3031

3132
@setup_workload begin

lib/GlobalDiffEq/src/correction.jl

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
GlobalDefectCorrection(alg; gtol=nothing, correction_alg=alg, maxiters=6,
3+
safety=0.8, companion_abstol, companion_reltol)
4+
5+
Wrap an ODE algorithm with global error estimation and control based on
6+
solving for the correction (defect correction). After a dense forward solve
7+
producing the interpolant `P(t)`, the global error `ε(t) ≈ y(t) - P(t)` is
8+
estimated by integrating the nonlinear companion ODE
9+
10+
```math
11+
ε'(t) = f(P(t) + ε(t), p, t) - P'(t), \\qquad ε(t_0) = 0,
12+
```
13+
14+
which requires no Jacobian, only extra evaluations of `f` on perturbed
15+
arguments. This is the "solving for the correction" technique of Zadunaisky
16+
(1976) and Dormand, Duckers and Prince (1984), in the continuous per-step
17+
dense-output form of Dormand, Lockyer, McGorrigan and Prince (1989); its
18+
validity under standard local-error step control was proven by Calvo, Higham,
19+
Montijano and Rández (1996).
20+
21+
Set the requested absolute endpoint error with `gtol`:
22+
23+
```julia
24+
solve(prob, GlobalDefectCorrection(Tsit5(); gtol = 1.0e-6))
25+
```
26+
27+
The solver then tightens local tolerances until the estimated endpoint global
28+
error 2-norm is at most `gtol`, like [`GlobalAdjoint`](@ref). Omit `gtol` when
29+
using the algorithm only with [`global_error_estimate`](@ref).
30+
`correction_alg` selects the solver for the companion equation, and
31+
`companion_abstol` / `companion_reltol` control its tolerances.
32+
33+
This implementation supports forward-time, standard-mass-matrix ODEs with real
34+
vector states and no callbacks. The wrapped solver must provide a dense
35+
first-derivative interpolation.
36+
37+
## References
38+
39+
- P. E. Zadunaisky, On the estimation of errors propagated in the numerical
40+
integration of ordinary differential equations, Numerische Mathematik 27
41+
(1976).
42+
- J. R. Dormand, R. R. Duckers and P. J. Prince, Global error estimation with
43+
Runge-Kutta methods, IMA Journal of Numerical Analysis 4 (1984).
44+
- J. R. Dormand, M. A. Lockyer, N. E. McGorrigan and P. J. Prince, Global
45+
error estimation with Runge-Kutta triples, Computers & Mathematics with
46+
Applications 18 (1989).
47+
"""
48+
struct GlobalDefectCorrection{A, CA, G, V} <: GlobalDiffEqAlgorithm
49+
alg::A
50+
correction_alg::CA
51+
gtol::G
52+
options::V
53+
end
54+
55+
function GlobalDefectCorrection(
56+
alg;
57+
correction_alg = alg,
58+
gtol = nothing,
59+
maxiters = 6,
60+
safety = 0.8,
61+
companion_abstol = gtol === nothing ? 1.0e-10 : min(gtol / 100, 1.0e-10),
62+
companion_reltol = gtol === nothing ? 1.0e-8 : min(gtol / 100, 1.0e-8)
63+
)
64+
options = _companion_options(
65+
gtol, maxiters, safety, companion_abstol, companion_reltol
66+
)
67+
return GlobalDefectCorrection(alg, correction_alg, gtol, options)
68+
end
69+
70+
SciMLBase.allows_arbitrary_number_types(::GlobalDefectCorrection) = false
71+
SciMLBase.allowscomplex(::GlobalDefectCorrection) = false
72+
SciMLBase.isautodifferentiable(::GlobalDefectCorrection) = false
73+
74+
function _correction_rhs(sol)
75+
prob = sol.prob
76+
foop = _oop_rhs(prob)
77+
return let sol = sol, foop = foop, p = prob.p
78+
function (ε, _, t)
79+
u = sol(t, continuity = :right)
80+
du = sol(t, Val{1}, continuity = :right)
81+
return foop(u + ε, p, t) - du
82+
end
83+
end
84+
end
85+
86+
"""
87+
global_error_estimate(prob, alg::GlobalDefectCorrection; abstol=1e-6, reltol=1e-3, kwargs...)
88+
89+
Solve an ODE problem and estimate the 2-norm of its global error at the final
90+
time by solving for the correction of the dense numerical solution. `abstol`
91+
and `reltol` control the forward solve; `companion_abstol` and
92+
`companion_reltol` control the companion error solve.
93+
"""
94+
function global_error_estimate(
95+
prob::SciMLBase.AbstractODEProblem, alg::GlobalDefectCorrection, args...;
96+
abstol = 1.0e-6,
97+
reltol = 1.0e-3,
98+
companion_abstol = alg.options.companion_abstol,
99+
companion_reltol = alg.options.companion_reltol,
100+
kwargs...
101+
)
102+
return _companion_error_estimate(
103+
_correction_rhs, "GlobalDefectCorrection",
104+
prob, alg.alg, alg.correction_alg, args...;
105+
abstol, reltol, companion_abstol, companion_reltol, kwargs...
106+
)
107+
end
108+
109+
function SciMLBase.__solve(
110+
prob::SciMLBase.AbstractODEProblem, alg::GlobalDefectCorrection, args...;
111+
abstol = something(alg.gtol, 1.0e-6),
112+
reltol = something(alg.gtol, 1.0e-3),
113+
kwargs...
114+
)
115+
alg.gtol === nothing && throw(
116+
ArgumentError("GlobalDefectCorrection requires a positive `gtol` constructor keyword")
117+
)
118+
_validate_tolerances(abstol, reltol, "local")
119+
haskey(kwargs, :callback) &&
120+
throw(ArgumentError("GlobalDefectCorrection does not currently support callbacks"))
121+
estimator = (local_abstol, local_reltol) -> global_error_estimate(
122+
prob, alg, args...;
123+
abstol = local_abstol, reltol = local_reltol, kwargs...
124+
)
125+
return _refine_to_gtol(
126+
estimator, prob, alg.alg, alg.gtol, alg.options, args...;
127+
abstol, reltol, kwargs...
128+
)
129+
end

lib/GlobalDiffEq/test/companion_tests.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const lv_tspan = (0.0, 10.0)
1010
prob = ODEProblem(lv!, [1.0, 1.0], lv_tspan)
1111
ref = solve(prob, Tsit5(); abstol = 1.0e-13, reltol = 1.0e-13)
1212

13-
for alg in (GlobalErrorTransport(Tsit5()),)
13+
for alg in (GlobalErrorTransport(Tsit5()), GlobalDefectCorrection(Tsit5()))
1414
for tol in (1.0e-4, 1.0e-6)
1515
est = global_error_estimate(prob, alg; abstol = tol, reltol = tol)
1616
sol = solve(prob, Tsit5(); abstol = tol, reltol = tol)
@@ -21,7 +21,7 @@ const lv_tspan = (0.0, 10.0)
2121

2222
# out-of-place problems
2323
prob_oop = ODEProblem(lv, [1.0, 1.0], lv_tspan)
24-
for alg in (GlobalErrorTransport(Tsit5()),)
24+
for alg in (GlobalErrorTransport(Tsit5()), GlobalDefectCorrection(Tsit5()))
2525
est = global_error_estimate(prob_oop, alg; abstol = 1.0e-6, reltol = 1.0e-6)
2626
sol = solve(prob_oop, Tsit5(); abstol = 1.0e-6, reltol = 1.0e-6)
2727
true_err = norm(sol.u[end] - ref.u[end])
@@ -36,6 +36,7 @@ end
3636

3737
for alg in (
3838
GlobalErrorTransport(Tsit5(); gtol),
39+
GlobalDefectCorrection(Tsit5(); gtol),
3940
)
4041
sol = solve(prob, alg; abstol = 1.0e-3, reltol = 1.0e-3)
4142
@test SciMLBase.successful_retcode(sol)
@@ -46,7 +47,7 @@ end
4647
@testset "Companion estimator argument validation" begin
4748
prob = ODEProblem(lv!, [1.0, 1.0], lv_tspan)
4849

49-
for Alg in (GlobalErrorTransport,)
50+
for Alg in (GlobalErrorTransport, GlobalDefectCorrection)
5051
alg = Alg(Tsit5())
5152
@test !SciMLBase.allows_arbitrary_number_types(alg)
5253
@test !SciMLBase.allowscomplex(alg)

0 commit comments

Comments
 (0)