Skip to content

Commit 94843bc

Browse files
Add GlobalErrorTransport global error estimation and control
Implements the linearized error-transport estimator (SciML/GlobalDiffEq.jl#5; Shampine 1986 / Berzins 1988 / Lang-Verwer 2007): after a dense forward solve, the companion linear ODE ε' = Jε + d(t) driven by the dense-output defect d(t) = f(P(t)) - P'(t) is integrated for the endpoint global error, with matrix-free Jacobian-vector products through DifferentiationInterface (autodiff accepts any ADTypes backend, default AutoForwardDiff). gtol-controlled solves tighten local tolerances via the shared refinement loop in companion.jl, mirroring GlobalAdjoint. global_error_estimate(prob, alg) is the standalone estimator, also added for GlobalAdjoint as an alias of adjoint_error_estimate. Verified locally on Lotka-Volterra vs a 1e-13 reference: estimate to true-error ratios 0.995-0.999 at tolerances 1e-4/1e-6, and gtol=1e-7 control yields 9.8e-9 final error. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent e362cb5 commit 94843bc

9 files changed

Lines changed: 383 additions & 7 deletions

File tree

docs/src/globalerrorcontrol/GlobalDiffEq.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ using GlobalDiffEq
2323
only a few extra stages per step over a plain method of the same order and
2424
require nothing beyond the right-hand side `f`.
2525
- To *control* the endpoint global error to a tolerance `gtol`, wrap any
26-
adaptive solver in [`GlobalAdjoint`](@ref) (adjoint-based, for endpoint
27-
functionals; requires SciMLSensitivity and QuadGK to be loaded). It solves
28-
the problem, estimates the endpoint global error, and tightens the local
29-
tolerances until the requested global tolerance is met.
26+
adaptive solver in [`GlobalErrorTransport`](@ref) (linearized
27+
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.
3032
- [`GlobalRichardson`](@ref) wraps any fixed-step method in global Richardson
3133
extrapolation over whole solves, interpreting `abstol` and `reltol` as
3234
global tolerances. It is the most robust and most expensive option.
@@ -60,6 +62,7 @@ global_error_estimate
6062

6163
```@docs
6264
GlobalRichardson
65+
GlobalErrorTransport
6366
GlobalAdjoint
6467
adjoint_error_estimate
6568
```

lib/GlobalDiffEq/Project.toml

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

66
[deps]
7+
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
78
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
9+
DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63"
810
FastBroadcast = "7034ab61-46d4-4ed7-9d0f-46aef9175898"
11+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
912
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1013
MuladdMacro = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221"
1114
OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8"
@@ -31,8 +34,11 @@ OrdinaryDiffEqCore = {path = "../OrdinaryDiffEqCore"}
3134
OrdinaryDiffEqTsit5 = {path = "../OrdinaryDiffEqTsit5"}
3235

3336
[compat]
37+
ADTypes = "1"
3438
DiffEqBase = "7"
39+
DifferentiationInterface = "0.6, 0.7"
3540
FastBroadcast = "1.3"
41+
ForwardDiff = "0.10, 1"
3642
LinearAlgebra = "1"
3743
MuladdMacro = "0.2.1"
3844
OrdinaryDiffEqCore = "4"

lib/GlobalDiffEq/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ provides:
1717
together with an asymptotically correct estimate of its global error.
1818
- `MM5GEE`: the Makazaga-Murua (2003) Dormand-Prince-based order-5 scheme
1919
with a cheap built-in global error estimate.
20+
- `GlobalErrorTransport`: global error estimation and `gtol`-based control by
21+
integrating the linearized error-transport equation driven by the
22+
dense-output defect (Shampine 1986, Berzins 1988, Lang-Verwer 2007).
2023
- `GlobalAdjoint`: adjoint-based a posteriori endpoint error estimation and
2124
control (Cao and Petzold 2004), available as a package extension when
2225
SciMLSensitivity and QuadGK are loaded.

lib/GlobalDiffEq/src/GlobalDiffEq.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ module GlobalDiffEq
33
using Reexport: @reexport
44
@reexport using DiffEqBase
55

6-
import LinearAlgebra, OrdinaryDiffEqCore, OrdinaryDiffEqTsit5, Random,
7-
RecursiveArrayTools, Richardson, SciMLBase, SciMLStructures
6+
import ADTypes, DifferentiationInterface, ForwardDiff, LinearAlgebra,
7+
OrdinaryDiffEqCore, OrdinaryDiffEqTsit5, Random, RecursiveArrayTools,
8+
Richardson, SciMLBase, SciMLStructures
89
import DiffEqBase: initialize!, calculate_residuals, calculate_residuals!
910
import OrdinaryDiffEqCore: perform_step!, @cache
1011
using FastBroadcast: FastBroadcast, @..
@@ -15,13 +16,16 @@ abstract type GlobalDiffEqAlgorithm <: SciMLBase.AbstractODEAlgorithm end
1516

1617
include("richardson.jl")
1718
include("adjoint.jl")
19+
include("companion.jl")
20+
include("transport.jl")
1821
include("glee/tableaus.jl")
1922
include("glee/algorithms.jl")
2023
include("glee/solve.jl")
2124
include("glee/caches.jl")
2225
include("glee/perform_step.jl")
2326

2427
export GlobalAdjoint, GlobalRichardson, adjoint_error_estimate
28+
export GlobalErrorTransport
2529
export GLEE23, GLEE24, GLEE35, MM5GEE, global_error_estimate
2630

2731
@setup_workload begin

lib/GlobalDiffEq/src/adjoint.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,17 @@ function adjoint_error_estimate(
312312
)
313313
end
314314

315+
"""
316+
global_error_estimate(prob, alg::GlobalAdjoint; kwargs...)
317+
318+
Alias for [`adjoint_error_estimate`](@ref).
319+
"""
320+
function global_error_estimate(
321+
prob::SciMLBase.AbstractODEProblem, alg::GlobalAdjoint, args...; kwargs...
322+
)
323+
return adjoint_error_estimate(prob, alg, args...; kwargs...)
324+
end
325+
315326
function SciMLBase.__solve(
316327
prob::SciMLBase.AbstractODEProblem, alg::GlobalAdjoint, args...;
317328
abstol = something(alg.gtol, 1.0e-6),

lib/GlobalDiffEq/src/companion.jl

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Shared machinery for the defect-companion global error estimators
2+
# (GlobalErrorTransport and GlobalDefectCorrection): both solve an auxiliary
3+
# ODE for the global error driven by the defect of the dense numerical
4+
# solution, d(t) = f(P(t)) - P'(t), where P is the solution interpolant.
5+
6+
function _validate_estimation_problem(prob, name)
7+
prob.u0 isa AbstractVector{<:AbstractFloat} ||
8+
throw(ArgumentError("$name requires a real floating-point vector state"))
9+
isempty(prob.u0) && throw(ArgumentError("$name requires a nonempty state"))
10+
prob.tspan[1] < prob.tspan[2] ||
11+
throw(ArgumentError("$name requires a forward-time ODEProblem"))
12+
prob.f.mass_matrix == LinearAlgebra.I ||
13+
throw(ArgumentError("$name currently requires the standard mass matrix"))
14+
problem_kwargs = values(prob.kwargs)
15+
if haskey(problem_kwargs, :callback) && problem_kwargs.callback !== nothing
16+
throw(ArgumentError("$name does not currently support callbacks"))
17+
end
18+
return nothing
19+
end
20+
21+
function _validate_estimation_solution(sol, name)
22+
SciMLBase.successful_retcode(sol) ||
23+
throw(ErrorException("the forward solve failed with retcode $(sol.retcode)"))
24+
length(sol.t) >= 2 ||
25+
throw(ArgumentError("the forward solution must save at least its two endpoints"))
26+
return nothing
27+
end
28+
29+
# Out-of-place view of the (possibly in-place) user RHS, safe for dual numbers.
30+
function _oop_rhs(prob)
31+
f = SciMLBase.unwrapped_f(prob.f)
32+
if SciMLBase.isinplace(prob)
33+
return let f = f
34+
function (u, p, t)
35+
du = similar(u)
36+
f(du, u, p, t)
37+
return du
38+
end
39+
end
40+
else
41+
return f
42+
end
43+
end
44+
45+
const _DENSE_SOLVE_KWARGS = (;
46+
dense = true,
47+
save_everystep = true,
48+
save_start = true,
49+
save_end = true,
50+
saveat = (),
51+
save_idxs = nothing,
52+
)
53+
54+
# Endpoint-error refinement loop shared by the companion estimators: solve,
55+
# estimate the endpoint global error, and tighten the local tolerances until
56+
# the estimate is at most gtol; then redo the solve with the user's original
57+
# saving options.
58+
function _refine_to_gtol(
59+
estimator, prob, inner_alg, gtol, options, args...;
60+
abstol, reltol, kwargs...
61+
)
62+
local_abstol = abstol
63+
local_reltol = reltol
64+
last_estimate = oftype(float(gtol), Inf)
65+
66+
for _ in 1:options.maxiters
67+
last_estimate = estimator(local_abstol, local_reltol)
68+
isfinite(last_estimate) ||
69+
throw(ErrorException("the global error estimate is not finite"))
70+
71+
if last_estimate <= gtol
72+
return SciMLBase.solve(
73+
prob, inner_alg, args...;
74+
abstol = local_abstol, reltol = local_reltol, kwargs...
75+
)
76+
end
77+
78+
tolerance_scale = min(0.5, options.safety * gtol / last_estimate)
79+
local_abstol *= tolerance_scale
80+
local_reltol *= tolerance_scale
81+
iszero(local_abstol) && iszero(local_reltol) &&
82+
throw(ErrorException("local tolerances underflowed during global error control"))
83+
end
84+
85+
throw(
86+
ErrorException(
87+
"failed to meet gtol=$(gtol) after $(options.maxiters) iterations; " *
88+
"last estimated global error was $(last_estimate)"
89+
)
90+
)
91+
end
92+
93+
function _companion_options(gtol, maxiters, safety, companion_abstol, companion_reltol)
94+
maxiters isa Integer && maxiters > 0 ||
95+
throw(ArgumentError("maxiters must be a positive integer"))
96+
safety isa Real && isfinite(safety) && 0 < safety < 1 ||
97+
throw(ArgumentError("safety must be between zero and one"))
98+
(gtol === nothing || _positive_finite_real(gtol)) ||
99+
throw(ArgumentError("gtol must be a positive finite real number"))
100+
_validate_tolerances(companion_abstol, companion_reltol, "companion")
101+
return (;
102+
maxiters = Int(maxiters), safety,
103+
companion_abstol, companion_reltol,
104+
)
105+
end
106+
107+
# Dense forward solve + companion error solve, returning the endpoint global
108+
# error 2-norm estimate. rhs_for(sol) builds the companion RHS from the dense
109+
# forward solution.
110+
function _companion_error_estimate(
111+
rhs_for, name, prob, inner_alg, companion_alg, args...;
112+
abstol, reltol, companion_abstol, companion_reltol, kwargs...
113+
)
114+
haskey(kwargs, :callback) &&
115+
throw(ArgumentError("$name does not currently support callbacks"))
116+
_validate_estimation_problem(prob, name)
117+
solve_kwargs = merge((; kwargs...), _DENSE_SOLVE_KWARGS)
118+
sol = SciMLBase.solve(prob, inner_alg, args...; abstol, reltol, solve_kwargs...)
119+
_validate_estimation_solution(sol, name)
120+
endpoint_error = _companion_endpoint(
121+
rhs_for(sol), sol, companion_alg;
122+
abstol = companion_abstol, reltol = companion_reltol
123+
)
124+
return LinearAlgebra.norm(endpoint_error)
125+
end
126+
127+
# Solve the companion error ODE ε' = rhs(ε, t), ε(t0) = 0, over the forward
128+
# solution's time span and return the endpoint error estimate ε(T).
129+
function _companion_endpoint(rhs, sol, companion_alg; abstol, reltol)
130+
ε0 = zero(sol.prob.u0)
131+
companion_prob = SciMLBase.ODEProblem{false}(rhs, ε0, sol.prob.tspan)
132+
companion_sol = SciMLBase.solve(
133+
companion_prob, companion_alg;
134+
abstol, reltol, dense = false, save_everystep = false,
135+
save_start = false, save_end = true
136+
)
137+
SciMLBase.successful_retcode(companion_sol) || throw(
138+
ErrorException(
139+
"the companion error solve failed with retcode $(companion_sol.retcode)"
140+
)
141+
)
142+
return companion_sol.u[end]
143+
end

lib/GlobalDiffEq/src/transport.jl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
GlobalErrorTransport(alg; gtol=nothing, transport_alg=alg,
3+
autodiff=ADTypes.AutoForwardDiff(), maxiters=6,
4+
safety=0.8, companion_abstol, companion_reltol)
5+
6+
Wrap an ODE algorithm with global error estimation and control based on the
7+
linearized error-transport (first variational) equation. After a dense forward
8+
solve producing the interpolant `P(t)`, the global error `ε(t) ≈ y(t) - P(t)`
9+
is estimated by integrating the linear companion ODE
10+
11+
```math
12+
ε'(t) = J(P(t), p, t) \\, ε(t) + d(t), \\qquad ε(t_0) = 0,
13+
```
14+
15+
where `d(t) = f(P(t), p, t) - P'(t)` is the defect of the dense output and `J`
16+
is the Jacobian of `f`, applied matrix-free through Jacobian-vector products
17+
computed with `autodiff` (any `ADTypes` backend supported by
18+
DifferentiationInterface). This is the error-transport approach of Shampine
19+
(1986) and Berzins (1988), in the continuous defect-driven form summarized by
20+
Lang and Verwer (2007).
21+
22+
Set the requested absolute endpoint error with `gtol`:
23+
24+
```julia
25+
solve(prob, GlobalErrorTransport(Tsit5(); gtol = 1.0e-6))
26+
```
27+
28+
The solver then tightens local tolerances until the estimated endpoint global
29+
error 2-norm is at most `gtol`, like [`GlobalAdjoint`](@ref). Omit `gtol` when
30+
using the algorithm only with [`global_error_estimate`](@ref). `transport_alg`
31+
selects the solver for the companion equation, and `companion_abstol` /
32+
`companion_reltol` control its tolerances.
33+
34+
This implementation supports forward-time, standard-mass-matrix ODEs with real
35+
vector states and no callbacks. The wrapped solver must provide a dense
36+
first-derivative interpolation.
37+
38+
## References
39+
40+
- L. F. Shampine, Global error estimation with one-step methods, Computers &
41+
Mathematics with Applications 12A (1986).
42+
- M. Berzins, Global error estimation in the method of lines for parabolic
43+
equations, SIAM Journal on Scientific and Statistical Computing 9 (1988).
44+
- J. Lang and J. Verwer, On global error estimation and control for initial
45+
value problems, SIAM Journal on Scientific Computing 29 (2007).
46+
"""
47+
struct GlobalErrorTransport{A, TA, AD, G, V} <: GlobalDiffEqAlgorithm
48+
alg::A
49+
transport_alg::TA
50+
autodiff::AD
51+
gtol::G
52+
options::V
53+
end
54+
55+
function GlobalErrorTransport(
56+
alg;
57+
transport_alg = alg,
58+
autodiff = ADTypes.AutoForwardDiff(),
59+
gtol = nothing,
60+
maxiters = 6,
61+
safety = 0.8,
62+
companion_abstol = gtol === nothing ? 1.0e-10 : min(gtol / 100, 1.0e-10),
63+
companion_reltol = gtol === nothing ? 1.0e-8 : min(gtol / 100, 1.0e-8)
64+
)
65+
options = _companion_options(
66+
gtol, maxiters, safety, companion_abstol, companion_reltol
67+
)
68+
return GlobalErrorTransport(alg, transport_alg, autodiff, gtol, options)
69+
end
70+
71+
SciMLBase.allows_arbitrary_number_types(::GlobalErrorTransport) = false
72+
SciMLBase.allowscomplex(::GlobalErrorTransport) = false
73+
SciMLBase.isautodifferentiable(::GlobalErrorTransport) = false
74+
75+
function _transport_rhs(sol, autodiff)
76+
prob = sol.prob
77+
foop = _oop_rhs(prob)
78+
return let sol = sol, foop = foop, p = prob.p, backend = autodiff
79+
function (ε, _, t)
80+
u = sol(t, continuity = :right)
81+
du = sol(t, Val{1}, continuity = :right)
82+
defect = foop(u, p, t) - du
83+
jv = only(
84+
DifferentiationInterface.pushforward(
85+
x -> foop(x, p, t), backend, u, (ε,)
86+
)
87+
)
88+
return jv + defect
89+
end
90+
end
91+
end
92+
93+
"""
94+
global_error_estimate(prob, alg::GlobalErrorTransport; abstol=1e-6, reltol=1e-3, kwargs...)
95+
96+
Solve an ODE problem and estimate the 2-norm of its global error at the final
97+
time by integrating the linearized error-transport equation driven by the
98+
dense-output defect. `abstol` and `reltol` control the forward solve;
99+
`companion_abstol` and `companion_reltol` control the companion error solve.
100+
"""
101+
function global_error_estimate(
102+
prob::SciMLBase.AbstractODEProblem, alg::GlobalErrorTransport, args...;
103+
abstol = 1.0e-6,
104+
reltol = 1.0e-3,
105+
companion_abstol = alg.options.companion_abstol,
106+
companion_reltol = alg.options.companion_reltol,
107+
kwargs...
108+
)
109+
return _companion_error_estimate(
110+
sol -> _transport_rhs(sol, alg.autodiff), "GlobalErrorTransport",
111+
prob, alg.alg, alg.transport_alg, args...;
112+
abstol, reltol, companion_abstol, companion_reltol, kwargs...
113+
)
114+
end
115+
116+
function SciMLBase.__solve(
117+
prob::SciMLBase.AbstractODEProblem, alg::GlobalErrorTransport, args...;
118+
abstol = something(alg.gtol, 1.0e-6),
119+
reltol = something(alg.gtol, 1.0e-3),
120+
kwargs...
121+
)
122+
alg.gtol === nothing && throw(
123+
ArgumentError("GlobalErrorTransport requires a positive `gtol` constructor keyword")
124+
)
125+
_validate_tolerances(abstol, reltol, "local")
126+
haskey(kwargs, :callback) &&
127+
throw(ArgumentError("GlobalErrorTransport does not currently support callbacks"))
128+
estimator = (local_abstol, local_reltol) -> global_error_estimate(
129+
prob, alg, args...;
130+
abstol = local_abstol, reltol = local_reltol, kwargs...
131+
)
132+
return _refine_to_gtol(
133+
estimator, prob, alg.alg, alg.gtol, alg.options, args...;
134+
abstol, reltol, kwargs...
135+
)
136+
end

0 commit comments

Comments
 (0)