Skip to main content

Ceridwen

CI License: MIT Python 3.11 | 3.12 Docs

Comprehensive SED Estimation Routine Involving Data-driven WEight calculatioNs. CERIDWEN fits the spectral energy distributions of galaxies (photometry, spectra and emission-line fluxes, alone or jointly) to infer their stellar populations, dust, nebular emission and star formation histories. The forward model is written in JAX, so the same code runs on a laptop CPU or a GPU, and is differentiable for gradient-based samplers. It is for astronomers who want a Bayesian SED fit with the evidence or with Hamiltonian Monte Carlo. Documentation: www.amanda-stoffers.de/ceridwen.

Features

  • Star formation history on a lookback-time grid: non-parametric continuity (logsfr_ratios) or any parametric form written as a transform
  • Constant metallicity or a metallicity history (zh_const)
  • [α/Fe] as a sampled stellar axis (CSPBasis_afe, aMIST + C3K grid)
  • Dust attenuation: Kriek & Conroy diffuse dust, power-law birth clouds, 12 registered laws (Calzetti, SMC, LMC, Gordon+03 SMC bar, Reddy+15, ...)
  • Dust emission from Draine & Li (2007) or THEMIS templates
  • Nebular continuum and emission lines from the FSPS CLOUDY grids
  • IGM attenuation (Madau 1995), with an optional damping wing and damped Ly-α absorber (MadauDampingDLA)
  • Photometry, spectra and emission-line fluxes, fitted alone or jointly
  • One broadening kernel: stellar and gas velocity dispersions, instrument line-spread function, library resolution removed automatically
  • Analytic marginalisation over emission-line fluxes (Spectrum(marginalize_elines=True))
  • Noise and calibration terms: jitter, calibration polynomials (sampled, profiled or marginalised), an outlier mixture, a Gaussian-process likelihood for correlated residuals
  • Samplers: nested sampling (with the evidence), NUTS, VI-preconditioned NUTS, and a MAP optimiser
  • Post-processing (PostProcess): SFRs, M_UV, ionising photon rates, posterior-predictive data, and summary, corner and diagnostic figures

Installation

CERIDWEN needs Python 3.11 or newer. Create an environment and install the package:

conda create -n ceridwen python=3.11 -y && conda activate ceridwen
pip install ceridwen

On Linux this installs the CUDA 12 JAX wheels, and JAX uses an NVIDIA GPU when one is present (it needs a recent NVIDIA driver, see JAX's install page). Everywhere else, and on Linux without a GPU, JAX runs on the CPU. On Windows, use WSL2.

CERIDWEN reads the nebular CLOUDY grids and the dust-emission templates from the FSPS data files. Clone FSPS into $SPS_HOME (3.4 GB) and make the variable permanent (replace ~/.zshrc with ~/.bashrc if your shell is bash):

export SPS_HOME="$HOME/fsps"
git clone --depth 1 https://github.com/cconroy20/fsps.git "$SPS_HOME"
echo 'export SPS_HOME="$HOME/fsps"' >> ~/.zshrc

Compiling the python-fsps wrapper is needed only to build your own SSP grid; the published grids and the FSPS data files are enough to fit (see Installation).

Check the installation:

python -m ceridwen.check

It prints one line per component, with the fix for anything missing. The python-fsps line is a warning until you compile it, and that is expected.

Quick start

This fits a mock galaxy (twelve photometric bands from GALEX to WISE and an optical spectrum) made with the same forward model, so you can compare the fit with the truth. Run the blocks in order in one Python session.

1. Get an SSP grid. The published MIST + MILES grid (Chabrier IMF) is downloaded once (67 MB) into ~/.ceridwen/grids (or $CERIDWEN_GRID_DIR), and its SHA-256 is checked on every call. available_grids() lists the others. All published grids are archived on Zenodo (doi:10.5281/zenodo.22937956); this README and fetch_grid always point at the current record.

from ceridwen import SSPData
from ceridwen.ssps import fetch_grid

ssp = SSPData.load(fetch_grid("mist_miles_chab"))
ssp.display()                                      # library, IMF, grid coverage

2. Build the model. lookback_time holds the SFH nodes in Gyr, increasing from 0 (today). The SFH is sampled as logsfr_ratios and turned into a star formation rate per node by a transform. logzsol is log10(Z/Z_sun) of the grid.

import jax, jax.numpy as jnp
import numpy as np
from ceridwen import CSPBasis, SedModel, Instrument, Cosmology
from ceridwen.observation import Photometry, Spectrum
from ceridwen.model import logsfr_ratios_to_sfh
from ceridwen.priors import Uniform, ClippedNormal, StudentT

ZRED = 0.1                                         # fixed spectroscopic redshift
FILTERS = ["galex_FUV", "galex_NUV", "sdss_u0", "sdss_g0", "sdss_r0",
           "sdss_i0", "sdss_z0", "twomass_J", "twomass_H", "twomass_Ks",
           "wise_w1", "wise_w2"]
SPEC_WAVE = np.linspace(4000.0, 8000.0, 600)       # observed-frame vacuum Angstrom
LSF = Instrument.sigma_kms(150.0)                  # instrumental line-spread function

csp = CSPBasis(ssp, lookback_time=jnp.linspace(0.0, 12.0, 6),
               cosmo=Cosmology.planck18(),
               zh_const=True, sfh_interp="step",
               add_dust=False, add_diffuse_dust=True, add_neb=False, verbose=False)

sfh_times_yr = np.array(csp.sfh_times)
def logsfr_to_sfh(free_theta):
    return logsfr_ratios_to_sfh(free_theta["logsfr_ratios"], sfh_times_yr=sfh_times_yr)

def build_model(observations):
    return SedModel(
        csp, observations=observations,
        priors={
            "logzsol": Uniform(low=-2.0, high=0.2),
            "logmass": Uniform(low=9.0, high=12.0),
            "diffuse_tau_kc": ClippedNormal(mean=0.3, sigma=1.0, low=0.0, high=4.0),
            "diffuse_dust_index": Uniform(low=-1.0, high=0.4),
            "logsfr_ratios": StudentT(df=2.0, mean=0.0, scale=1.0),
        },
        transforms={"sfh": logsfr_to_sfh},
        free_param_init={"logsfr_ratios": jnp.zeros(5), "logmass": jnp.array([10.0])},
        zred=ZRED,
    )

SedModel also applies a galaxy velocity dispersion of 300 km/s by default. Pass kinematics=Kinematics(sigma_gal=...) to change it, or Kinematics(sigma_gal="sigma_gal") with a prior to fit it (see Conventions).

3. Make the mock data. Push known parameters through the model and add noise (signal-to-noise 20 in the photometry, 25 in the spectrum).

TRUTH = {
    "logsfr_ratios":      jnp.array([0.3, 0.2, -0.1, -0.4, -0.6]),
    "logzsol":            jnp.array([-0.2]),
    "logmass":            jnp.array([10.5]),
    "diffuse_tau_kc":     jnp.array([0.5]),
    "diffuse_dust_index": jnp.array([-0.7]),
}
gen = build_model([Photometry(filters=FILTERS, name="phot"),
                   Spectrum(wavelength=SPEC_WAVE, instrument=LSF, name="spec")])
truth_pred = gen.predict(TRUTH)                    # AB maggies (phot), F_nu in cgs (spec)

rng = np.random.default_rng(42)
mag = np.asarray(truth_pred["phot"]); mag_unc = mag / 20.0
sfx = np.asarray(truth_pred["spec"]); sfx_unc = np.abs(sfx) / 25.0
phot = Photometry(filters=FILTERS, name="phot", uncertainty=mag_unc,
                  flux=mag + mag_unc * rng.standard_normal(mag.shape))
spec = Spectrum(wavelength=SPEC_WAVE, instrument=LSF, name="spec", uncertainty=sfx_unc,
                flux=sfx + sfx_unc * rng.standard_normal(sfx.shape))
model = build_model([phot, spec])

4. Fit with nested sampling. These settings are sized for a laptop CPU: the fit took 21-29 min (67 800 likelihood calls) in two runs on an 11-core Apple M3 Pro shared with other jobs. It prints its progress and writes ./my_fit/ceridwen_result.h5. For production, raise num_live (fitSED's default is 500) and lower logZ_tol; for NUTS, see Samplers.

from ceridwen import fitSED

result = fitSED(
    model,
    sampler="nested",
    sampler_kwargs={"num_live": 100, "num_delete": 20, "logZ_tol": -2.0},
    rng_key=jax.random.PRNGKey(42),
    output_dir="./my_fit",
)
print(f"log Z = {result.log_evidence:.2f} +/- {result.log_evidence_err:.2f}")

5. Inspect and post-process. PostProcess resamples the draws to equal weight, pushes them through the fitted model, and computes derived quantities and predictions.

from ceridwen import PostProcess

pp  = PostProcess(model, result, n_samples=1000)
out = pp.run()

truth = {p: float(TRUTH[p][0]) for p in ("logzsol", "logmass", "diffuse_tau_kc", "diffuse_dust_index")}
for p, t in truth.items():
    lo, med, hi = np.percentile(out["theta"][p], [16, 50, 84])
    print(f"{p:>20}: true {t:+7.3f}   fit {med:+7.3f}  (-{med - lo:.3f}/+{hi - med:.3f})")

print(np.percentile(out["extras"]["sfh"]["sfr100"], [16, 50, 84]))   # SFR over 100 Myr [Msun/yr]
pp.figures("./my_fit/figures", title="mock galaxy", truths=truth)
pp.save("./my_fit/post.npz")

The table compares the posterior median and 16-84% range with the truth. At these settings the recovered values can sit 2-3 sigma from the truth: with a five-ratio SFH, logmass trades against the SFH shape, and the MAP of this mock lies at logmass 10.44, not at 10.50 (the data and the degeneracy, not the sampler). figures writes summary.pdf (SED with residuals, SFH, marginals), corner.pdf and diagnostics.pdf (the sampler's diagnostics). post.npz holds every draw's predictions and is about 100 MB; load_postprocess reads it back. Everything PostProcess returns is described in Post-processing.

Verification

The test suite checks units and conventions, the broadening against direct quadrature, the likelihoods, the samplers, post-processing and the misuse guards. Two layers compare the forward model with stored arrays: 18 regression categories at atol=1e-10, rtol=1e-7 with a maximum relative residual of 1e-6 (tests/regression/test_regression.py), and six SFH and metallicity configurations (tests/csp/test_lookback_flip_invariant.py) with SSP weights at rtol=1e-12 and spectra, line fluxes and photometry at rtol=1e-6. See For developers to run them.

Documentation

For developers

Install from a clone (--depth 1 skips the history: 41 MB instead of 244 MB):

git clone --depth 1 https://github.com/Espe13/ceridwen.git
cd ceridwen
pip install -e .

The test suite needs the published BPASS grid. The first command below runs what CI runs; it took 33 min on an 11-core laptop CPU shared with other jobs. The second compares the forward model with the stored baselines and needs $SPS_HOME.

export CERIDWEN_TEST_SSP=$(python -c "from ceridwen.ssps import fetch_grid; print(fetch_grid('mist_bpass_v2'))")
pytest -m "not fsps and not gpu" -q -ra
pytest tests/regression/test_regression.py tests/csp/test_lookback_flip_invariant.py -q

A grid-dependent test skips when it finds no grid, so read the skip summary: a skip is not a pass. Development lists every check and how the code is built. Do not run scripts from the folder that contains the clone: Python then finds the clone folder ceridwen before the installed package.

Citing

If you use CERIDWEN in your research, please cite it:

@misc{stoffers2026ceridwen,
  author       = {Stoffers, Amanda},
  title        = {{CERIDWEN}: Fast and Flexible {GPU}-Accelerated Stellar Population Inference},
  year         = {2026},
  note         = {Version 1.0.12},
  howpublished = {\url{https://github.com/Espe13/ceridwen}}
}

License

MIT; see LICENSE.

Release files for ceridwen 1.0.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ceridwen 1.0.12
File Size Uploaded
ceridwen-1.0.12.tar.gz 3.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for ceridwen 1.0.12
File Interpreter ABI Platform
ceridwen-1.0.12-py3-none-any.whl Python 3 none any Details

Total release size: 7.6 MB

Release files / ceridwen-1.0.12.tar.gz

Download URL ceridwen-1.0.12.tar.gz
Size 3.7 MB
Tags Source
SHA-256 checksum
How to use checksums
afd093ace1f707d46519f19d1a2c8027b201a0eee737d5f45c9cbaa8d00889ae
BLAKE2b-256 checksum
How to use checksums
e158cc5ad0f6c6b0931daaab91a54373ebe7fb3a7115963e6bd6ecacf78cc139
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / ceridwen-1.0.12-py3-none-any.whl

Download URL ceridwen-1.0.12-py3-none-any.whl
Size 3.8 MB
Tags Python 3
SHA-256 checksum
How to use checksums
c12f905ebfbecdca622d9d096fcc1ad42985d7694f043f275fee1b1252af3814
BLAKE2b-256 checksum
How to use checksums
c4a3b5fd06c903c0fdf6f258665d453a6a770b70da76113976f4812cc914773e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release history Release notifications | RSS feed

This release

1.0.12 This release

2 release files

1.0.11

2 release files

1.0.10

2 release files

1.0.9

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page