Solver Adapters¶
cplots plots accept raw arrays, but they also accept solver outputs directly. An adapter translates one code's results into the cplots data model, using a shared vocabulary of quantity names and units, so every plot type works with every code and comparing codes needs no glue.
Built-in adapters:
| Code | Adapter | Wraps | Outputs |
|---|---|---|---|
| CLASS / hi_class | ClassOutput |
a computed classy.Class |
background, P(k), Cℓ, primordial, thermodynamics |
| CAMB | CambOutput |
a camb.CAMBdata |
background, P(k), Cℓ, primordial |
| eft4py | Eft4pyOutput |
a BackgroundGrid or BackgroundSolution |
background |
Quick start: as_observables¶
You rarely need to name the adapter. cplots.as_observables picks it from the type of
the result, and plots call it for you:
import cplots
out = cplots.as_observables(cosmo) # classy.Class -> ClassOutput
bg = out.background(quantities=["H", "dA"], units={"H": "km/s/Mpc"})
cplots.BackgroundEvolution(bg).show()
# or let the plot wrap it
cplots.BackgroundEvolution(cosmo, ["H", "dA"]).show()
Adapters register lazily: import cplots imports none of classy, camb or eft4py.
An Observables instance passed to as_observables is returned unchanged.
The Observables interface¶
Every adapter subclasses cplots.Observables. All outputs are optional: a method a code
does not provide raises NotImplementedError, and supports() tells which ones exist.
| Method | Returns | Notes |
|---|---|---|
background(quantities=None, *, labels, units, z) |
BackgroundData |
shared implementation, see below |
pk(z, **kwargs) |
PKData |
|
cl(**kwargs) |
dict[str, ClData] |
|
primordial(**kwargs) |
PKData |
|
thermodynamics(**kwargs) |
BackgroundData |
background()¶
background() is the same for every code:
quantities: canonical names (see the vocabulary) or a code's raw names. Unknown names raiseKeyErrorlisting what is available, also given byout.available_background().units: per-quantity target units, e.g.{"H": "km/s/Mpc"}or{"dA": "Gpc"}. Each code returns its native units by default.z: the redshift grid. Codes that evaluate on demand (CAMB) evaluate there; codes that tabulate (CLASS, eft4py) are interpolated, cubic Hermite in $\ln a$. Outside the table range it raisesValueError. Withoutz, you get the code's own grid.labels: display labels, overriding the generated unit-aware ones.
The result, a BackgroundData, records the units of every quantity and the source
code, which is what lets plots compare codes safely.
H(z) native units
ClassOutput and Eft4pyOutput return H in 1/Mpc, CambOutput in km/s/Mpc. Ask
for the same units from each, or let an overlay plot convert them (below).
Comparing codes¶
Pass a {label: source} mapping to BackgroundEvolution. Sources can be adapters,
raw solver results, or BackgroundData. Each source gets its own line style, each
quantity its own colour, and every source is converted to the first one's units (or to
units=). Mismatched dimensions raise ValueError.
cplots.BackgroundEvolution(
{"hi_class": cosmo, "eft4py": grid},
["w_de"],
z=np.linspace(0, 3, 100),
latex_labels=True,
).show()
Quantities of different kinds (a rate, distances, dimensionless functions) do not
belong on one axis. BackgroundPanels gives each kind its own panel:
cplots.BackgroundPanels(
{"hi_class": cosmo, "eft4py": grid},
["H", "dA", "dL", "w_de", "alpha_M"],
units={"H": "km/s/Mpc"},
styles={"eft4py": {"lw": 2}}, # per-source matplotlib-style overrides
).show()
Multi-panel figures render with matplotlib, plotly and altair.
Quantity vocabulary¶
Canonical names shared by all adapters, with their display label and dimension. Quantities of the same dimension can be converted between the listed units. Densities and pressures use CLASS units ($\rho_{\rm CLASS} = \rho_{\rm phys}/3$ with $M_{\rm Pl} = 1$, so $H^2 = \sum\rho$), in 1/Mpc².
Add a name with cplots.core.quantities.register_quantity(Quantity(name, label,
dimension)). Names outside the vocabulary still work: they keep the code's raw name and
no unit.
Using ClassOutput¶
Optional dependency
pip install cplots[classy], or pip install classy.
ClassOutput wraps a computed classy.Class; call cosmo.compute() first.
from classy import Class
import cplots
cosmo = Class()
cosmo.set({
"output": "tCl pCl lCl mPk",
"lensing": "yes",
"P_k_max_h/Mpc": 1.0,
"z_max_pk": 2.0,
})
cosmo.compute()
out = cplots.ClassOutput(cosmo) # or cplots.as_observables(cosmo)
background()¶
Default quantities: "H", "dL", "dA", "dC", read from CLASS's background table.
From the table's columns the adapter also derives, when present:
| Canonical | From CLASS / hi_class columns |
|---|---|
tau |
conf. time [Mpc] |
a |
$1/(1+z)$ |
rho_m, rho_r |
(.)rho_b + (.)rho_cdm, (.)rho_g + (.)rho_ur |
rho_de, p_de |
(.)rho_smg (else (.)rho_lambda), (.)p_smg |
w_de, Omega_de |
$p_{\rm DE}/\rho_{\rm DE}$, $\rho_{\rm DE}/H^2$ |
M_star_sq, alpha_K/B/M/T |
M*^2_smg, kineticity_smg, braiding_smg, M2_running_smg, tensor_excess_smg |
cT2 |
$1 + \alpha_T$ |
Every raw column ("(.)rho_crit", "proper time [Gyr]", ...) stays available under
its CLASS name.
bg = out.background(quantities=["H", "dL"], units={"H": "km/s/Mpc"})
bg = out.background(quantities=["w_de", "alpha_B"], z=np.linspace(0, 2, 50)) # hi_class
pk()¶
| Parameter | Default | Notes |
|---|---|---|
z |
0.0 |
Redshift |
k_min, k_max |
1e-4, 1.0 |
Wavenumber range |
n_k |
200 |
Number of k points (log-spaced) |
nonlinear |
False |
Requires non_linear in CLASS params |
h_units |
True |
k in h/Mpc, P(k) in (Mpc/h)³ |
cl()¶
| Parameter | Default | Notes |
|---|---|---|
lensed |
True |
Lensed or unlensed spectra |
spectra |
["tt"] |
Any subset of "tt", "ee", "bb", "te", "pp" |
l_max |
2500 |
Maximum multipole |
ell_factor |
True |
Apply ℓ(ℓ+1)/2π scaling |
units |
"muK2" |
"muK2", "K2", or "dimensionless" |
primordial() and thermodynamics()¶
primordial() returns P_s(k), k in 1/Mpc. thermodynamics() returns columns of
CLASS's thermodynamics table, by default the visibility function "g [Mpc^-1]":
cplots.PKSpectrum(out.primordial()).show()
cplots.ThermodynamicsEvolution(out.thermodynamics(quantities=["g [Mpc^-1]"])).show()
Using CambOutput¶
Optional dependency
pip install cplots[camb], or pip install camb.
CambOutput wraps a computed camb.CAMBdata (camb.get_results(pars), or
camb.get_background(pars) for the background only).
import camb
import cplots
pars = camb.set_params(
H0=67.4, ombh2=0.022, omch2=0.12, mnu=0.06, omk=0, tau=0.054,
As=2.1e-9, ns=0.965, lmax=2500, WantTransfer=True,
)
out = cplots.CambOutput(camb.get_results(pars))
background()¶
CAMB evaluates on demand: H, dL, dA, dC, on z (default
np.linspace(0, 3, 300)). H is in km/s/Mpc by default.
pk(), cl(), primordial()¶
pk() uses CAMB's matter-power interpolator, so z need not be one of the
redshifts given to set_params; it takes the same parameters as ClassOutput.pk.
cl() takes the same parameters as ClassOutput.cl, with l_max=None meaning
CAMB's own lmax. primordial() is the power law from Params.InitPower.
thermodynamics() is not available.
Using Eft4pyOutput¶
eft4py solves the background of Horndeski
dark-energy models. Eft4pyOutput wraps its BackgroundGrid (from
ScipyBackground.solve or JaxBackground.solve) or a BackgroundSolution.
Requirements
eft4py needs Python ≥ 3.11 and version 0.0.6 or later. cplots never imports it; install it yourself.
import numpy as np
import cplots
from eft4py import ScipyBackground, frozen_field
solver = ScipyBackground(model, [V], ic=frozen_field(10.0))
theta = solver.shoot([guess], cosmo, index=0)
grid = solver.solve(theta, cosmo, np.linspace(np.log(1 / 11), 0.0, 400))
out = cplots.as_observables(grid) # -> Eft4pyOutput
out.background(quantities=["H", "w_de", "alpha_M"], units={"H": "km/s/Mpc"})
eft4py exports its grid in hi_class's format (grid.to_hiclass() returns what
classy.Class.get_background() would), and the adapter reads it with the same code as
ClassOutput. eft4py and hi_class runs therefore come out with identical names, units
and conventions. Fields without an hi_class column (phi, X, cs2, the kinetic term
D, ...) are available under their eft4py names.
z=on aBackgroundGridis interpolated by cplots; on aBackgroundSolutionit uses eft4py's own interpolators (exact derivatives for H and the distances).- Points where the solve failed (
ok=False, e.g. a failedJaxBackgroundsolve) are masked to NaN, with aRuntimeWarning. - Batched (
vmap-ed) grids must be indexed to a single background first.
The eft4py example is a complete, interactive notebook.
Writing your own adapter¶
Subclass Observables and provide only what your code has. For the background,
implement one hook and the base class does the rest (name checks, units, labels,
resampling):
The code produces a table on its own redshift grid (like CLASS):
import numpy as np
import cplots
from cplots import Observables
from cplots.core.observables import BackgroundTable
class MyCodeOutput(Observables):
source = "MyCode"
def __init__(self, result):
self._r = result
def _background_table(self) -> BackgroundTable:
return BackgroundTable(
z=self._r.z,
columns={ # name -> (values, native unit)
"H": (self._r.hubble, "1/Mpc"),
"dA": (self._r.d_ang, "Mpc"),
"w_de": (self._r.w, ""),
},
)
The code evaluates at any redshift (like CAMB):
class MyCodeOutput(Observables):
source = "MyCode"
def __init__(self, result):
self._r = result
def available_background(self) -> tuple[str, ...]:
return ("H", "dA")
def _default_background_z(self) -> np.ndarray:
return np.linspace(0, 3, 300)
def _evaluate_background(self, names, z):
funcs = {"H": (self._r.hubble, "km/s/Mpc"), "dA": (self._r.d_ang, "Mpc")}
return {n: (funcs[n][0](z), funcs[n][1]) for n in names}
If your code can export a dict in classy's get_background() format, reuse the
reader shared by ClassOutput and Eft4pyOutput, and get the whole CLASS
vocabulary for free:
Add pk, cl, primordial or thermodynamics by overriding them; they return the
same PKData / ClData / BackgroundData containers the plots accept.
Then let as_observables (and therefore every plot) recognise your result type: