Benchmark Suite

DifferentialEquations.jl provides a benchmarking suite to be able to test the difference in error, speed, and efficiency between algorithms. DifferentialEquations.jl includes current benchmarking notebooks to help users understand the performance of the methods. These benchmarking notebooks use the included benchmarking suite. There are two parts to the benchmarking suite: shootouts and work-precision. The Shootout tests methods head-to-head for timing and error on the same problem. A WorkPrecision draws a work-precision diagram for the algorithms in question on the chosen problem.

Rendered Benchmarks

The rendered SciML Benchmarks can be found at benchmarks.sciml.ai. The source code for the benchmarks can be found at https://github.com/SciML/SciMLBenchmarks.jl.

Shootout

A shootout is where you compare between algorithms. For example, to see how different Runge-Kutta algorithms fair against each other, one can define a setup which is a dictionary of Symbols to Any, where the symbol is the keyword argument. Then you call Shootout on that setup. The code is as follows:

using OrdinaryDiffEq, DiffEqProblemLibrary.ODEProblemLibrary, DiffEqDevTools, ODE,
    ODEInterface, ODEInterfaceDiffEq

ODEProblemLibrary.importodeproblems()
prob = ODEProblemLibrary.prob_ode_2Dlinear
setups = [
    Dict(:alg => DP5())
    Dict(:abstol => 1.0e-3, :reltol => 1.0e-6, :alg => ode45()) # Fix ODE to be normal
    Dict(:alg => dopri5())
]
names = ["DifferentialEquations"; "ODE"; "ODEInterface"]
shoot = Shootout(prob, setups; dt = 1 / 2^(10), names = names)

Note that keyword arguments applied to Shootout are applied to every run, so in this example every run has the same starting timestep. Here we explicitly chose names. If you don't, then the algorithm name is the default. This returns a Shootout type which holds the times it took for each algorithm and the errors. Using these, it calculates the efficiency defined as 1/(error*time), i.e. if the error is low or the run was quick then it's efficient. print(shoot) will show all of this information, and plot(shoot) will show the efficiencies of the algorithms in comparison to each other.

For every benchmark function there is a special keyword numruns which controls the number of runs used in the time estimate. To be more precise, these functions by default run the algorithm 20 times on the problem and take the average time. This amount can be increased and decreased as needed.

The keyword appxsol allows for specifying a reference against which the error is computed. The method of error computation can be specified by the keyword error_estimate with values :L2 for the L2 error over the solution time interval, :l2 calculates the l2 error at the actual steps and the default :final only compares the endpoints.

A ShootoutSet is a where you define a vector of probs and tspans and run a shootout on each of these values.

WorkPrecision

A WorkPrecision calculates the necessary components of a work-precision plot. This shows how time scales with the user chosen tolerances on a given problem. To make a WorkPrecision, you give it a vector of absolute and relative tolerances:

abstols = 1 ./ 10 .^ (3:10)
reltols = 1 ./ 10 .^ (3:10)
wp = WorkPrecision(prob, DP5(), abstols, reltols; name = "Dormand-Prince 4/5")

If we want to plot many WorkPrecisions together in order to compare between algorithms, you can make a WorkPrecisionSet. To do so, you pass the setups into the function as well:

wp_set = WorkPrecisionSet(prob, tspan, abstols, reltols, setups; numruns = 2)
setups = [
    Dict(:alg => RK4()); Dict(:alg => Euler()); Dict(:alg => BS3());
    Dict(:alg => Midpoint()); Dict(:alg => BS5()); Dict(:alg => DP5())
]
wp_set = WorkPrecisionSet(prob, abstols, reltols, setups; dt = 1 / 2^4, numruns = 2)

Both of these types have a plot recipe to produce a work-precision diagram, and a print which will show some relevant information.

Tags and comparison plots

A benchmark usually wants several views of the same data: each family of methods on its own, then the best of each family against each other, with a couple of reference methods in every plot. Preset tags derived from algorithm traits and supertypes produce all of those from a single run. Add only benchmark-specific tags such as :reference, request every error metric the plots need with error_estimates, and slice the result afterwards:

setups = [
    Dict(:alg => Rosenbrock23()),
    Dict(:alg => Rodas5P()),
    Dict(:alg => TRBDF2()),
    Dict(:alg => KenCarp4()),
    Dict(:alg => RadauIIA5(), :tags => [:reference]),
]
wp_set = WorkPrecisionSet(
    prob, abstols, reltols, setups;
    error_estimates = [:final, :l2], appxsol = test_sol
)

plot(wp_set, tags = [:rosenbrock])                     # one family
plot(best_of_families(wp_set, [:rosenbrock, :sdirk]))  # cross-family comparison
plot(wp_set, x = :l2)                                  # a second error metric, no re-solve

# a family against the baseline
plot(
    wp_set, tags = [:sdirk], include_tags = [:reference],
    reference_tags = [:reference]
)

For example, auto_tags(KenCarp4()) includes :order_4, :adaptive, :implicit, :sdirk, :esdirk, and :split. Explicit setup tags are appended without duplicates. Use :auto_tags => false when a setup needs only its manually supplied tags.

plot(wp_set; tags) keeps the entries carrying all of tags, include_tags adds entries back regardless of that filter, and exclude_tags drops entries. Entries matching reference_tags are drawn in a separate, de-emphasized style controlled by reference_style. autoplot returns the whole standard collection of subsets at once, keyed by name.

Slow configurations can be capped with timeout (seconds per tolerance): a solve is never interrupted, but once one exceeds the budget its repeated timing runs are skipped and the point is recorded as NaN, which the plot recipe drops.

API

DiffEqDevTools.ShootoutType
Shootout(
    prob, setups; appxsol = nothing, names = nothing,
    error_estimate = :final, numruns = 20, seconds = 2, kwargs...
)

Benchmark multiple solver configurations on one problem. Each entry of setups is a dictionary containing an :alg and any solver-specific keyword arguments. The result stores the solutions, errors, timings, efficiencies, and the configuration with the highest efficiency, where efficiency is 1 / (error * time).

Use appxsol as a numerical reference when the problem has no analytic solution. Additional keyword arguments are forwarded to every solve.

source
DiffEqDevTools.ShootoutSetType
ShootoutSet(
    probs, setups; probaux = nothing, names = nothing,
    print_names = false, kwargs...
)

Run a Shootout for every problem in probs. probaux may contain one dictionary of per-problem keyword arguments for each problem; other keyword arguments are shared by every shootout.

source
DiffEqDevTools.WorkPrecisionType
WorkPrecision(
    prob, alg, abstols, reltols, dts = nothing;
    name = nothing, appxsol = nothing, error_estimate = :final,
    numruns = 20, seconds = 2, tags = Symbol[], auto_tags = true,
    timeout = nothing, kwargs...
)

Measure error and execution time for alg at corresponding absolute and relative tolerances. When dts is provided, its entries select a fixed time step for each tolerance pair. The result stores the measured errors, timings, solver statistics, and inputs for plotting a work-precision diagram.

Use appxsol as a numerical reference when the problem has no analytic solution. tags attaches metadata symbols used by filter_by_tags and the plot recipe. They are merged with auto_tags(alg) unless auto_tags = false. timeout gives a per-tolerance wall-clock budget in seconds; see WorkPrecisionSet. Additional keyword arguments are forwarded to solve.

source
DiffEqDevTools.WorkPrecisionSetType
WorkPrecisionSet(prob, abstols, reltols, setups; error_estimates = nothing,
    timeout = nothing, kwargs...)

Build one WorkPrecision result for each solver configuration in setups so their work-precision curves can be compared. Each setup is a dictionary containing an :alg and may override the shared tolerances or fixed step sizes with :abstols, :reltols, or :dts, or its legend entry with :name. Preset tags from auto_tags are attached by default and merged with any :tags entry. Set :auto_tags => false in a setup to use only its explicit tags. The resulting metadata is used by filter_by_tags, best_of_families, autoplot, and the plot recipe to build family and cross-family comparisons.

error_estimates requests several error metrics from a single run (for example [:final, :l2, :L2]), so plots for each metric can be drawn without re-solving; the computed metrics are reported by available_errors. error_estimate remains the metric the plot recipe defaults to.

timeout is a per-tolerance wall-clock budget in seconds. A solve is never interrupted, but once one exceeds the budget its repeated timing runs are skipped and its error and time are recorded as NaN, which the plot recipe drops.

source
DiffEqDevTools.get_sample_errorsFunction
get_sample_errors(
    prob::AbstractRODEProblem, setup, test_dt = nothing;
    numruns, solution_runs, appxsol_setup = nothing,
    sample_error_runs = 10^7, parallel_type = :none, kwargs...
)

Estimate an approximate 95% confidence half-width for the sampling error of an RODE solver setup. numruns may be one sample count or a collection of counts; the return value is respectively a scalar or a collection of sampling-error estimates. solution_runs controls the number of independent estimates used to measure their variation.

When an analytic solution is available, sample_error_runs controls the Monte Carlo estimate of its expected endpoint. Otherwise, repeated numerical solution means provide the endpoint reference. Set parallel_type = :threads to parallelize the solution samples.

source

Tagging and comparison helpers

DiffEqDevTools.auto_tagsFunction
auto_tags(alg) -> Vector{Symbol}

Return preset metadata tags derived from the public traits and type hierarchy of alg. Differential-equation algorithms receive an order tag such as :order_5 (or :order_3_2 for order 3 // 2) when their order is defined, plus :adaptive or :fixed_step. When OrdinaryDiffEqCore is loaded, the result also describes known implicitness, families, and structural traits such as :rosenbrock, :firk, :sdirk, :split, and :multistep.

Algorithms without applicable traits return an empty vector. WorkPrecision and WorkPrecisionSet merge these presets with explicit tags by default.

source
DiffEqDevTools.get_tagsFunction
get_tags(wp_set::WorkPrecisionSet) -> Vector{Vector{Symbol}}

Return the tags of each entry of wp_set, in order. Tags come from the :tags entry of the corresponding setup dictionary.

source
DiffEqDevTools.unique_tagsFunction
unique_tags(wp_set::WorkPrecisionSet) -> Vector{Symbol}

Return every tag used by any entry of wp_set, sorted and deduplicated.

source
DiffEqDevTools.filter_by_tagsFunction
filter_by_tags(wp_set::WorkPrecisionSet, tags::Symbol...) -> WorkPrecisionSet

Return the entries of wp_set tagged with every one of tags (AND logic), as a new WorkPrecisionSet. Passing no tags returns wp_set unchanged.

source
DiffEqDevTools.exclude_by_tagsFunction
exclude_by_tags(wp_set::WorkPrecisionSet, tags::Symbol...) -> WorkPrecisionSet

Return the entries of wp_set carrying none of tags (OR logic on the exclusion), as a new WorkPrecisionSet. Passing no tags returns wp_set unchanged.

source
DiffEqDevTools.merge_wp_setsFunction
merge_wp_sets(sets::WorkPrecisionSet...) -> WorkPrecisionSet

Concatenate the entries of several WorkPrecisionSets into one. The tolerances, problem and error estimates are taken from the first set, so merging results computed on different problems or tolerance grids gives a set whose metadata describes only the first of them.

source
DiffEqDevTools.available_errorsFunction
available_errors(wp_set::WorkPrecisionSet) -> Vector{Symbol}

Return the error estimates computed for wp_set, i.e. the error_estimates requested from WorkPrecisionSet or, when none were, the single error_estimate. Each one is a valid x for the plot recipe.

source
DiffEqDevTools.wp_areaFunction
wp_area(wp::WorkPrecision) -> Float64

Trapezoidal area under the log₁₀(time)-vs-log₁₀(error) curve of wp, using the tolerances that produced a usable (positive, non-NaN) error and time. Lower is better. Returns Inf when fewer than two such points exist.

The area grows with the width of the error range covered, so it only compares methods that span a similar range; best_by_tag normalizes by that width instead.

source
DiffEqDevTools.best_by_tagFunction
best_by_tag(wp_set::WorkPrecisionSet, tag::Symbol; n = 1, metric = :area)
    -> WorkPrecisionSet

Return the n best-performing entries of wp_set tagged tag. With metric = :area (the only metric currently supported) entries are ranked first by how many tolerances produced a usable measurement — so a method that fails at tight tolerances does not win on the few points it survived — and then by their mean log₁₀ solve time over the log₁₀ error range they cover, i.e. wp_area normalized by that range.

source
DiffEqDevTools.best_of_familiesFunction
best_of_families(wp_set::WorkPrecisionSet, family_tags; n = 1, metric = :area)
    -> WorkPrecisionSet

Combine the n best entries of each family in family_tags (see best_by_tag) into one WorkPrecisionSet for a cross-family comparison. An entry belonging to several families is included once. Throws an ArgumentError when no entry carries any of family_tags.

source
DiffEqDevTools.with_autodiff_variantsFunction
with_autodiff_variants(setups; ad_backends, tag_prefix = :autodiff)
    -> Vector{Dict{Symbol, Any}}

Expand setups with one copy per entry of ad_backends, each copy replacing :alg with the same algorithm rebuilt for that backend. Setups whose algorithm takes no autodiff argument (explicit Runge-Kutta methods, say) are passed through unexpanded.

The originals are kept and tagged $(tag_prefix)_default; each variant is tagged with the lowercased backend name, e.g. :autodiff_forwarddiff for AutoForwardDiff(), and named after its backend so the legends stay readable. Existing tags are preserved and the input setups are not mutated, so the result plots as an AD comparison via plot(wp_set, tags = [:autodiff_forwarddiff]).

source
DiffEqDevTools.autoplotFunction
autoplot(wp_set::WorkPrecisionSet; families = nothing, reference_tags = nothing,
    best_n = 2) -> Dict{String, WorkPrecisionSet}

Split one tagged WorkPrecisionSet into the subsets a benchmark page usually plots, without re-solving anything:

  • "family_<tag>" for each family in families, holding that family's entries plus any entry matching reference_tags
  • "best_of_families", the best_of_families selection of the best_n best entries per family, again alongside the reference entries
  • "all", the full input set

Each value plots directly with plot(subset). When families is not given it is taken from the tags in use, dropping tags carried by more than 80% of the entries, since those describe the benchmark as a whole (:stiff, :nonstiff) rather than a family.

source