Examples

In this page we go through various examples of combining processes to make models that have been already used in the literature, or using DynamicalSystems.jl or other packages to analyse conceptual climate models.

Classic Snowball Earth hysteresis

The origins of energy balance models ((Sellers, 1969; Budyko, 1969; Ghil, 1981; North et al., 1981)) examined the impact of variations in insolation on the global climate. In particular, they studied how simple energy balance models with only ice-albedo and water vapor feedbacks yielded bi-stable hysteresis between a cold "snowball" state and a warm Earth, as the solar constant was varied. The same kind of behaviour is also used in (Datseris and Parlitz, 2022; Ch. 2) as an introductory example to dynamical systems.

We can easily replicate such a model by creating a global-mean temperature model without even having the water vapor feedback. We will combine the processes:

using ConceptualClimateModels
using ConceptualClimateModels.CCMV

budyko_processes = [
    BasicRadiationBalance(),
    EmissivityStefanBoltzmanOLR(),
    IceAlbedoFeedback(; min = 0.3, max = 0.7),
    α ~ α_ice,
    ParameterProcess(ε), # emissivity is a parameter
    f ~ 0, # no external forcing
    # absorbed solar radiation has a default process
]

budyko = processes_to_coupledodes(budyko_processes)
println(dynamical_system_summary(budyko))
1-dimensional CoupledODEs
 deterministic: true
 discrete time: false
 in-place:      false

with equations:
 Differential(t)(T(t)) ~ (-0.0029390154298310064(ASR(t) + f(t) - OLR(t))) / (-τ_T)
 ε(t) ~ ε_0
 f(t) ~ 0.0
 S(t) ~ 1.0
 α_ice(t) ~ max_α_ice + 0.5(-max_α_ice + min_α_ice)*(1 + tanh((2(-Tfreeze_α_ice + (1//2)*Tscale_α_ice + T(t))) / Tscale_α_ice))
 OLR(t) ~ 5.670374419e-8ε(t)*(T(t)^4)
 α(t) ~ α_ice(t)
 ASR(t) ~ 340.25S(t)*(1 - α(t))

and parameter values:
 τ_T ~ 1.4695077149155033e6
 Tfreeze_α_ice ~ 273.15
 min_α_ice ~ 0.3
 max_α_ice ~ 0.7
 Tscale_α_ice ~ 10.0
 ε_0 ~ 0.5

We can perform a typical hysteresis loop analysis straightforwardly by doing a continuation analysis with the Attractors.jl subpackage of DynamicalSystems.jl.

For setting up the continuation we leverage the integration with DynamicalSystems.jl:

using DynamicalSystems

grid = physically_plausible_grid(budyko)
mapper = AttractorsViaRecurrences(budyko, grid)
rfam = RecurrencesFindAndMatch(mapper)
sampler = physically_plausible_ic_sampler(budyko)
sampler() # randomly sample initial conditions
1-element Vector{Float64}:
 295.57915724019756

Now, to obtain the symbolic parameter index corresponding to the insolation parameter, there are several ways as described in the main tutorial. The simplest way is

index = :ε_0
:ε_0

Now we perform the continuation versus the effective emissivity, to approximate increasing or decreasing the strength of the greenhouse effect:

εrange = 0.3:0.01:0.8
fractions_curves, attractors_info = continuation(
    rfam, εrange, index, sampler;
    samples_per_parameter = 1000,
    show_progress = false,
)
([Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0)  …  Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0)], Dict{Int64, StateSpaceSet{1, Float64}}[Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 1-dimensional StateSpaceSet{Float64} with 1 points)  …  Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 1-dimensional StateSpaceSet{Float64} with 1 points)])

and visualize it

using CairoMakie
a2r = A -> first(first(A)) # plot attractor: extract first point and first dimension of point
plot_basins_attractors_curves(fractions_curves, attractors_info, a2r, εrange)
Example block output

we see there are two attractors at low and high temperatures and that they have approximately the same basin fractions of 50% each.

Rate dependent tipping in the Stommel model

The Stommel model is a good example for rate dependent tipping (Lohmann et al., 2021). We can modify the process ΔTStommelModel and make its parameter η3 be a time- dependent variable instead, like so:

using ConceptualClimateModels
using ConceptualClimateModels.CCMV

@variables η1(t)
@parameters η1_0 = 2.0 # starting value for η1 parameter
@parameters r_η = 0.0  # the rate that η1 changes

processes = [
    ΔTStommelModel(; η1 = η1), # replace keyword with a symbolic variable
    η1 ~ η1_0 + r_η*t, # this symbolic variable has its own equation!
]

stommel = processes_to_coupledodes(processes)
println(dynamical_system_summary(stommel))
2-dimensional CoupledODEs
 deterministic: true
 discrete time: false
 in-place:      false

with equations:
 Differential(t)(ΔT(t)) ~ η1(t) - ΔT(t) - ΔT(t)*abs(-ΔS(t) + ΔT(t))
 Differential(t)(ΔS(t)) ~ η2 - ΔS(t)*abs(-ΔS(t) + ΔT(t)) - ΔS(t)*η3
 η1(t) ~ η1_0 + r_η*t

and parameter values:
 r_η ~ 0.0
 η1_0 ~ 2.0
 η2 ~ 1.0
 η3 ~ 0.3

At the moment r_η = 0 and the system is autonomous. Hence, we can easily estimate its bifurcation diagram using the same steps as the above example.

using DynamicalSystems

grid = physically_plausible_grid(stommel)
mapper = AttractorsViaRecurrences(stommel, grid;
    consecutive_recurrences = 1000, attractor_locate_steps = 10,
)
rfam = RecurrencesFindAndMatch(mapper)
sampler = physically_plausible_ic_sampler(stommel)

ηrange = 2.0:0.01:4.0
fractions_curves, attractors_info = continuation(
    rfam, ηrange, η1_0, sampler;
    samples_per_parameter = 1000,
    show_progress = false,
)
([Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0), Dict(1 => 1.0)  …  Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0), Dict(2 => 1.0)], Dict{Int64, StateSpaceSet{2, Float64}}[Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(1 => 2-dimensional StateSpaceSet{Float64} with 1 points)  …  Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points), Dict(2 => 2-dimensional StateSpaceSet{Float64} with 1 points)])

and visualize it

using CairoMakie
a2r = A -> first(first(A)) # plot attractor: extract first point and first dimension of point
fig = plot_attractors_curves(attractors_info, a2r, ηrange)
ax = content(fig[1,1])
ax.ylabel = "ΔT - fixed points"
ax.xlabel = "parameter η1"
fig
Example block output

Alright, now we can perform simple simulations where we evolve the system forwards in time while η_1 increases at different rates. We can use the trajectory function to evolve it and due to the nice integration between DynamicalSystems.jl and ModelingToolkit.jl we can use any "observable" of the system for the trajectory output.

r1, r2 = 0.02, 0.2
u0 = [1.61334  1.85301] # always start from same state
set_parameter!(stommel, η1_0, 2.0) # set it to initial value

for (j, r) in enumerate((r1, r2))
    # update the named parameter `r_η` to the numeric value `r`
    set_parameter!(stommel, r_η, r)
    # simulate until η1 becomes 4
    tfinal = (4.0 - default_value(η1_0))/r
    # trajectory: first column = ΔΤ, second column = η1
    X, tvec = trajectory(stommel, tfinal, u0; save_idxs = [ΔT, η1])
    lines!(ax, X[:, 2], X[:, 1]; color = Cycled(j+2), label = "r_η = $(r)")
end
axislegend(ax; unique = true, position = :lt)
ylims!(ax, 1, 4)
fig
Example block output

As you can see from the figure, depending on the rate the system either "tracks" the fixed point of high ΔΤ or it collapses down to the small ΔT branch. This happens because the system crosses the unstable manifold of the lower branch (Datseris and Parlitz, 2022; Chap. 12). To visualize the unstable manifold we could use BifurcationKit.jl, however, it is very inconvenient to do so, because BifurcationKit.jl does not provide most of the conveniences that DynamicalSystems.jl does. For example, it does not integrate well enough with DifferentialEquations.jl (to allow passing ODEProblem which is created by DynamicalSystem). It also does not allow indexing parameters by their symbolic bindings. Lastly, it does not work with models generated with ModelingToolkit.jl so we would have to re-write all the equations that the chosen processes made for us.

Glacial oscillations

Coming soon!