@@ -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))
5554end
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
66125The computed solutions must contain error measurements, usually obtained from an
67126analytic solution. Use [`analyticless_test_convergence`](@ref) when only a numerical
68127reference 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"""
70154function 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)
0 commit comments