A minimal CMB angular power spectrum calculator in ~1600 lines of Python.
Designed for students learning CMB theory, researchers prototyping new physics, or anyone who wants to understand what a Boltzmann solver actually does. The entire calculation lives in a single readable Python file.
Computes the unlensed TT, EE, and TE angular power spectra for flat LCDM cosmologies from first principles: Friedmann equation, RECFAST recombination, Boltzmann hierarchy in synchronous gauge, line-of-sight integration with precomputed Bessel tables, and optimally constructed non-uniform grids. Matches CAMB to sub-percent accuracy on the unlensed spectra. Since lensing is not included, the output should not be compared directly to observed CMB data.
Contributions welcome — whether it's improving speed, accuracy, conciseness, or adding new physics. Pull requests encouraged!
TE residuals use the covariance normalization described in Accuracy.
pip install -r requirements.txt
python nanocmb.pyThe default calculation takes about 7s with optional Numba JIT and warm caches on the benchmark machine; runtime depends on hardware. The first run takes longer as it compiles the numerical kernels and builds spherical Bessel function tables. Subsequent runs reuse the Bessel cache.
Against the previous implementation (8c6191a), median compute time fell from 10.51s to 7.08s (33% less time) over three warm runs per version, using the default grids through l=2500. The timer includes background, thermodynamics, and spectrum calculation. This comparison used 12 logical CPUs, Python 3.12.12, NumPy 2.3.5, SciPy 1.17.0, and Numba 0.63.1.
Output is saved to nanocmb_output.npz with arrays ells, DlTT, DlEE, DlTE (D_l in muK^2).
For notebooks, compute_cls(..., n_workers=1) runs the ODEs serially. Script callers using multiple processes should put the calculation under if __name__ == '__main__':. The solver automatically falls back to serial execution for stdin/REPL entry points that spawned workers cannot import.
compute_cls also exposes los_workers, ell_step, ells_compute, bessel_dx, and ode_rtol/ode_atol/ode_max_step for numerical checks. Rebuild the background and thermodynamics dictionaries after changing cosmological parameters.
Compare against CAMB and generate plots:
pip install -r requirements-validate.txt
python scripts/validate.pyThis produces comparison plots in plots/ with residual panels.
To regenerate the tracked spectrum figures embedded in this README:
python nanocmb.py
python scripts/validate.py --update-assetsRun the numerical regression tests with:
python -m unittest discover -s tests -vValidated against CAMB 2.0.4 (AccuracyBoost=3) with Planck 2018 best-fit parameters:
| l range | TT (mean ratio) | TT (std) | EE (mean ratio) | EE (std) |
|---|---|---|---|---|
| 2-29 | 0.9995 | 0.01% | 1.0010 | 0.23% |
| 30-499 | 0.9996 | 0.07% | 1.0001 | 0.15% |
| 500-1999 | 0.9997 | 0.05% | 1.0000 | 0.05% |
| 2000-2500 | 0.9985 | 0.04% | 0.9988 | 0.04% |
Over l=2–2500, default-cosmology RMS residuals are 0.089% TT, 0.095% EE, and 0.047% TE. TE residuals are normalized by sqrt(TT_CAMB * EE_CAMB), avoiding divisions at TE zero crossings.
Across 50 cosmologies from a Latin hypercube spanning +/-3 sigma of the Planck 2018 posterior (scripts/benchmark.py, seed 42), the largest absolute residuals are 0.27% TT, 0.82% EE, and 0.35% TE (normalized as above); the median per-cosmology worst cases are 0.23%, 0.75%, and 0.28%. The EE maxima occur at l < 500, where EE is small. A zero-reionization case gives 0.17% TT and 0.64% EE. The calculation uses massless neutrinos and matched unlensed CAMB spectra; these checks do not establish accuracy outside the tested parameter range.
The RECFAST parameters follow CAMB 2.0.4 (refit Hswitch Gaussians, HeI fudge exponent, and the HeI rate correction), so the ionization history matches CAMB's to 2-4e-5 through recombination (z = 800-4000).
The entire calculation lives in nanocmb.py, structured as a top-to-bottom pipeline:
- Background cosmology -- Friedmann equation, conformal time, sound horizon
- Recombination -- Full RECFAST (H + He ODEs, matter temperature, Hswitch and HeI corrections as in CAMB 2.0.4), reionisation, visibility function
- Grid construction -- Optimal non-uniform grids in k and tau via error equidistribution
- Perturbations -- Boltzmann hierarchy in synchronous gauge (CDM frame) with tight-coupling approximation
- Source functions -- Multi-channel IBP decomposition with ISW, Doppler, and quadrupole terms
- Line-of-sight integration -- Precomputed Bessel tables with dead-zone skipping and recurrence derivatives
- Power spectrum assembly -- Primordial spectrum, k-integration on fine grid, l-interpolation
- Flat geometry (K = 0)
- Massless neutrinos only
- Cosmological constant (w = -1)
- No lensing, no tensors, no isocurvature modes
- First-order tight-coupling approximation
pip install -r requirements.txt- numpy (>= 2.0)
- scipy
- numba (optional; accelerates the perturbation and line-of-sight kernels)
That's it. CAMB and matplotlib are only needed for the scripts in scripts/ (pip install -r requirements-validate.txt).
Planck 2018 best-fit flat LCDM:
| Parameter | Symbol | Value |
|---|---|---|
| Hubble parameter | H0 | 67.36 km/s/Mpc |
| Baryon density | omega_b h^2 | 0.02237 |
| CDM density | omega_c h^2 | 0.1200 |
| Optical depth | tau | 0.0544 |
| Scalar spectral index | n_s | 0.9649 |
| Scalar amplitude | A_s | 2.1e-9 |
| Effective neutrino number | N_eff | 3.044 |
Contributions are welcome! In particular:
- Performance improvements -- Faster ODE integration, better parallelisation, reduced memory usage, or other optimisations that cut runtime without sacrificing accuracy.
- Accuracy improvements -- Better tight-coupling schemes, higher-order corrections, improved grid strategies, or other changes that reduce residuals vs CAMB.
- New physics -- Extensions such as gravitational lensing, tensor modes, massive neutrinos, spatial curvature, or non-standard dark energy. The modular structure is designed to make these additions straightforward.
- Bug fixes -- If you find a discrepancy or numerical issue, please open an issue or submit a fix.
Please keep contributions consistent with the project philosophy: everything in a single readable Python file, minimal dependencies, and every approximation made explicit.
If you use nanoCMB in your research, please cite the accompanying paper (submitted to Astronomy and Computing).
MIT


