Skip to content

Commit 0cec66d

Browse files
authored
Merge branch 'SciML:master' into master
2 parents d6d7f6d + 2111422 commit 0cec66d

37 files changed

Lines changed: 542 additions & 243 deletions

File tree

docs/src/devtools/alg_dev/convergence.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ A type which holds the data from a convergence simulation.
2020

2121
### Fields
2222

23-
- `solutions::Array{<:DESolution}`: Holds all the PdeSolutions.
23+
- `solutions::Array{<:DESolution}`: Holds the solutions. For the Monte-Carlo method of
24+
`test_convergence` these hold a single representative trajectory unless
25+
`retain_solutions = true` is passed — see
26+
[Memory use of Monte-Carlo studies](@ref).
2427

2528
- `errors`: Dictionary of the error calculations. Can contain:
2629

@@ -92,10 +95,34 @@ log2(error[i+1]/error[i])
9295

9396
Returns the mean of the convergence estimates.
9497

98+
## Memory use of Monte-Carlo studies
99+
100+
For an SDE or RODE problem, `test_convergence` runs an ensemble per step size. A full
101+
solution per trajectory carries the solver cache, the noise process, the problem and its
102+
interpolation, so keeping them makes a study over many trajectories retain far more than
103+
the error estimates it reports — at hundreds of thousands of trajectories, tens of
104+
gigabytes.
105+
106+
They are therefore **not** kept by default. Each trajectory is reduced to a
107+
[`ConvergenceTrajectory`](@ref) by the ensemble's `output_func` as it is solved, so the
108+
full solutions never coexist, and once the errors have been computed the ensemble is
109+
stripped to a single representative trajectory. `errors`, `weak_errors`, `error_means`
110+
and `𝒪est` come from the full ensemble and are unaffected — what changes is that
111+
`solutions[i].u` holds one entry rather than `trajectories` of them.
112+
113+
Pass `retain_solutions = true` when the trajectories themselves are the point, such as
114+
comparing two algorithms path by path. `sim[i, j]` forwards into `solutions[i][j]`, so
115+
it too reaches only the representative trajectory unless the solutions are retained.
116+
117+
The reduction is skipped where it cannot apply, and the solutions retained: when you
118+
supply your own `EnsembleProblem` (set its `output_func` instead), when `expected_value`
119+
is given, and when `weak_timeseries_errors` or `weak_dense_errors` is set.
120+
95121
## API
96122

97123
```@docs
98124
DiffEqDevTools.ConvergenceSimulation
125+
DiffEqDevTools.ConvergenceTrajectory
99126
DiffEqDevTools.test_convergence
100127
DiffEqDevTools.analyticless_test_convergence
101128
```

docs/src/devtools/internals/public_api.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ documented under the API section.
1010
Do not build application code against these hooks. They exist so solver
1111
packages can extend common traits, controllers, interpolation hooks, and
1212
initialization protocols without depending on undocumented implementation
13-
details. Concrete per-algorithm cache types and low-level nonlinear solve
14-
helper functions are not public API; only the abstract cache supertypes a
15-
solver package subtypes are.
13+
details. Concrete per-algorithm caches and low-level nonlinear-solve helpers
14+
remain internal unless listed below; the listed automatic-switch caches and
15+
nonlinear-solver hooks are shared with sibling integrator packages.
1616

1717
## DiffEqBase solver hooks
1818

@@ -99,6 +99,7 @@ OrdinaryDiffEqCore.StochasticDiffEqJumpNewtonDiffusionAdaptiveAlgorithm
9999

100100
```@docs
101101
OrdinaryDiffEqCore.CompositeAlgorithm
102+
OrdinaryDiffEqCore.AutoSwitchCache
102103
OrdinaryDiffEqCore.isautoswitch
103104
OrdinaryDiffEqCore.default_autoswitch
104105
OrdinaryDiffEqCore.unwrap_alg
@@ -193,6 +194,7 @@ extension points.
193194
```@docs
194195
OrdinaryDiffEqCore.AbstractNLSolver
195196
OrdinaryDiffEqCore.AbstractNLSolverAlgorithm
197+
OrdinaryDiffEqCore.AbstractNLSolverCache
196198
OrdinaryDiffEqCore.nlsolve_f
197199
OrdinaryDiffEqCore.get_W
198200
OrdinaryDiffEqCore.set_new_W!
@@ -204,6 +206,12 @@ OrdinaryDiffEqCore.isJcurrent
204206
OrdinaryDiffEqCore.resize_J_W!
205207
OrdinaryDiffEqCore.resize_nlsolver!
206208
OrdinaryDiffEqCore.default_nlsolve
209+
OrdinaryDiffEqNonlinearSolve.nlsolve!
210+
OrdinaryDiffEqNonlinearSolve.nlsolvefail
211+
OrdinaryDiffEqNonlinearSolve.compute_step!
212+
OrdinaryDiffEqNonlinearSolve.initial_η
213+
OrdinaryDiffEqNonlinearSolve.anderson
214+
OrdinaryDiffEqNonlinearSolve.anderson!
207215
```
208216

209217
## Jacobian / W-matrix / differentiation configuration
@@ -249,6 +257,7 @@ OrdinaryDiffEqCore.OrdinaryDiffEqMutableCache
249257
OrdinaryDiffEqCore.StochasticDiffEqCache
250258
OrdinaryDiffEqCore.StochasticDiffEqConstantCache
251259
OrdinaryDiffEqCore.StochasticDiffEqMutableCache
260+
OrdinaryDiffEqCore.DefaultCache
252261
OrdinaryDiffEqCore.@cache
253262
OrdinaryDiffEqCore.alg_cache
254263
OrdinaryDiffEqCore.get_fsalfirstlast
@@ -266,6 +275,9 @@ OrdinaryDiffEqCore._ode_init
266275
OrdinaryDiffEqCore._initialize_dae!
267276
OrdinaryDiffEqCore.find_algebraic_vars_eqs
268277
OrdinaryDiffEqCore.get_differential_vars
278+
OrdinaryDiffEqCore.handle_callback_modifiers!
279+
OrdinaryDiffEqCore.resolve_stage_step_limiters
280+
OrdinaryDiffEqCore.trivial_limiter!
269281
```
270282

271283
## Dense output / interpolation

lib/DelayDiffEq/Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DelayDiffEq"
22
uuid = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb"
33
authors = ["Chris Rackauckas <accounts@chrisrackauckas.com>"]
4-
version = "6.1.0"
4+
version = "6.1.1"
55

66
[deps]
77
ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
@@ -73,7 +73,7 @@ LinearAlgebra = "1"
7373
LinearSolve = "5.1"
7474
Logging = "1"
7575
OrdinaryDiffEqBDF = "2"
76-
OrdinaryDiffEqCore = "4"
76+
OrdinaryDiffEqCore = "4.8.1"
7777
OrdinaryDiffEqDefault = "2"
7878
OrdinaryDiffEqDifferentiation = "3"
7979
OrdinaryDiffEqFeagin = "2"

lib/DiffEqDevTools/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DiffEqDevTools"
22
uuid = "f3b72e0c-5b89-59e1-b016-84e28bfd966d"
33
authors = ["Chris Rackauckas <accounts@chrisrackauckas.com>"]
4-
version = "3.1.6"
4+
version = "3.2.0"
55

66
[deps]
77
CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"

lib/DiffEqDevTools/src/DiffEqDevTools.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ include("test_solution.jl")
5757
include("ode_tableaus.jl")
5858
include("tableau_info.jl")
5959

60-
export ConvergenceSimulation, Shootout, ShootoutSet, TestSolution
60+
export ConvergenceSimulation, ConvergenceTrajectory, Shootout, ShootoutSet, TestSolution
6161

6262
#Benchmark Functions
6363
export Shootout, ShootoutSet, WorkPrecision, WorkPrecisionSet

lib/DiffEqDevTools/src/convergence.jl

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ function ConvergenceSimulation(
2424
expected_value = nothing
2525
)
2626
N = size(solutions, 1)
27-
uEltype = eltype(solutions[1].u[1])
2827
errors = Dict() #Should add type information
2928
if expected_value == nothing
3029
if isnothing(solutions[1].errors) || isempty(solutions[1].errors)
@@ -54,6 +53,66 @@ function ConvergenceSimulation(
5453
return (ConvergenceSimulation(solutions, errors, N, auxdata, 𝒪est, convergence_axis))
5554
end
5655

56+
"""
57+
ConvergenceTrajectory(t, u, u_analytic, errors)
58+
59+
The part of a trajectory's solution that a convergence study actually consumes: its
60+
error measurements and the endpoint values the weak errors are formed from.
61+
62+
[`test_convergence`](@ref) stores one of these per trajectory in place of the full
63+
solution unless `retain_solutions = true`, which is what keeps a Monte-Carlo study's
64+
memory proportional to the errors it reports rather than to the solver state it
65+
happened to allocate along the way.
66+
"""
67+
struct ConvergenceTrajectory{tType, uType, EType <: NamedTuple}
68+
t::Tuple{tType}
69+
u::Tuple{uType}
70+
u_analytic::Tuple{uType}
71+
errors::EType
72+
end
73+
74+
"""
75+
_convergence_output_func(sol, ctx) -> (ConvergenceTrajectory, false)
76+
77+
`output_func` that reduces a trajectory to its errors and endpoints as soon as it is
78+
solved, so the ensemble never holds the full solutions at once.
79+
80+
Only valid when the weak timeseries and dense errors are switched off, since those
81+
need the whole timeseries and the noise process; [`test_convergence`](@ref) checks
82+
that before installing this.
83+
"""
84+
function _convergence_output_func(sol, ctx)
85+
# One-tuples rather than one-element vectors, and a NamedTuple rather than the
86+
# solver's error `Dict`: `[end]` and key lookup work the same, but a `Dict` alone
87+
# costs several hundred bytes per trajectory, which dominates everything else
88+
# retained here once the study runs to millions of trajectories.
89+
u_analytic = sol.u_analytic === nothing ? sol.u[end] : sol.u_analytic[end]
90+
reduced = ConvergenceTrajectory(
91+
(sol.t[end],), (sol.u[end],), (u_analytic,), NamedTuple(sol.errors)
92+
)
93+
94+
return (reduced, false)
95+
end
96+
97+
"""
98+
_drop_trajectories(sim::EnsembleTestSolution) -> EnsembleTestSolution
99+
100+
Discard every trajectory but the first, keeping the error statistics that were already
101+
computed from the full ensemble.
102+
103+
Reducing at `output_func` bounds the peak, but the reduced trajectories still add up
104+
across step sizes, and once `calculate_ensemble_errors` has run they are dead weight —
105+
a [`ConvergenceSimulation`](@ref) reads only the error dictionaries. One is kept rather
106+
than none so that anything reaching for a representative trajectory still finds one.
107+
"""
108+
function _drop_trajectories(sim::SciMLBase.EnsembleTestSolution{T, N}) where {T, N}
109+
u = sim.u[1:min(1, length(sim.u))]
110+
return SciMLBase.EnsembleTestSolution{T, N, typeof(u)}(
111+
u, sim.errors, sim.weak_errors, sim.error_means, sim.error_medians,
112+
sim.elapsedTime, sim.converged
113+
)
114+
end
115+
57116
"""
58117
test_convergence(dts, prob, alg[, ensemblealg]; kwargs...)
59118
test_convergence(probs, convergence_axis, alg; kwargs...)
@@ -66,6 +125,31 @@ is passed to the solver as `dt`. Remaining keyword arguments are forwarded to `s
66125
The computed solutions must contain error measurements, usually obtained from an
67126
analytic solution. Use [`analyticless_test_convergence`](@ref) when only a numerical
68127
reference solution is available.
128+
129+
## Keyword Arguments
130+
131+
- `retain_solutions`: whether `ConvergenceSimulation.solutions` keeps the trajectory
132+
solutions (default `false`). A Monte-Carlo study over many trajectories otherwise
133+
retains one complete solution object — solver cache, noise process, problem and
134+
interpolation — per trajectory per step size, which for large `trajectories` is
135+
orders of magnitude more memory than the error estimates it is computing, and is
136+
what put the weak-convergence test groups past a 16 GB runner.
137+
138+
By default each trajectory is reduced to a [`ConvergenceTrajectory`](@ref) by the
139+
ensemble's `output_func` as it is solved, so the full solutions never coexist, and
140+
the reduced ensemble is stripped to one representative trajectory once the errors
141+
have been computed from it. `errors`, `weak_errors`, `error_means` and `𝒪est` are
142+
computed from the full ensemble either way and are unaffected; what changes is that
143+
`solutions[i].u` holds one entry rather than `trajectories` of them.
144+
145+
Pass `true` when the trajectories themselves are the point, for example to compare
146+
two algorithms path by path.
147+
148+
The reduction is skipped, and the solutions retained, where it cannot apply: when
149+
you supply your own `EnsembleProblem` (set its `output_func` instead), when
150+
`expected_value` is given (the weak error is formed from the trajectory values
151+
directly), and when `weak_timeseries_errors` or `weak_dense_errors` is set (both
152+
need the whole timeseries).
69153
"""
70154
function test_convergence(
71155
dts::AbstractArray,
@@ -77,12 +161,19 @@ function test_convergence(
77161
trajectories, save_start = true, save_everystep = true,
78162
timeseries_errors = save_everystep, adaptive = false,
79163
weak_timeseries_errors = false, weak_dense_errors = false,
80-
expected_value = nothing, kwargs...
164+
expected_value = nothing, retain_solutions = false, kwargs...
81165
)
82166
N = length(dts)
83167

168+
reduce_trajectories = !retain_solutions &&
169+
!(prob isa AbstractEnsembleProblem) &&
170+
expected_value === nothing &&
171+
!weak_timeseries_errors && !weak_dense_errors
172+
84173
if prob isa AbstractEnsembleProblem
85174
ensemble_prob = prob
175+
elseif reduce_trajectories
176+
ensemble_prob = EnsembleProblem(prob; output_func = _convergence_output_func)
86177
else
87178
ensemble_prob = EnsembleProblem(prob)
88179
end
@@ -98,20 +189,24 @@ function test_convergence(
98189
kwargs...
99190
)
100191
@info "dt: $(dts[i]) ($i/$N)"
101-
_solutions[i] = sol
192+
# Summarise each ensemble before solving the next, so that only one step size's
193+
# trajectories are ever reachable rather than the whole study's.
194+
_solutions[i] = if expected_value === nothing
195+
summarised = SciMLBase.calculate_ensemble_errors(
196+
sol;
197+
weak_timeseries_errors = weak_timeseries_errors,
198+
weak_dense_errors = weak_dense_errors
199+
)
200+
reduce_trajectories ? _drop_trajectories(summarised) : summarised
201+
else
202+
sol
203+
end
102204
end
103205

104206
auxdata = Dict("dts" => dts)
105207

106208
if expected_value == nothing
107-
solutions = [
108-
SciMLBase.calculate_ensemble_errors(
109-
sim;
110-
weak_timeseries_errors = weak_timeseries_errors,
111-
weak_dense_errors = weak_dense_errors
112-
)
113-
for sim in _solutions
114-
]
209+
solutions = _solutions
115210
# Now Calculate Weak Errors
116211
additional_errors = Dict()
117212
for k in keys(solutions[1].weak_errors)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using StochasticDiffEq, DiffEqDevTools, Random, Test
2+
3+
# The Monte-Carlo method of `test_convergence` used to keep one full solution object per
4+
# trajectory per step size, which is orders of magnitude more than the error estimates
5+
# need and is what put the weak-convergence groups past a 16 GB runner. Trajectories are
6+
# now reduced by default: an `output_func` turns each into a `ConvergenceTrajectory` as
7+
# it is solved, and the ensemble is stripped to one representative once the errors have
8+
# been computed. The statistics must be identical either way.
9+
10+
f_iip(du, u, p, t) = @.(du = 1.01 * u)
11+
σ_iip(du, u, p, t) = @.(du = 0.87 * u)
12+
linear_analytic(u0, p, t, W) = @.(u0 * exp(0.63155t + 0.87W))
13+
prob = SDEProblem(
14+
SDEFunction(f_iip, σ_iip, analytic = linear_analytic), [1 / 2], (0.0, 1.0)
15+
)
16+
17+
dts = 1 .// 2 .^ (5:-1:3)
18+
ntraj = 400
19+
20+
function run_sim(; kwargs...)
21+
Random.seed!(100)
22+
return test_convergence(
23+
dts, prob, EM(); save_everystep = false, trajectories = ntraj,
24+
weak_timeseries_errors = false, kwargs...
25+
)
26+
end
27+
28+
reduced = run_sim() # the new default
29+
full = run_sim(retain_solutions = true)
30+
31+
@testset "statistics are unchanged" begin
32+
@test keys(full.𝒪est) == keys(reduced.𝒪est)
33+
for k in keys(full.𝒪est)
34+
@test full.𝒪est[k] reduced.𝒪est[k]
35+
end
36+
@test keys(full.errors) == keys(reduced.errors)
37+
for k in keys(full.errors)
38+
@test collect(full.errors[k]) collect(reduced.errors[k])
39+
end
40+
for i in eachindex(dts)
41+
@test full.solutions[i].weak_errors == reduced.solutions[i].weak_errors
42+
@test full.solutions[i].error_means == reduced.solutions[i].error_means
43+
# the per-trajectory error vectors are computed from the full ensemble either way
44+
for k in keys(full.solutions[i].errors)
45+
@test full.solutions[i].errors[k] reduced.solutions[i].errors[k]
46+
@test length(reduced.solutions[i].errors[k]) == ntraj
47+
end
48+
end
49+
end
50+
51+
@testset "trajectories are dropped by default" begin
52+
for i in eachindex(dts)
53+
@test length(reduced.solutions[i].u) == 1
54+
@test length(full.solutions[i].u) == ntraj
55+
end
56+
@test reduced.solutions[1].u[1] isa ConvergenceTrajectory
57+
@test full.solutions[1].u[1] isa SciMLBase.AbstractRODESolution
58+
# the representative keeps the endpoints the weak error is formed from
59+
@test reduced.solutions[1].u[1].u[end] == full.solutions[1].u[1].u[end]
60+
@test reduced.solutions[1].u[1].u_analytic[end] == full.solutions[1].u[1].u_analytic[end]
61+
@test Base.summarysize(reduced.solutions) < Base.summarysize(full.solutions) / 20
62+
end
63+
64+
@testset "reduction is skipped where it cannot apply" begin
65+
# these need the full timeseries, so the solutions are retained rather than the
66+
# call failing
67+
for kwargs in ((; weak_timeseries_errors = true), (; weak_dense_errors = true))
68+
sim = run_sim(; kwargs...)
69+
@test length(sim.solutions[1].u) == ntraj
70+
end
71+
# a caller-supplied EnsembleProblem owns its own output_func
72+
Random.seed!(100)
73+
sim = test_convergence(
74+
dts, EnsembleProblem(prob), EM(); save_everystep = false,
75+
trajectories = ntraj, weak_timeseries_errors = false
76+
)
77+
@test length(sim.solutions[1].u) == ntraj
78+
79+
# `expected_value` averages the trajectory values themselves, so it is used with an
80+
# EnsembleProblem that reduces to the quantity being averaged (as PL1WM.jl does).
81+
# That path must keep every reduced value, not one representative.
82+
Random.seed!(100)
83+
ens = EnsembleProblem(prob; output_func = (sol, ctx) -> (sol.u[end][1], false))
84+
sim = test_convergence(
85+
dts, ens, EM(); save_everystep = false, save_start = false,
86+
trajectories = ntraj, weak_timeseries_errors = false,
87+
expected_value = 1 / 2 * exp(1.01)
88+
)
89+
@test length(sim.solutions[1].u) == ntraj
90+
@test sim.solutions[1].u[1] isa Float64
91+
@test haskey(sim.𝒪est, :weak_final)
92+
end

0 commit comments

Comments
 (0)