Skip to content

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
out.supports("background")   # True
out.supports("cl")           # False for Eft4pyOutput

background()

background() is the same for every code:

  • quantities: canonical names (see the vocabulary) or a code's raw names. Unknown names raise KeyError listing what is available, also given by out.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 raises ValueError. Without z, 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)³
cplots.PKSpectrum(out.pk(z=0.0, n_k=300)).show()

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"
cplots.ClSpectrum(out.cl(spectra=["tt", "ee", "te"])).show()

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.

bg = out.background(z=np.linspace(0, 2, 500), units={"H": "1/Mpc"})

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 a BackgroundGrid is interpolated by cplots; on a BackgroundSolution it uses eft4py's own interpolators (exact derivatives for H and the distances).
  • Points where the solve failed (ok=False, e.g. a failed JaxBackground solve) are masked to NaN, with a RuntimeWarning.
  • 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:

from cplots.data import class_background_table

class MyCodeOutput(Observables):
    source = "MyCode"

    def _background_table(self):
        return class_background_table(self._r.to_class_dict())

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:

cplots.as_observables.register(MyResult)(MyCodeOutput)

cplots.BackgroundPanels({"mine": my_result, "CLASS": cosmo}, ["H", "dA"]).show()