Perturbations: Structure Formation¶
The Perturbations module computes structure formation in the universe.
Building on the Background module, Perturbations calculates how galaxies, clusters, and cosmic web structures form and evolve.
Overview¶
This module computes the growth and evolution of density fluctuations over cosmic time, including:
- Matter power spectra at various redshifts and scales
- Growth rates of structures at different epochs
- Key cosmological parameters like σ₈
This module bridges smooth background cosmology and the observed clustered universe.
The Perturbations Protocol¶
Protocol Definition: cloelib.cosmology.cosmology.Perturbations
The Perturbations protocol defines what every perturbation calculator must provide. This module requires a Background instance as it depends on cosmological distances and densities.
Units convention
All wavenumbers within cloelib are always in 1/Mpc (not h/Mpc). Power spectra are therefore in Mpc³. Any class implementing the Perturbations protocol must adopt this convention. If the underlying code uses h/Mpc internally, the implementation wrapper is responsible for converting inputs and outputs.
Required Property¶
background: Reference to the associated Background object
This is key. Perturbations always needs a Background to compute distances, densities, etc.
Required Methods¶
matter_power_spectrum(zs, ks)¶
Compute the matter power spectrum for total matter Pmm(k, z).
Inputs:
zs: Redshifts (can be array)ks: Wavenumbers in 1/Mpc (can be array)
Returns: Power spectrum in Mpc³
Note: Can be linear or non-linear depending on implementation.
matter_power_spectrum_cb(zs, ks)¶
Compute the CDM+baryons matter power spectrum Pcb(k, z).
Inputs:
zs: Redshifts (can be array)ks: Wavenumbers in 1/Mpc (can be array)
Returns: Power spectrum in Mpc³
Note: Can be linear or non-linear depending on implementation.
cb x matter cross spectrum: cb_cross_prescription¶
Pairing a PositionsTracer(use_Pcb=True) with a tracer of the total matter (lensing, magnification, CMB lensing) needs the cb x matter cross spectrum, which a backend does not provide directly. Every backend implementing matter_power_spectrum_cb declares how it is built through the class attribute cb_cross_prescription, one of the constants in cloelib.cosmology.cosmology:
| Value | Cross spectrum | Exact when |
|---|---|---|
CB_CROSS_CORRELATED ("correlated") |
\(\sqrt{P_{cb} P_{mm}}\) | the backend applies the same non-linear boost to cb and total matter, \(P_{cb}^{NL}/P_{cb}^{L} = P_{mm}^{NL}/P_{mm}^{L}\) (always true if linear) |
CB_CROSS_LINEAR_NU ("linear_neutrinos") |
\(f_{cb}\,(P_{cb}^{NL} - P_{cb}^{L}) + \sqrt{P_{cb}^{L} P_{mm}^{L}}\) | massive neutrinos, and the cb x neutrino spectrum, stay linear, \(P_{mm}^{NL} - P_{mm}^{L} = f_{cb}^2\,(P_{cb}^{NL} - P_{cb}^{L})\); needs linearperturbations |
with \(f_{cb} = \Omega_{cb,0} / \Omega_{m,0}\). A backend declares the prescription whose condition its own non-linear \(P_{mm}\) and \(P_{cb}\) satisfy. How much the two prescriptions differ depends on the backend: at \(m_\nu = 0.15\) eV, about \(3 \times 10^{-4}\) in the cross spectrum for CAMB, at most \(2 \times 10^{-3}\) for the emulators and about \(1\%\) for CLASS with HMcode2020.
| Prescription | Backends |
|---|---|
CB_CROSS_CORRELATED |
all linear backends; CAMB, HMcode2020Emu and CosmoPowerJAX (NonLinear, NonLinearHalofit) non-linear |
CB_CROSS_LINEAR_NU |
EE2, BACCOemu, JAX; CLASS, hi_class, MGCLASS and mochi_class non-linear |
| that of the baseline | MG-CosmoPowerJAX non-linear and emantis |
- Emulators (HMcode2020Emu, CosmoPowerJAX): \(P_{mm}^{NL}\) and \(P_{cb}^{NL}\) are emulated separately, and at \(m_\nu \lesssim 0.15\) eV the two conditions cannot be told apart within the emulation accuracy (\(\sim 10^{-3}\)). At higher masses the HMcode-based emulators follow the uniform boost of CAMB, so they are
correlated. For the CosmoPowerJAX halofit sector the check is inconclusive; it shares the label of the HMcode one. - CLASS family (CLASS,
hi_class, MGCLASS,mochi_class): the non-linear model (halofit/HMcode) is run on \(P_{cb}\) separately from \(P_{mm}\), so neither condition holds exactly.CB_CROSS_LINEAR_NUis used since it only relies on the backend's own non-linear \(P_{cb}\) and on linear spectra. Pass the matching*LinearPerturbationsaslinearperturbationsto get the cross spectrum. - Boosts of a baseline (MG-CosmoPowerJAX, emantis): the same boost rescales the ΛCDM baseline \(P_{mm}\) and \(P_{cb}\), so
cb_cross_prescriptionis a property returning the baseline's. emantis is calibrated without massive neutrinos, and itsmatter_power_spectrum_cbraises aValueErrorwhen \(m_\nu > 0\).
A new backend providing matter_power_spectrum_cb must declare it, as a class attribute or a property (and be added to tests/test_cb_cross_prescription.py): computing the cross spectrum for a backend without a valid cb_cross_prescription raises a ValueError. The attribute is not part of the Perturbations protocol, since only the cross spectrum needs it.
growth_factor(zs, ks)¶
Calculate the linear growth factor D(z, k).
Normalized such that D(z=0) ≈ 1 in matter-dominated era.
growth_rate(zs=None, ks=None)¶
Calculate the linear growth rate f(z) = d ln D / d ln a. Every implementation follows the same convention:
zs=Nonereturns the growth rate on the redshift grid the instance was built on; otherwise it is evaluated atzs.ks=Nonereturns an array of shape(nz,); otherwise an array of shape(nz, nk). A scale-independent growth rate is broadcast alongk.
Scale-dependent implementations evaluate f(z, k) at the given ks. Without ks, hi_class and mochi_class use k = 1 Mpc⁻¹, while BACCOemu, which has no such default, raises a ValueError.
MGCLASS computes the growth rate by finite differences on its redshift grid, so it needs to be built on at least 3 redshifts to provide it.
sigma8_0()¶
Compute σ₈ at redshift z=0.
The RMS matter fluctuation in 8 Mpc/h spheres—a key cosmological parameter.
Constructors¶
All implementations are built with the same keyword arguments, so they can be swapped without changing the calling code:
lin = LinearPerturbations(background=bg, redshifts=zs)
nonlin = NonLinearPerturbations(background=bg, linearperturbations=lin, redshifts=zs)
This is described by the LinearPerturbationsFactory and NonLinearPerturbationsFactory protocols in cloelib.cosmology.cosmology, which can be used to type a class passed around as a parameter. Their type parameters are the background and, for the nonlinear one, the linear perturbations the class is built from. For example, NonLinearPerturbationsFactory[CAMBBackground, HMemuLinearPerturbations] is satisfied by HMemuNonLinearPerturbations, while NonLinearPerturbationsFactory[CAMBBackground, CAMBLinearPerturbations] is not, since HMcode2020Emu needs the cached Pk/Pk_cb grid of its linear perturbations (WithLinearSpectrumGrid).
Options specific to a backend (e.g. nonlinear_model) are passed as additional keyword arguments and are not part of these protocols. In particular, the HMcode2020 baryonic feedback parameter log10TAGN is accepted by the HMcode-based backends (CAMB, CLASS, mochi_class, HMcode2020Emu and CosmoPower-JAX). CAMB, CLASS and mochi_class select their HMcode2020 feedback model when log10TAGN is given without a nonlinear model, and raise a ValueError if the chosen model ignores it.
Backends that compute the nonlinear spectrum themselves (CAMB, CosmoPower-JAX) accept but do not use linearperturbations; CLASS, hi_class, MGCLASS and mochi_class only use it for the cb x matter cross spectrum (see cb_cross_prescription). EmantisFofrNonLinearPerturbations additionally requires the ΛCDM nonlinear perturbations and fR0.
Existing Implementations¶
CAMB Perturbations¶
CAMB provides separate linear and non-linear perturbation implementations:
CAMBLinearPerturbations and CAMBNonLinearPerturbations.
Interfaces with CAMB for perturbation calculations.
Location: cloelib/cosmology/camb_cosmology.py
When to use: accurate, production-ready, but sometimes slow
Features:
- Linear and non-linear power spectra
- Multiple non-linear models (Halofit, HMCode, ...)
- Neutrino effects
- Modified gravity
Example:
from cloelib.cosmology.camb_cosmology import (
CAMBBackground,
CAMBLinearPerturbations,
CAMBNonLinearPerturbations,
)
import numpy as np
# First, create background
bg = CAMBBackground(
H0=67.5,
Omega_b0=0.0492,
Omega_cdm0=0.2650,
As=2.1e-9,
ns=0.965,
alpha_s=0.0, # running of the spectral index
# ... other parameters
)
# Then create linear and non-linear perturbations
redshifts = np.array([0.0, 0.5, 1.0])
linear_pert = CAMBLinearPerturbations(background=bg, redshifts=redshifts)
pert = CAMBNonLinearPerturbations(
background=bg,
linearperturbations=linear_pert,
redshifts=redshifts,
)
# Compute power spectrum
z = np.array([0.0, 0.5, 1.0])
k = np.logspace(-3, 1, 100) # k in 1/Mpc
P_k_z = pert.matter_power_spectrum(z, k)
# Get σ₈
sigma8 = pert.sigma8_0()
print(f"σ₈ = {sigma8:.4f}")
CLASSPerturbations¶
Interfaces with CLASS.
Location: cloelib/cosmology/class_cosmology.py
When to use: CLASS-specific features, comparison studies
Example:
from cloelib.cosmology.class_cosmology import (
CLASSBackground,
CLASSLinearPerturbations,
CLASSNonLinearPerturbations,
)
bg = CLASSBackground(...)
lin = CLASSLinearPerturbations(background=bg, redshifts=zs)
# HMcode2020 with baryonic feedback, as with CAMB and HMcode2020Emu
nonlin = CLASSNonLinearPerturbations(
background=bg, linearperturbations=lin, redshifts=zs, log10TAGN=7.8
)
HMCode2020EmuPerturbations¶
Fast emulator for non-linear power spectra using HMCode2020Emu.
Location: cloelib/cosmology/HMcode2020Emu_cosmology.py
When to use: Fast non-linear predictions, MCMC sampling
Features:
- Lightning-fast emulator
- Accurate non-linear P(k)
- Limited parameter range
EE2Perturbations¶
Simulation-based emulator for non-linear power spectra using euclidemu2.
Location: cloelib/cosmology/EE2_cosmology.py
When to use: Fast non-linear predictions based on simulations, MCMC sampling
Features:
- Lightning-fast (emulator.)
- Accurate DM-only non-linear boost for P_mm
- More limited parameter range
BACCOemuPerturbations¶
Accurate and fast emulators of the linear, non-linear, and baryonic power spectra using BACCOemu.
Location: cloelib/cosmology/baccoemu_cosmology.py
When to use: Predictions for the matter clustering, in the linear and non linear regime. Predictions of baryonic effects.
Features:
- Fast predictions of linear power spectra, growth factors, growth rates, and amplitude of fluctuations;
- Accurate total and cold non-linear matter power spectrum emulated from high-resolution simulations;
- Inclusion of baryonic effects through baryonification;
- Large cosmological parameter range;
- Neural network evaluation with JAX;
FlamingoBaryonResponseEmulatorPerturbations¶
CAMB nonlinear power spectrum with a baryonic suppression correction from the FLAMINGO hydrodynamical simulation suite.
Location: cloelib/cosmology/FlamingoBaryonResponseEmulator_cosmology.py
When to use: Accurate baryonic feedback modelled on FLAMINGO hydrodynamical simulations; can vary AGN jet fraction and ICM gas/stellar parameters.
Features:
- CAMB-based nonlinear baseline
- FLAMINGO emulator response \(B(k,z) = P_\text{hydro}/P_\text{DMO}\) applied at evaluation time
- Three baryonic parameters:
fgas_sigma,Mstar_sigma,jet_fraction - Valid for \(z \leq 3\) and \(k \leq 10^{1.5}\,h/\text{Mpc}\); outside these ranges the response is automatically clamped to 1
- Standalone
FlamingoBaryonBoostMixincomposable with any nonlinear perturbations class
Example:
from cloelib.cosmology.FlamingoBaryonResponseEmulator_cosmology import (
CAMBNonLinearFLAMINGOBaryonicPerturbations,
)
pert = CAMBNonLinearFLAMINGOBaryonicPerturbations(
background=bg,
linearperturbations=lin_pert,
redshifts=zs,
baryon_kwargs=dict(
fgas_sigma=0.0, # 0σ from calibrated gas fraction
Mstar_sigma=0.0, # 0σ from stellar mass function
jet_fraction=0.0, # pure thermal AGN feedback
),
)
k = np.logspace(-2, 1, 100) # k in 1/Mpc
Pk = pert.matter_power_spectrum(np.array([0.5]), k) # shape (n_k,)
ReACTEmu / BoostedPerturbations¶
Modified-gravity nonlinear boost module based on ReACT and the MGEmu emulator package.
Location: cloelib/cosmology/ReACTEmu_cosmology.py
When to use: Fast nonlinear modified-gravity corrections on top of an existing LCDM perturbation pipeline.
Features:
- Supports
fr,dgp,gamma,mu,wCDM,w0waCDM, andidemodel tags - Builds a boost interpolator
B(z, k)and applies it to a baseline nonlinear matter spectrum - Includes configurable high-redshift behavior via
high_z_policyandz_decay - Exposes boosted
matter_power_spectrum,growth_factor, andsigma8_0
Installation:
This extra installs MGEmu together with the TensorFlow support it expects. In environments where you want to drive the baseline spectra with CAMB as well, pip install ".[react,camb]" is a sensible default.
Example:
import numpy as np
from cloelib.cosmology.camb_cosmology import (
CAMBBackground,
CAMBLinearPerturbations,
CAMBNonLinearPerturbations,
)
from cloelib.cosmology.ReACTEmu_cosmology import (
MGemuNonlinearBoost,
BoostedPerturbations,
)
zs = np.array([0.0, 0.5, 1.0])
k = np.logspace(-3, 1, 100)
background = CAMBBackground(...)
linear_pert = CAMBLinearPerturbations(background, zs)
nonlinear_pert = CAMBNonLinearPerturbations(
background, linearperturbations=linear_pert, redshifts=zs
)
boost = MGemuNonlinearBoost(
background,
linear_pert,
zs,
gravity_model="fr",
mgpars={"fr0": 1e-5},
)
mg_pert = BoostedPerturbations(linear_pert, nonlinear_pert, boost.MGboost_interp)
P_mg = mg_pert.matter_power_spectrum(zs, k)
Usage notes:
MGemuNonlinearBoostexpects an existing background plus a linear LCDM perturbation object.BoostedPerturbationsthen combines that boost with a baseline nonlinear spectrum.- Internal wavenumbers follow the standard
cloelibconvention in1/Mpc, even when external emulator data are defined inh/Mpc. - For models other than
wCDM,w0waCDM,ide, andmu, the current implementation assumes a LCDM background withw0 = -1andwa = 0.
Emulator ranges:
fr:Omega_m0.24-0.35,Omega_b0.04-0.06,H063-75,ns0.9-1.01,As1.7e-9-2.5e-9,Omega_nu1e-11-0.00317,fR01e-10-1e-4,z0-2.0dgp:Omega_m0.2899-0.3392,Omega_b0.04044-0.05686,H062.9-73.1,ns0.9432-0.9862,As1.5e-9-2.7e-9,Omega_nu1e-11-0.00317,omegarc1e-3-100,z0-2.4gamma:Omega_m0.2899-0.3392,Omega_b0.04044-0.05686,H063.8-73.1,ns0.9432-0.9862,As1.9511e-9-2.2669e-9,Omega_nu1e-11-0.00317,gamma0-1,q1-5 to 5,z0-2.4mu:Omega_m0.24-0.4,H060-84,As1.7e-9-2.5e-9,w0-1.5 to -0.5,wa-0.5 to 0.5,mu0-0.999 to 3,c1-0.3333 to 1,lam0-2,q1/q2/q3-2 to 2,z0-2.5wCDM,w0waCDM,ide(shareddsbackend):Omega_m0.22-0.37,Omega_b0.03-0.08,H063-84,ns0.8-1.1,As1.7e-9-2.5e-9,Omega_nu1e-11-0.00317,w0-1.3 to -0.5,wa-2 to 0.5,xi0-150,z0-2.5
Stay within these training ranges when sampling. Outside them, the implementation clips emulator inputs and applies its configured high-redshift policy, which is convenient for robustness but should not be treated as a new calibration region.
TabulatedBoost / TabulatedBoostedPerturbations¶
Lightweight wrapper for applying a tabulated beyond-LCDM nonlinear boost to an existing nonlinear matter power spectrum.
Location: cloelib/cosmology/TabulatedBoost_cosmology.py
When to use: You already have boost data from simulations or another external pipeline and want to interpolate it onto the cloelib perturbation grid.
Features:
- Reads tabulated boost files with columns
[k, B(z_1), B(z_2), ...] - Converts input
kvalues fromh/Mpcto the internal1/Mpcconvention - Reuses
cloelib's wavenumber extrapolation machinery - Supports
power_law,freeze,one, andtaperhigh-redshift policies - Exposes a
TabulatedBoostedPerturbationswrapper with boostedmatter_power_spectrum,growth_factor, andsigma8_0
Input format:
- Column 1:
kinh/Mpc - Remaining columns: boost values
B(k, z_i)at each tabulated redshift z_colsmust list the redshifts corresponding to those boost columns, in the same order as the file
Example:
import numpy as np
from cloelib.cosmology.camb_cosmology import (
CAMBBackground,
CAMBLinearPerturbations,
)
from cloelib.cosmology.HMcode2020Emu_cosmology import HMemuNonLinearPerturbations
from cloelib.cosmology.TabulatedBoost_cosmology import (
TabulatedNonlinearBoost,
TabulatedBoostedPerturbations,
)
zs = np.linspace(0.0, 4.0, 100)
z_cols = [
3.017980,
2.479559,
2.161320,
2.013288,
1.609499,
1.259818,
1.000000,
0.823800,
0.677100,
0.552300,
0.444200,
0.349200,
0.264800,
0.188900,
0.120200,
0.057540,
0.000031,
0.0,
]
background = CAMBBackground(...)
linear_perturbations = CAMBLinearPerturbations(background, zs)
nonlinear_perturbations = HMemuNonLinearPerturbations(
background,
linear_perturbations,
zs,
)
tabulated_boost = TabulatedNonlinearBoost(
background,
linear_perturbations,
zs,
"m11_p01_boosts.txt",
z_cols,
high_z_policy="freeze",
)
boosted_perturbations = TabulatedBoostedPerturbations(
linear_perturbations,
nonlinear_perturbations,
tabulated_boost.MGboost_interp,
)
CosmoPowerJAXPerturbations¶
Fast JAX-based emulator for linear and nonlinear power spectra using cosmopower-jax.
Location: cloelib/cosmology/cosmopower_jax_cosmology.py
When to use: Fast predictions with full JAX compatibility, gradient-based inference, MCMC sampling
Features:
- Full JAX compatibility — automatic differentiation and JIT compilation
- Linear and nonlinear P(k) and P_cb(k)
- Two nonlinear prescriptions: HMcode2020 (with baryonic feedback) and halofit (dark-matter-only)
- σ₈(z), fσ₈(z), growth factors D(z,k) and D_cb(z,k), growth rate f(z)
- Baseline (ΛCDM/wCDM/w0waCDM) and extended (curvature, running spectral index) cosmologies
- Emulator files downloaded automatically on first use from the Euclid-DR1-matter-emulators GitHub repository; the k-mode grid is read directly from each emulator
Available classes¶
Every Perturbations class exposes the same inner classes, each providing both
the total-matter spectrum (matter_power_spectrum) and the CDM+baryon spectrum
(matter_power_spectrum_cb, built at construction from the paired cb emulator), with
the cached Pk/Pk_cb grids and growth_factor/growth_factor_cb:
Linear— linear P(k) and P_cb(k)NonLinear— nonlinear P(k) and P_cb(k) using HMcode2020, with baryonic feedback controlled bylog10TAGNNonLinearHalofit— nonlinear P(k) and P_cb(k) using halofit (Takahashi 2012); dark-matter-only, solog10TAGNis accepted for interface compatibility but ignored.
| Class | Cosmology | N_mnu | Nonlinear recipes |
|---|---|---|---|
CosmoPowerJAXLCDMPerturbations |
ΛCDM | 0,1,2,3 | HMcode2020, halofit |
CosmoPowerJAXwCDMPerturbations |
wCDM | 0,1,2,3 | HMcode2020, halofit |
CosmoPowerJAXw0waCDMPerturbations |
w0waCDM | 0,1,2,3 | HMcode2020, halofit |
CosmoPowerJAXLCDMCurvaturePerturbations |
ΛCDM + curvature (Ω_k) | 0,1,3 | HMcode2020 |
CosmoPowerJAXw0waCurvaturePerturbations |
w0waCDM + curvature | 0,1,3 | HMcode2020 |
CosmoPowerJAXLCDMRunningIndexPerturbations |
ΛCDM + running (α_s) | 0,1,3 | HMcode2020 |
CosmoPowerJAXw0waRunningIndexPerturbations |
w0waCDM + running | 0,1,3 | HMcode2020 |
The neutrino configuration is selected automatically from background.N_mnu: 0 (massless), 1 (one massive), 2 (two degenerate), 3 (three degenerate). Baseline cosmologies cover 0–3; the extended (curvature / running) cosmologies cover 0, 1 and 3 (an unsupported value raises ValueError).
Selecting the nonlinear prescription¶
The recipe is chosen by which nonlinear class you use — the constructor signature is identical, so it is a drop-in swap:
# HMcode2020 (default) — baryonic feedback via log10TAGN
nl = CosmoPowerJAXLCDMPerturbations.NonLinear(
background=bg, linearperturbations=lin, redshifts=zs, log10TAGN=7.8
)
# halofit — dark-matter-only (log10TAGN ignored)
nl = CosmoPowerJAXLCDMPerturbations.NonLinearHalofit(
background=bg, linearperturbations=lin, redshifts=zs
)
Parameter ranges¶
Inputs are validated against a single global guardrail (CP_EMULATOR_BOUNDS); a parameter outside its range raises ValueError. The redshift z is not bounds-checked (it is clipped to the training range and extrapolated; z=0 is always added to the emulator grid, so σ₈(z=0) and the D(z=0)=1 normalisation are exact even when the requested redshifts start above zero). These global bounds are deliberately loose — each emulator's actual training box is tighter and varies by family.
| Parameter | Enforced range | Applies to |
|---|---|---|
| ombh2 | [0.001, 0.1] | all |
| omch2 | [0.05, 0.9] | all |
| H0 | [20, 100] | all |
| ns | [0.6, 1.3] | all |
| lnAs | [1.61, 5.0] | all |
| mnu | [0, 1] eV | massive-neutrino runs |
| w0 | [-3, 1] | w0waCDM |
| wa | [-3, 3] | w0waCDM |
| w | [-3, 0] | wCDM |
| Omega_k0 | [-0.3, 0.3] | curvature |
| alpha_s | [-0.3, 0.3] | running |
| log10TAGN | [7.3, 8.5] | HMcode nonlinear |
Example¶
import numpy as np
from cloelib.cosmology.camb_cosmology import CAMBBackground
from cloelib.cosmology.cosmopower_jax_cosmology import CosmoPowerJAXLCDMPerturbations
zs = np.array([0.0, 0.5, 1.0, 2.0])
ks = np.logspace(-4, 1, 200)
bg = CAMBBackground(
H0=67.0, Omega_b0=0.049, Omega_cdm0=0.270,
As=2.1e-9, ns=0.96, mnu=0.06, N_mnu=1,
w0=-1.0, wa=0.0, Omega_k0=0.0, gamma_MG=0.0,
)
# Linear P(k)
lin = CosmoPowerJAXLCDMPerturbations.Linear(background=bg, redshifts=zs)
Pk = lin.matter_power_spectrum(0.0, ks) # shape (1, len(ks))
# Nonlinear P(k) with baryonic feedback (HMcode2020)
nl = CosmoPowerJAXLCDMPerturbations.NonLinear(
background=bg, linearperturbations=lin, redshifts=zs, log10TAGN=7.6
)
Pk_nl = nl.matter_power_spectrum(0.0, ks)
# sigma8 and fsigma8 as a function of redshift
print(f"sigma8(z=0) = {lin.sigma8[0]:.4f}")
print(f"fsigma8(z=0) = {lin.fsigma8[0]:.4f}")
JAXPerturbations¶
Pure JAX implementation for automatic differentiation.
Location: cloelib/cosmology/jax_cosmology.py
When to use: Computing gradients, Fisher forecasts, HMC sampling
MG CosmoPower-JAX boost perturbations¶
Applies a CosmoPower-JAX modified-gravity boost \(B(k,z) = P*\mathrm{MG}/P*{\Lambda\mathrm{CDM}}\) as a multiplicative operator on top of an external LCDM baseline perturbation object, so that the GR limit (\(\mu = \eta = 1\)) reproduces the baseline spectrum up to the boost emulators' own residual at GR (\(|B-1| \lesssim 3\times10^{-4}\) for \(k \le 1\,h/\mathrm{Mpc}\), up to \(2.5\times10^{-3}\) at \(k \sim 10\,h/\mathrm{Mpc}\) for the nonlinear multi-bin emulator); no regridding error is introduced.
Location: cloelib/cosmology/mg_cosmopower_jax_cosmology.py
When to use: Phenomenological \(\mu(z), \eta(z)\) modified gravity where the
nonlinear MG power spectrum is obtained by boosting a fast LCDM emulator
(e.g. CosmoPowerJAXLCDMPerturbations) rather than solving MG directly.
Two modes, selected by MGParams.bin_index:
- Single-bin (
bin_indexis an int): \(\mu, \eta\) vary in one redshift bin (per-bin emulatorsmg-boost-{linear,nonlinear}-bin{0..4}.npz); - Multi-bin (
bin_index=None): \(\mu\) (and \(\eta\), linear only) vary in all five bins simultaneously (joint emulatorsmg-boost-{linear,nonlinear}-multibin.npz).
Features:
- Emulator-based MG boost applied to any external LCDM
Perturbationssolver; - Provides the modified lensing parameter \(\Sigma(z) = \mu(1+\eta)/2\) (a step
function over the redshift bins), applied to every lensing kernel: cosmic shear,
magnification bias and CMB lensing (
photo.shear,photo.positions,cmb); - Scale-independent growth rate \(f(z)\) and \(\sigma_8\) from an internally-built linear MG \(P(k)\); the boost is applied on the baseline k-grid, so the GR limit is recovered up to the emulators' own residual (no regridding error);
- Mutable
MGParamsholder for injecting sampled \(\mu, \eta\) each likelihood call; - Implemented in module-level
MGLinearPerturbations/MGNonLinearPerturbations(statically checked against thePerturbationsprotocol);mg_perturbationsreturns subclasses with the MG parameters and LCDM baseline bound; - Emulators are downloaded on first use from the Euclid-DR1-matter-emulators
GitHub repository and cached locally, mirroring
CosmoPowerJAXPerturbations(no local model directory needed);
Example:
import numpy as np
from cloelib.cosmology.cosmopower_jax_cosmology import CosmoPowerJAXLCDMPerturbations as LCDM
from cloelib.cosmology.mg_cosmopower_jax_cosmology import MGParams, mg_perturbations
# single-bin (bin 4); use MGParams(mu=..., eta=...) with bin_index=None for multi-bin
mg = MGParams(mu=1.0, eta=1.0, bin_index=4)
Lin, NonLin = mg_perturbations(
mg, baseline_linear=LCDM.Linear, baseline_nonlinear=LCDM.NonLinear
)
# in the sampling wrapper, before each loglike: mg.mu, mg.eta = ...
Emulator ranges (enforced; predictions outside raise ValueError):
| Parameter | Single-bin | Multi-bin |
|---|---|---|
| \(\Omega_m\) | 0.25–0.35 | 0.25–0.40 |
| \(\Omega_b\) | 0.040–0.055 | 0.040–0.055 |
| \(h\) | 0.65–0.73 | 0.65–0.75 |
| \(n_s\) | 0.95–1.00 | 0.80–1.20 |
| \(\ln(10^{10}A_s)\) | 2.996–3.091 | 2.944–3.219 |
| \(\mu\) (per bin) | 0.9–1.1 | 0.9–1.1 |
| \(\eta\) (per bin) | 0.9–1.1 | 0.9–1.1 |
| \(z\) | 0.01–3 | 0–3 |
The \(z\) upper edge is enforced (and handled by the redshift clamp); the lower edge is not, so \(z = 0\) remains available for the growth/\(\sigma_8\) normalisation.
MGCLASSPerturbations¶
Interfaces with MGCLASS.
Location: cloelib/cosmology/mgclass_cosmology.py
When to use: MGCLASS-specific features, comparison studies
Example:
from cloelib.cosmology.mgclass_cosmology import MGCLASSBackground, MGCLASSPerturbations
bg = MGCLASSBackground(...)
pert = MGCLASSPerturbations(
background=bg,
# other parameters
)
hi_classPerturbations¶
Interfaces with hi_class.
Location: cloelib/cosmology/hi_class_cosmology.py
When to use: hi_class-specific features, comparison studies
Example:
from cloelib.cosmology.hi_class_cosmology import hi_classBackground, hi_classPerturbations
bg = hi_classBackground(...)
pert = hi_classPerturbations(
background=bg,
# other parameters
)
EmantisPerturbations¶
Accurate and fast emulator of the nonlinear matter power spectrum in modified gravity using e-MANTIS.
Location: cloelib/cosmology/emantis_cosmology.py
When to use: Predictions for the nonlinear matter clustering in f(R) gravity.
Features:
- Fast predictions of the nonlinear matter power spectrum in f(R) gravity;
- Accurate emulation of the nonlinear modified gravity boost based on N-body simulations;
- Limited to the Hu & Sawicki model (n=1) with fR0 as free parameter;
Baryonic Boost Mixin Framework¶
cloelib provides a composable BaryonBoostMixin Protocol that decouples the baryonic suppression from the underlying nonlinear solver. This makes it easy to combine any nonlinear backend with any baryonic model.
Protocol location: cloelib.cosmology.cosmology.BaryonBoostMixin
BaryonBoostMixin Protocol¶
Any class implementing this protocol must expose:
def baryonic_suppression(self, zs, ks, k_hunit=False) -> np.ndarray:
"""Return B(k,z) = P_hydro / P_DMO, shape (n_z, n_k)."""
Three ready-made mixins are provided:
| Mixin | Backend | Baryonic parameters |
|---|---|---|
FlamingoBaryonBoostMixin |
FLAMINGO emulator | fgas_sigma, Mstar_sigma, jet_fraction |
BACCOemuBaryonBoostMixin |
BACCOemu | M_c, eta, beta, M1_z0_cen, theta_out, theta_inn, M_inn |
HMcode2020BaryonBoostMixin |
HMcode2020emu | log10TAGN |
Each mixin __init__ must be called after the base nonlinear __init__ (it reads self.background and emulator internals set by the base).
with_baryon_boost Factory¶
cloelib.cosmology.cosmology.with_baryon_boost(NonLinearClass, BaryonMixinClass) creates a combined class with zero boilerplate:
from cloelib.cosmology.cosmology import with_baryon_boost
from cloelib.cosmology.baccoemu_cosmology import BACCOemuNonLinearPerturbations
from cloelib.cosmology.FlamingoBaryonResponseEmulator_cosmology import FlamingoBaryonBoostMixin
BACCOemuFLAMINGO = with_baryon_boost(BACCOemuNonLinearPerturbations, FlamingoBaryonBoostMixin)
pert = BACCOemuFLAMINGO(
background, linear_pert, redshifts,
nonlinear_model_name="Arico2023",
baryon_kwargs=dict(fgas_sigma=0.0, Mstar_sigma=0.0, jet_fraction=0.0),
)
The resulting object is fully Perturbations-compatible and can be passed directly to any cloelib tracer or likelihood.
Total vs. cold matter boost
The same suppression \(B(k,z)\) is applied to matter_power_spectrum and to
matter_power_spectrum_cb. The backends do not all define the response the same
way — FLAMINGO's is \((b + \mathrm{cdm} + \nu)/(\mathrm{cdm} + \nu)\) while
BACCOemu's is the cold \((b + \mathrm{cdm})/(\mathrm{cdm} + \nu)\) — and one could
refine this by adding the neutrino contribution at linear order. The difference is
~\(10^{-5}\) even for \(m_\nu = 0.4\) eV, and FLAMINGO simulations show total and cold
boosts to be equivalent at that level, so cloelib treats them as the same.
Return shape
All matter_power_spectrum methods (including baryonic ones) return shape (n_z, n_k), also for a single redshift, which gives (1, n_k). baryonic_suppression returns the same shape, so the boosted spectra keep it.
Adding Your Own Perturbations Implementation¶
To add a new Perturbations implementation, follow these steps.
Step 1: Create Your Class¶
Create cloelib/cosmology/my_solver_cosmology.py:
from cloelib.cosmology.cosmology import Perturbations, Background
import numpy as np
class MySolverPerturbations:
"""Interface to MySolver for perturbation calculations."""
def __init__(
self,
background: Background,
linearperturbations: Optional[object] = None,
redshifts: np.ndarray = None,
nonlinear_model: str = "halofit",
kmax: float = 10.0,
zmax: float = 5.0,
**kwargs
):
"""
Initialize perturbation calculator.
Args:
background: Background object (any implementation)
linearperturbations: Linear perturbations object. Accepted for interface
compatibility with cloelike, which always passes this as the second
positional argument when constructing NonLinPerturbations. Unused by
codes that compute nonlinear corrections internally (e.g. CAMB, CLASS).
redshifts: Array of redshifts for the calculations.
nonlinear_model: Which non-linear model to use
kmax: Maximum wavenumber in 1/Mpc
zmax: Maximum redshift
"""
self._background = background
self.nonlinear_model = nonlinear_model
# Initialize your solver
self._solver = MySolver(
H0=background.H0,
Omega_b=background.Omega_b0,
# ... pass cosmological parameters from background
)
# Set up k and z grids
self._solver.set_k_grid(kmin=1e-4, kmax=kmax, nk=200)
self._solver.set_z_grid(zmin=0.0, zmax=zmax, nz=50)
# Compute power spectrum
self._solver.compute_power_spectrum(nonlinear=nonlinear_model)
@property
def background(self) -> Background:
"""Return the background object."""
return self._background
def matter_power_spectrum(self, zs: np.ndarray, ks: np.ndarray) -> np.ndarray:
"""
Compute the total matter power spectrum P_mm(k, z).
Args:
zs: Redshifts (1D array)
ks: Wavenumbers in 1/Mpc (1D array)
Returns:
P(k, z) in Mpc³, shape (len(zs), len(ks))
"""
# Ensure inputs are arrays
zs = np.atleast_1d(zs)
ks = np.atleast_1d(ks)
# Call solver and interpolate
P_k_z = self._solver.get_power_spectrum(zs, ks)
return P_k_z
def matter_power_spectrum_cb(self, zs: np.ndarray, ks: np.ndarray) -> np.ndarray:
"""
Compute the CDM+baryons matter power spectrum P_cb(k, z).
Args:
zs: Redshifts (1D array)
ks: Wavenumbers in 1/Mpc (1D array)
Returns:
P_cb(k, z) in Mpc³, shape (len(zs), len(ks))
"""
zs = np.atleast_1d(zs)
ks = np.atleast_1d(ks)
# If the solver distinguishes cb from total matter, use the dedicated method;
# otherwise fall back to the total matter power spectrum
P_cb_k_z = self._solver.get_cb_power_spectrum(zs, ks)
return P_cb_k_z
def growth_factor(self, zs: np.ndarray, ks: np.ndarray) -> np.ndarray:
"""Calculate linear growth factor D(z, k)."""
zs = np.atleast_1d(zs)
ks = np.atleast_1d(ks)
# Most codes compute scale-independent D(z)
# If scale-dependent, use ks; otherwise ignore
D_z = self._solver.get_growth_factor(zs)
# Return shape (len(zs), len(ks)) even if scale-independent
return D_z[:, np.newaxis] * np.ones((len(zs), len(ks)))
def growth_rate(self, zs: np.ndarray | None = None, ks: np.ndarray | None = None) -> np.ndarray:
"""Calculate growth rate f(z) = d ln D / d ln a."""
if zs is None:
raise ValueError("zs must be provided")
zs = np.atleast_1d(zs)
f_z = self._solver.get_growth_rate(zs)
# If ks provided, expand to 2D array
if ks is not None:
ks = np.atleast_1d(ks)
f_z = f_z[:, np.newaxis] * np.ones((len(zs), len(ks)))
return f_z
def sigma8_0(self) -> float:
"""Compute σ₈ at z=0."""
return self._solver.get_sigma8()
Step 2: Connect to Background¶
The key pattern: your Perturbations wraps around a Background object.
# Users do this:
bg = CAMBBackground(...) # Or any Background implementation
pert = MySolverPerturbations(background=bg)
# Now pert can access:
H_z = pert.background.hubble_parameter(z)
chi = pert.background.comoving_distance(z)
This means implementations can be mixed and matched — for example, using a CAMB background with a custom perturbations class.
Step 3: Handle Array Shapes¶
Pay attention to array shapes—it's easy to get confused! cloelib will always return arrays!
def matter_power_spectrum(self, zs, ks):
"""
Output shape: (len(zs), len(ks))
Example:
zs = [0.0, 0.5, 1.0] # 3 redshifts
ks = [0.1, 0.2, ...] # 100 wavenumbers
P_k_z has shape (3, 100)
"""
pass
Step 4: Add Tests¶
Create tests/test_my_solver_cosmology.py:
import pytest
import numpy as np
from cloelib.cosmology.camb_cosmology import CAMBBackground
from cloelib.cosmology.my_solver_cosmology import MySolverPerturbations
@pytest.fixture
def background():
"""Create a test background."""
return CAMBBackground(
H0=67.5,
Omega_b0=0.0492,
Omega_cdm0=0.2650,
As=2.1e-9,
ns=0.965,
# ... minimal parameters
)
def test_initialization(background):
"""Test Perturbations initialization."""
pert = MySolverPerturbations(background=background)
assert pert.background is background
def test_matter_power_spectrum(background):
"""Test P(k, z) calculation."""
pert = MySolverPerturbations(background=background)
z = np.array([0.0, 0.5, 1.0])
k = np.logspace(-2, 1, 50)
P_k_z = pert.matter_power_spectrum(z, k)
# Check shape
assert P_k_z.shape == (len(z), len(k))
# Check positivity
assert np.all(P_k_z > 0)
# Check power decreases at high z (structure less developed)
assert np.all(P_k_z[0, :] > P_k_z[-1, :])
def test_sigma8(background):
"""Test σ₈ calculation."""
pert = MySolverPerturbations(background=background)
sigma8 = pert.sigma8_0()
# Reasonable range for σ₈
assert 0.5 < sigma8 < 1.2
def test_growth_factor(background):
"""Test growth factor calculation."""
pert = MySolverPerturbations(background=background)
z = np.array([0.0, 1.0, 2.0])
k = np.array([0.1, 1.0])
D = pert.growth_factor(z, k)
# Check shape
assert D.shape == (len(z), len(k))
# Check D decreases with z (earlier = less grown)
assert np.all(D[0, :] > D[1, :])
assert np.all(D[1, :] > D[2, :])
Also add the new classes to the shared interface tests:
tests/test_perturbations_interface.py: add a builder toBACKENDS, so the class is checked at runtime against the commongrowth_rateconvention.tests/typing/perturbations_conformance.py: add the class, so ty checks that it implementsPerturbationsand that its constructor matchesLinearPerturbationsFactory/NonLinearPerturbationsFactory.
Step 5: Update Documentation¶
- Add to
docs/api.md - Update supported codes table in
README.md - Add usage examples within the
playrgoundrepository
Interface Patterns¶
Working with External Codes¶
Most Perturbations implementations wrap external Boltzmann codes:
class MyPerturbations:
def __init__(self, background, ...):
# Extract parameters from background
params = {
'h': background.h,
'Omega_b': background.Omega_b0,
'Omega_c': background.Omega_cdm0,
# ... more parameters
}
# Initialize external code
self._external = ExternalCode(**params)
self._external.compute()
# Store for later use
self._z_grid = ...
self._k_grid = ...
self._P_k_z_grid = ...
Tips & Tricks¶
Non-linear vs. Linear Power Spectra¶
Make it clear what you are computing linear or non-linear power spectra at the level of the protocol. The protocols should always return this method and their corresponding CDM and baryon only matter power spectrum _cb.
Parameter Validation¶
Check inputs early:
def matter_power_spectrum(self, zs, ks):
if np.any(ks <= 0):
raise ValueError("Wavenumbers must be positive")
if np.any(zs < 0):
raise ValueError("Redshifts must be non-negative")
if np.any(ks > self.kmax):
raise ValueError(f"k exceeds kmax={self.kmax}")
Next Steps¶
Now that you have a grounding in Perturbations, explore:
- Observables – Connect structure to survey measurements
- Summary Statistics – Compute final data products
- Background – Review the foundation
- API Reference – Full technical details