Introducing Ensemble Simulations: The Lorenz Equations
We have already seen the simulation of the trajectories of ODEs, as it would be the Lorenz equations:
using DifferentialEquations,Plots
function parameterized_lorenz!(du,u,p,t)
= u
x,y,z = p
σ,ρ,β 1] = dx = σ*(y-x)
du[2] = dy = x*(ρ-z) - y
du[3] = dz = x*y - β*z
du[end
= [1.0,1.0,1.0] # Initial Conditions
u0 = (0.0,20)
tspan = [10.0,28.0,8/3]
p =ODEFunction(parameterized_lorenz!,syms = [:x, :y, :z])
f= ODEProblem(f,u0,tspan,p)
prob = solve(prob)
sol plot(sol,idxs=:x)
savefig("lorenz_x.png")
When we want to simulate a bunch of solutions, called and ensemble simulation, we need to define a function which will assign new problems or initial conditions (one could also change the parameters) to the same ODEs.
A simple scenario is to modify the initial condition by a small random perturbation, to see the effect of dependence with respect to the initial conditions:
function prob_func(prob,i,repeat)
= prob.u0+randn()*1e-2
@. prob.u0
probend
The ensemble simulation is a set of and ODEProblem together with a function which updates the initial conditions or the parameters at every individual simulation.
= EnsembleProblem(prob,prob_func=prob_func) ensemble_prob
We now generate the simulation
= solve(ensemble_prob,trajectories=500) sim
and there is a plotting recipe to plot the simulation
plot(sim,idxs=(:x,:y),linealpha=0.1)
savefig("lorenz_ensemble_xy.png")
We can analyze the solutions of our problem using quantiles and generate a plot
= EnsembleSummary(sim;quantiles=[0.05,0.95])
summplot(summ,
=:x,
idxs=0.2,
fillalpha=(-20,20),
ylims= RGB(0.2, 0.2, 0.2)
background_color
)savefig("lorenz_quantiles.png")
Using parallelism
An interesting feature of Ensemble Simulations is that one can use different kinds of parallelism or multithreading. From the documentation
- EnsembleSerial() - No parallelism
- EnsembleThreads() - The default. This uses multithreading. It's local (single computer, shared memory) parallelism only. Fastest when the trajectories are quick.
- EnsembleDistributed() - Uses pmap internally. It will use as many processors as you have Julia processes. To add more processes, use addprocs(n). See Julia's documentation for more details. Recommended for the case when each trajectory calculation isn't “too quick” (at least about a millisecond each?).
- EnsembleSplitThreads() - This uses threading on each process, splitting the problem into nprocs() even parts. This is for solving many quick trajectories on a multi-node machine. It's recommended you have one process on each node.
- EnsembleGPUArray() - Requires installing and using DiffEqGPU. This uses a GPU for computing the ensemble with hyperparallelism. It will automatically recompile your Julia functions to the GPU. A standard GPU sees a 5x performance increase over a 16 core Xeon CPU. However, there are limitations on what functions can auto-compile in this fashion, please see the DiffEqGPU README for more details
To start julia with multi-threading you need to run julia --threads=10
or any number of threads. No parallelism is equivalent to the serial version
@time sim = solve(ensemble_prob, EnsembleSerial(), trajectories=10_000)
while a threaded version with the number of threads we called wen running julia is
@time sim = solve(ensemble_prob, EnsembleThreads(), trajectories=10_000)
The improvement in time depend on your machine configuration. In my case, using 10 cores we get the following, which is above a factor of \(9\).