Parameter ensembles
This example is very similar to Example 7 but shows how to create and run parallelized parameter ensembels.
Make sure to explicitly import the LiteImplicit
submodule which has the relevant solver types.
using CryoGrid
using CryoGrid.LiteImplicit
using Distributions
import Plots
import Random
if Threads.nthreads() == 1
@warn "Only one thread is available. Ensemble execution will run sequentially. Did you start julia with `--threads=auto` ?"
end
┌ Warning: Only one thread is available. Ensemble execution will run sequentially. Did you start julia with `--threads=auto` ?
└ @ Main cglite_parameter_ensembles.md:20
Load forcings and build stratigraphy like before, except this time we assign Param
values to the quantiies which we want to vary in the ensemble. Here we vary the porosity in each layer as well as the n-factors.
forcings = loadforcings(CryoGrid.Forcings.Samoylov_ERA_MkL3_CCSM4_long_term);
soilprofile = SoilProfile(
0.0u"m" => SimpleSoil(por=Param(0.80, prior=Uniform(0.65,0.95)),sat=1.0,org=0.75),
0.1u"m" => SimpleSoil(por=Param(0.80, prior=Uniform(0.65,0.95)),sat=1.0,org=0.25),
0.4u"m" => SimpleSoil(por=Param(0.55, prior=Uniform(0.35,0.75)),sat=1.0,org=0.25),
3.0u"m" => SimpleSoil(por=Param(0.50, prior=Uniform(0.30,0.70)),sat=1.0,org=0.0),
10.0u"m" => SimpleSoil(por=Param(0.30, prior=Uniform(0.10,0.50)),sat=1.0,org=0.0),
100.0u"m" => SimpleSoil(por=0.05,sat=1.0,org=0.0),
)
z_top = -2.0u"m"
z_bot = 1000.0u"m"
upperbc = TemperatureBC(
Input(:Tair),
NFactor(
nf=Param(0.5, prior=Beta(1,1)),
nt=Param(0.9, prior=Beta(1,1)),
),
)
ssinit = ThermalSteadyStateInit(T0=-15.0u"°C")
heatop = Heat.EnthalpyImplicit()
heat = HeatBalance(heatop)
soil_layers = map(para -> Ground(para; heat), soilprofile)
strat = Stratigraphy(
z_top => Top(upperbc),
soil_layers,
z_bot => Bottom(GeothermalHeatFlux(0.053u"W/m^2"))
);
modelgrid = CryoGrid.DefaultGrid_2cm
tile = Tile(strat, modelgrid, forcings, ssinit);
Since the solver can take daily timesteps, we can easily specify longer simulation time spans at minimal cost. Here we specify a time span of 10 years.
tspan = (DateTime(2000,1,1), DateTime(2010,12,31))
u0, du0 = initialcondition!(tile, tspan);
prob = CryoGridProblem(tile, u0, tspan, saveat=24*3600.0, savevars=(:T,))
CryoGridProblem with uType ComponentArrays.ComponentVector{Float64, Vector{Float64}, Tuple{ComponentArrays.Axis{(H = 1:1:278,)}}} and tType Float64. In-place: true
Non-trivial mass matrix: false
timespan: (6.3113904e10, 6.34609728e10)
u0: ComponentVector{Float64}(H = [-2.9924427544543177e7, -2.992328263362953e7, -2.992213772271589e7, -2.9920992811802246e7, -2.99198479008886e7, -2.916496405698563e7, -2.916405260804127e7, -2.9163141159096915e7, -2.9162229710152555e7, -2.9161318261208195e7 … -2.6012263230263706e7, -2.206699911380834e7, -1.921675312728595e7, -1.6366507140763562e7, -1.3516261154241173e7, -1.0666015167718783e7, -7.8157691811963925e6, -4.965523194674003e6, -2.115277208151613e6, 1.8591668329946272e7])
Here we retrieve the CryoGridParams
from the CryoGridProblem
constructed above. The CryoGridParams type behaves like a table and can be easily converted to a DataFrame with DataFrame(params) when DataFrames.jl is loaded.
params = prob.p
ComponentVector{Float64}(top = (nf = [0.5], nt = [0.9]), ground1 = (por = [0.8]), ground2 = (por = [0.8]), ground3 = (por = [0.55]), ground4 = (por = [0.5]), ground5 = (por = [0.3]))
Note that you can also use Julia's vec
method to convert CryoGridParams
into a ComponentVector
with labels.
p0 = vec(params)
ComponentVector{Float64}(top = (nf = [0.5], nt = [0.9]), ground1 = (por = [0.8]), ground2 = (por = [0.8]), ground3 = (por = [0.55]), ground4 = (por = [0.5]), ground5 = (por = [0.3]))
Here we extract prior distributions and collect them into a multivariate Product distribution; note that this assumes each parameter to be independent from the others
prior = Product(collect(params[:prior]))
Method 1: SciML EnsembleProblem
function make_prob_func(ensmeble::AbstractMatrix)
function prob_func(prob, i, repeat)
return remake(prob, p=ensmeble[:,i])
end
end
function output_func(sol, i)
# return output and repeat signal;
# false indicates that the run does not need to be repeated
return CryoGridOutput(sol), false
end
output_func (generic function with 1 method)
Now we sample parameter values from prior with fixed RNG; the number of samples determines the size of the ensemble
const rng = Random.MersenneTwister(1234)
prior_ensemble = rand(rng, prior, 64)
We create an EnsembleProblem
from CryoGridProblem
and prob/output functions; note that we use safetycopy=true because we're using multithreading; this prevents the different threads from using the same state caches
prob_func = make_prob_func(prior_ensemble)
ensprob = EnsembleProblem(prob; prob_func, output_func, safetycopy=true)
solve each trajectory with LiteImplicitEuler and multithreading; alternatively, one can specify EnsembleDistributed() for process or slurm parallelization or EnsembleSerial() for sequential execution.
enssol = @time solve(ensprob, LiteImplicitEuler(), EnsembleThreads(), trajectories=size(prior_ensemble,2));
nothing #hide
Now we will extract permafrost temperatures at 20m depth and plot the ensemble.
T20m_ens = reduce(hcat, map(out -> out.T[Z(Near(20.0u"m"))], enssol))
Plots.plot(T20m_ens, leg=nothing, c=:black, alpha=0.5, ylabel="Permafrost temperature")
alt_ens = reduce(hcat, map(out -> Diagnostics.active_layer_thickness(out.T), enssol))
Plots.plot(alt_ens, leg=nothing, c=:black, alpha=0.5, ylabel="Active layer thickness")
Method 2: Simple (sequential) for-loop; this could also be parallelized with pmap or @distributed. Multithreading with @threads is also possible, but then it will be necessary to call deepcopy(prob)
to prevent cache collisions
using ProgressMeter
@showprogress for i in axes(prior_ensemble)[2]
p_i = prior_ensemble[:,i]
# solve with parameters p_i
sol_i = solve(prob, LiteImplicitEuler(), p=p_i)
# here we would need to process the output and store the results;
# omitted in this example for brevity
end
This page was generated using Literate.jl.