Distances and expansion history¶
class-e emulates the background observables a BAO, supernova or CMB-angle likelihood needs: \(H(z)\), the comoving distance \(\chi(z)\) and everything built from them (\(D_M\), \(D_A\), \(D_L\), \(D_H\), \(D_V\)), plus the scalars of the recombination era (\(r_d\), \(z_d\), \(r_*\), \(z_*\), \(\theta_*\)).
The point is to get them from one emulator, consistent with each other. Since
and integration is linear, the emulator learns the integrand on a redshift grid; \(H(z)\) and \(\chi(z)\) at any redshift follow from the same prediction, with \(\chi' = c/H\) exactly. This is the "integral of a GP is a GP" idea, applied along \(z\) (the GP itself is over the cosmological parameters).
Generate¶
Background observables come from a CLASS run that stops after the thermodynamics (about 0.05 s per point), so large training sets are cheap:
theory:
classy:
extra_args:
output: "" # background and thermodynamics only
tol_background_integration: 1.0e-14 # strongly phantom w0wa: see below
background:
z_max: 1200.0 # or an explicit grid: z: [0.0, 0.1, ...]
n_z: 100 # uniform in ln(1+z), from z = 0
observables: [Hz, chi, rs_drag, z_drag, rs_star, z_star, theta_star_100]
| Observable | Content | Grid |
|---|---|---|
Hz |
\(H(z)\) in km/s/Mpc | z_bg |
chi |
\(\chi(z)\) in Mpc (for validation: the emulator gets it from Hz) |
z_bg |
rs_drag, rs_star |
comoving sound horizon at the drag epoch and at \(z_*\), Mpc | — |
z_drag, z_star |
their redshifts | — |
theta_star_100, theta_s_100 |
\(100\,\theta_*\), and CLASS's \(100\,\theta_s\) (the shooting input) | — |
H0 |
km/s/Mpc (for models sampled in \(\theta_s\)) | — |
Ready-made configurations: configs/lcdm-background.yaml and configs/w0wacdm-mnu-background.yaml.
Background tolerance for phantom dark energy
At CLASS's default tol_background_integration (1e-10), strongly phantom points (\(w_0 + w_a \approx -4\)) miss \(H_0 = 100h\) by up to 0.2% and \(\chi(1100)\) by \(2\times10^{-4}\). 1.0e-14 brings this to \(\sim10^{-6}\) at no extra cost; every w0wa configuration sets it.
Train¶
With grid/z_bg in the dataset, Hz trains a DistanceEmulator and chi is left out (it comes with Hz). The training section of configs/w0wacdm-mnu-background.yaml:
train:
test_design: test
backend: jax
defaults:
n_pca: 1
inputs: {h: h, omega_b: omega_b, omega_cdm: omega_cdm, m_nu: m_nu, w0: w0, s: "w0 + wa"}
baseline: {kind: cpl, m_ncdm: m_nu, w0: w0, wa: wa, N_ur: 2.0308}
observables:
Hz: {n_pca: 16}
rs_drag: {}
theta_star_100: {}
out: bundles/w0wacdm-mnu-background.cemu
The baseline is what makes it accurate¶
The emulator does not learn \(H(z)\) itself but its ratio to an analytic baseline, baseline: {kind: cpl, ...}:
where each density is an expression of your parameters (h, omega_m, w0, wa, Omega_k, m_ncdm, N_ur, ...). Massive neutrinos follow the Komatsu et al. (2011) fit (radiation early, matter late, as in CLASS), which matches CLASS's \(H(z)\) to \(\lesssim 6\times10^{-5}\) at every redshift; counting them as matter misses it by 1% above \(z \sim 100\). The emulator then learns a ratio within \(10^{-4}\) of one.
The scalars take the same baseline: they become RatioEmulators over a RecombinationBaseline, the sound horizon \(r_s = \int_z^\infty c_s\,dz'/H\) on the analytic background (with fitting formulas for \(z_d\) and \(z_*\)). This matters for w0waCDM: as \(w_0 + w_a \to 0\) dark energy behaves like matter at early times and \(r_s\) drops by up to 10% (\(\theta_*\) moves by 20%), a sharp edge a GP fits poorly; the baseline follows it and leaves a ratio within \(\pm0.3\%\).
Models sampled in \(\theta_s\), and one bundle for CMB and background¶
For a model sampled in \(\theta_s\) rather than \(h\), write h: "H0 / 100": the expressions may read trained scalar observables, whose emulators are fitted first (H0 before everything) and stored with the wrapper.
The background observables come from the same CLASS run as the spectra, so a CMB configuration can list them too and train one bundle that serves CMB, BAO, SN and \(\theta_*\) likelihoods (configs/w0wacdm-mnu-theta.yaml). Give each emulator only the parameters it depends on, with per-observable inputs:
observables: [TT, TE, EE, Hz, H0, rs_drag, z_drag, rs_star, z_star, theta_star_100]
train:
inputs: {theta_s_100: theta_s_100, ..., logA: logA, n_s: n_s, tau_reio: tau_reio, w0: w0, s: "w0 + wa"}
observables:
TT: {}
H0: {n_pca: 1, inputs: &background {theta_s_100: theta_s_100, omega_b: omega_b, omega_cdm: omega_cdm,
w0: w0, s: "w0 + wa"}}
Hz: {n_pca: 16, log_space: false, inputs: *background,
baseline: &cpl {kind: cpl, h: "H0 / 100", m_ncdm: "0.06", w0: w0, wa: wa, N_ur: 2.0308}}
theta_star_100: {n_pca: 1, inputs: {<<: *background, tau_reio: tau_reio}, baseline: *cpl}
CLASS's \(z_*\) depends on reionization
CLASS defines \(z_*\) by an optical depth of one, reionization included. Across \(\tau_{\rm reio} \in [0.01, 0.125]\), \(z_*\) moves by 1%, \(r_s(z_*)\) and \(\theta_*\) by 0.6%, \(r_d\) by \(10^{-4}\); \(100\,\theta_s\) (at the visibility peak, CLASS's shooting input) and \(H(z)\) do not move. Emulators of z_star, rs_star, theta_star_100 (and, for \(10^{-5}\) accuracy, rs_drag, z_drag) must see tau_reio when it varies.
Without a baseline (baseline: mean, the default) the ratio is to the training mean: fine for \(\Lambda\)CDM, poor across a wide w0wa box.
Use¶
from class_e import EmulatorBundle
bundle = EmulatorBundle.load("bundles/w0wacdm-mnu-background.cemu")
hz = bundle["Hz"] # a DistanceEmulator
z = [0.295, 0.51, 0.706, 0.934, 1.321, 2.33]
d = hz.distances(theta, z) # H, chi, D_M, D_A, D_L, D_H, D_V; (M, len(z))
d["D_M"] / bundle["rs_drag"].predict(theta) # BAO D_M / r_d
hz.comoving_distance_jax(theta_1d, z) # differentiable (JAX backend)
Any \(z\) from 0 to the grid's \(z_{\max}\) works. distances takes the curvature from the cpl baseline (its Omega_k), or Omega_k= explicitly, and turns \(\chi\) into \(D_M\) with a form exact and smooth through \(\Omega_k = 0\).
Accuracy¶
Held-out CLASS points, relative errors (JAX backend, float64, 16 PCA components for Hz).
| Model, training points | Quantity | p50 | p99 | max |
|---|---|---|---|---|
| \(\Lambda\)CDM + \(m_\nu\) = 0.06 eV, 512 | \(\chi(z)\) | \(3\times10^{-9}\) | \(2\times10^{-7}\) | \(7\times10^{-7}\) |
| \(\theta_*\) | \(3\times10^{-8}\) | \(1\times10^{-7}\) | \(1\times10^{-7}\) | |
| w0waCDM + free \(m_\nu\), 2048 | \(\chi(z)\) | \(5\times10^{-9}\) | \(9\times10^{-8}\) | \(6\times10^{-7}\) |
| \(H(z)\), \(z < 3\) | \(3\times10^{-9}\) | \(1\times10^{-7}\) | \(7\times10^{-7}\) | |
| \(H(z)\), \(z > 100\) | \(4\times10^{-8}\) | \(8\times10^{-7}\) | \(3\times10^{-6}\) | |
| \(r_d\) | \(8\times10^{-7}\) | \(2\times10^{-5}\) | \(8\times10^{-5}\) | |
| \(\theta_*\) | \(8\times10^{-7}\) | \(3\times10^{-5}\) | \(1\times10^{-4}\) |
Without the baseline, the w0wa errors are \(10^3\)–\(10^4\) times larger (\(\chi\) worst \(9\times10^{-3}\), \(\theta_*\) worst \(1.3\times10^{-2}\)); counting the neutrinos as matter leaves \(H(z > 100)\) at \(3\times10^{-3}\). The largest remaining scalar errors sit at the prior edge \(w_0 + w_a \to 0\), where only ~7% of the training points fall; a denser design there reduces them.
Use float64
In float32 the JAX backend loses about a factor 50 on these targets. The CLI enables float64; from Python call class_e.stats.jax_config.configure_jax(x64=True) first.
How it works¶
DistanceEmulator wraps any backend. On the grid nodes \(z_i\) (uniform in \(x = \ln(1+z)\)) it learns
with \(b\) the baseline. Between nodes \(r\) is a cubic spline, so \(H(z) = c(1+z)/(r\,b)\) at any \(z\), and \(\chi\) is a composite Gauss-Legendre integral of \(r\,b\), exact for the spline. Both are fixed linear operators applied to the emulator's output. With 100 nodes up to \(z = 1200\), the quadrature error is \(\sim10^{-9}\) with a CPL baseline.
See the Emulators API.