Solving models
First create a symbolic cosmological model:
using SymBoltz
M = SymBoltz.ΛCDM()
Creating the problem
Once the symbolic cosmological model M
has been constructed, it can be turned into a numerical problem: For example:
pars = SymBoltz.parameters_Planck18(M)
prob = CosmologyProblem(M, pars)
Cosmology problem for model ΛCDM
Stages:
Background: 5 unknowns, 0 splines
Perturbations: 61 unknowns, 5 splines
Parameters & initial conditions:
c₊Ω₀ = 0.26447041034523616
b₊Ω₀ = 0.04936780993111075
γ₊T₀ = 2.7255
b₊YHe = 0.2454
h₊m_eV = 0.06
h = 0.6736
I₊ns = 0.965
ν₊Neff = 2.99
I₊ln_As1e10 = 3.0440461338325417
SymBoltz.CosmologyProblem
— TypeCosmologyProblem(
M::System, pars::Dict, shoot_pars = Dict(), shoot_conditions = [];
ivspan = (0.0, 100.0), bg = true, pt = true, spline = true, debug = false, fully_determined = true, jac = false, kwargs...
)
Create a numerical cosmological problem from the model M
with parameters pars
. Optionally, the shooting method determines the parameters shoot_pars
(mapped to initial guesses) such that the equations shoot_conditions
are satisfied at the final time.
If bg
and pt
, the model is split into the background and perturbations stages. If spline
is a Bool
, it decides whether all background unknowns in the perturbations system are replaced by splines. If spline
is a Vector
, it rather decides which (unknown and observed) variables are splined.
Updating the parameters
Constructing a CosmologyProblem
is an expensive operation that compiles all the symbolics down to numerics. It is not necessary to repeat this just to update parameter values. To do so, use the function parameter_updater
that returns a function that quickly creates new problems with updated parameter values:
probmaker = parameter_updater(prob, [M.g.h, M.c.Ω₀]) # fast factory function
prob = probmaker([0.70, 0.27]) # create updated problem
Cosmology problem for model ΛCDM
Stages:
Background: 5 unknowns, 0 splines
Perturbations: 61 unknowns, 5 splines
Parameters & initial conditions:
c₊Ω₀ = 0.27
b₊Ω₀ = 0.04936780993111075
γ₊T₀ = 2.7255
b₊YHe = 0.2454
h₊m_eV = 0.06
h = 0.7
I₊ns = 0.965
ν₊Neff = 2.99
I₊ln_As1e10 = 3.0440461338325417
SymBoltz.parameter_updater
— Functionparameter_updater(prob::CosmologyProblem, idxs; kwargs...)
Create and return a function that updates the symbolic parameters idxs
of the cosmological problem prob
. The returned function is called with numerical values (in the same order as idxs
) and returns a new problem with the updated parameters.
Solving the problem
The (updated) problem can now be solved for some wavenumbers:
using Unitful, UnitfulAstro
ks = 10 .^ range(-5, +1, length=100) / u"Mpc"
sol = solve(prob, ks)
Cosmology solution for model ΛCDM
Stages:
Background: return code Terminated; solved with Rodas4P; 1311 points
Perturbations: return codes Success; solved with KenCarp4; 81-826 points; x100 k ∈ [0.042827494000000015, 42827.49400000001] H₀/c (interpolation in-between)
CommonSolve.solve
— MethodCommonSolve.solve(args...; kwargs...)
Solves an equation or other mathematical problem using the algorithm specified in the arguments. Generally, the interface is:
CommonSolve.solve(prob::ProblemType,alg::SolverType; kwargs...)::SolutionType
where the keyword arguments are uniform across all choices of algorithms.
By default, solve
defaults to using solve!
on the iterator form, i.e.:
solve(args...; kwargs...) = solve!(init(args...; kwargs...))
function solve(
prob::CosmologyProblem, ks::Union{Nothing, AbstractArray} = nothing;
bgopts = (alg = Rodas4P(), reltol = 1e-9, abstol = 1e-9),
ptopts = (alg = KenCarp4(), reltol = 1e-8, abstol = 1e-8),
shootopts = (alg = NewtonRaphson(), abstol = 1e-5),
thread = true, verbose = false, kwargs...
)
Solve the cosmological problem prob
up to the perturbative level with wavenumbers ks
(or only to the background level if it is empty). The options bgopts
and ptopts
are passed to the background and perturbations ODE solve()
calls, and shootopts
to the shooting method nonlinear solve()
. If threads
, integration over independent perturbation modes are parallellized.
Accessing the solution
The returned solution sol
can be conveniently accessed to obtain any variable y
of the model M
:
sol(y, τ)
returns the background variable(s) $y(τ)$ as a function of conformal time(s) $τ$. It interpolates between time points using the ODE solver's custom-tailored interpolator.sol(y, τ, k)
returns the perturbation variable(s) $y(τ,k)$ as a function of the wavenumber(s) $k$ and conformal time(s) $τ$. It also interpolates linearly between the logarithms of the wavenumbers passed tosolve
.
Note that y
can be any symbolic variables in the model M
, and even expressions thereof. Unknown variables are part of the state vector integrated by the ODE solver, and are returned directly from its solution. Observed variables or expressions are functions of the unknowns, and are automatically calculated from the equations that define them in the symbolic model. For example:
τs = sol[M.τ] # get time points used in the background solution
ks = [1e-3, 1e-2, 1e-1, 1e0] / u"Mpc" # wavenumbers
as = sol(M.g.a, τs) # scale factors
Ωms = sol((M.b.ρ + M.c.ρ) / M.G.ρ, τs) # matter-to-total density ratios
κs = sol(M.b.κ, τs) # optical depths
Φs = sol(M.g.Φ, τs, ks) # metric potentials
Φs_over_Ψs = sol(M.g.Φ / M.g.Ψ, τs, ks) # ratio between metric potentials
Plotting the solution
SymBoltz.jl includes plot recipes for easily visualizing the solution. It works similarly to the solution accessing: call plot(sol, [wavenumber(s),] x_expr, y_expr)
to plot y_expr
as a function of x_expr
. For example, to plot some of the same quantities that we obtained above:
using Plots
p1 = plot(sol, log10(M.g.a), (M.b.ρ + M.c.ρ) / M.G.ρ)
p2 = plot(sol, log10(M.g.a), log10(abs(M.b.κ)))
p3 = plot(sol, log10(M.g.a), M.g.Φ / M.g.Ψ, ks[1:3]) # exclude last k, where Φ and Ψ cross 0
plot(p1, p2, p3, layout=(3, 1), size=(600, 800))
More examples are shown on the models page.
Solve background and perturbations directly
For lower-level control, you can solve the background and perturbations separately:
SymBoltz.solvebg
— Functionsolvebg(bgprob::ODEProblem; alg = Rodas4P(), reltol = 1e-9, abstol = 1e-9, verbose = false, kwargs...)
Solve the background cosmology problem bgprob
.
SymBoltz.solvept
— Functionsolvept(ptprob::ODEProblem, bgsol::ODESolution, ks::AbstractArray, var2spl::Dict; alg = KenCarp4(), reltol = 1e-8, abstol = 1e-8, output_func = (sol, i) -> (sol, false), thread = true, verbose = false, kwargs...)
Solve the perturbation cosmology problem ptprob
with wavenumbers ks
. A background solution bgsol
must be passed (see solvebg
), and a dictionary var2spl
that maps background variables to spline parameters in the perturbation problem. If thread
and Julia is running with multiple threads, the solution of independent wavenumbers is parallellized. The return value is an EnsembleSolution
over all ks
.
Choice of ODE solver
In principle, models can be solved with any DifferentialEquations.jl ODE solver. But most cosmological models have very stiff Einstein-Boltzmann equations that can only be solved by implicit solvers, while explicit solvers usually fail. For the standard ΛCDM model, some good solvers are:
Rodas4P
: Slow for large systems. Very accurate. Handles extreme stiffness. Default background solver.Rodas5P
: Slow for large systems. Very accurate. Handles severe stiffness.KenCarp4
(andKenCarp47
): Fast. Handles medium stiffness. Default perturbation solver.Kvaerno5
: Behaves similar toKenCarp4
. Slightly more accurate. Slightly slower.TRBDF2
: Very fast. Decent accuracy. Handles severe stiffness.
See also the solver benchmarks.