Changelog¶
Newest first. Every version below was published as a GitHub release.
From v0.25.0 to v0.40.0 those are -dev pre-releases cut from
the dev branch, while main stayed at v0.22.0; v1.0.0 is
where main catches up with all of it. Version numbers bumped in flight but never
released are folded into the release that carried them.
The record starts at v0.3.0 – v0.1 and v0.2.0 predate it. The
entries up to v0.25.0 were kept in the README’s “Project Status”
section and were moved here unchanged.
Each entry says what changed and, where it matters more, how it was found out to be wrong – a bug that produced a plausible number is worth more words than a feature that worked first time.
v1.0.0¶
The first stable release, and the point where main catches up with
sixty-four commits of dev.
What makes it 1.0.0 is not a feature. It is that the three things the Roadmap had listed since the beginning – a stable public API, complete documentation, test coverage across the whole library – are done and held there by tests rather than by intention. The rest of this entry is the last of that work.
The GUI catches up with three releases¶
CosmoFit.theory landed in v0.36.0 and the GUI had no route to it.
It offered one way to a model the library does not ship – Custom,
which takes an E(z), the result of a derivation somebody did by
hand. Now it takes the input instead: a gravitational Lagrangian,
from which the library derives the Friedmann equation and shows it.
Six worked examples to start from, covering every route the module
has – algebraic, root-solved, Lagrange-multiplier and integrated –
and each a real model rather than a syntax demonstration.
A new Inference tab, for four things the library grew in
v0.36.0 and the app had none of: profile likelihoods (reported as
the Δχ² = 1 crossings, which is what a profile actually measures),
Fisher matrices with the ratio to the chain (the comparison is the
point – it is a Gaussian approximation, and that is the only way to
know whether it holds here), Bayesian evidence by nested sampling,
and tension by two definitions that disagree exactly when the
Gaussian one has stopped meaning anything.
Two smaller gaps: the eboss_surface figure, which is what shows why
a Gaussian summary of the two tabulated eBOSS datasets would be
wrong; and best_fit(restarts=), which exists because ΛsCDM once fit
worse than the ΛCDM it contains.
And two ways the GUI could hand somebody a result that looks like a
measurement and is not. ADE derives Omega_m and the parameter
table ticked it anyway, so choosing that model put the app straight
into the state Fitter warns about – a posterior that is the prior
back again. And the warning itself was invisible: Fitter warns
through warnings, which in a Streamlit app means stderr, which
means nowhere. The run loop catches them and renders them now, so
every guard the library grows later shows up without the GUI
restating any of it.
The package is typed¶
py.typed ships. That tells every downstream type checker to trust
these annotations, so it had to be true first: 201 of 201 public
callables fully annotated, from 58. CosmoFit.typing spells out the
two aliases the whole surface is written in – E(0.5) returns
np.float64 and E([0.1, 0.2]) returns an ndarray, so annotating
the return as np.ndarray alone would have been wrong for the
commonest call in the library.
Two tests hold it there: every annotation resolves, including the
ones deferred to TYPE_CHECKING (which is what a checker sees and
get_type_hints cannot – matplotlib.figure costs 0.65 s to
import, more than the rest of the library, and nobody who never draws
a figure should pay it), and nothing on the public surface is left
unannotated.
Running mypy over it for the first time found four annotations that
were simply false: load_chain declared MCMCResult and returns
a StoredSampler, theory.fields.build_system declared one object
and returns two, BaseLikelihood.model declared an array where
S8Likelihood returns a number, and EnsembleSampler.run narrows
its base class’s **kwargs into named parameters. All corrected –
which is exactly the class of thing py.typed would otherwise have
started telling other people’s type checkers.
mypy reports 81 remaining errors, all inside function bodies rather
than in the signatures py.typed is a promise about. It is in the
dev extra so a contributor can run it; CI does not gate on it yet.
Two workflows that nothing can trigger by accident¶
publish builds, runs twine check --strict, and uploads to PyPI
through Trusted Publishing – GitHub mints a short-lived OIDC token
and PyPI accepts it in place of an API token, so no long-lived secret
lives in this repository at all. pages deploys the API reference to
GitHub Pages.
Both are workflow_dispatch only, and that is the design rather
than a placeholder. A version number, once published, can never be
reused; a site, once deployed, is on the public internet under this
repository’s name. Neither should be able to happen as a side effect
of pushing a tag. Someone has to open the workflow and run it, and
each needs one thing set up on the other side first – a Trusted
Publisher on PyPI, Pages enabled here.
The release, rehearsed¶
Everything that could be checked before a first publish was, rather than being left to find out on the day:
cosmofitis free on PyPI and TestPyPI.Both artifacts pass
twine check --strict. The wheel is 22 MB against PyPI’s 100 MB per-file limit – 10 MB of it the Pantheon+ covariance – so no limit increase is needed.The sdist was incomplete, and nothing would have said so. The bundled data travels because it lives inside the package, but
CITATION.cff,REFERENCES.md,CHANGELOG.mdandCONTRIBUTING.mdsit at the repository root and were simply absent. AMANIFEST.inadds them and prunesdocs/,examples/,app/andtools/, which have no business in a source distribution. The citation file is the one that matters: this library bundles other people’s data and implements other people’s methods, and the file saying how to credit them should travel with the source rather than living only on a web page.The wheel was installed into a clean virtual environment, outside this source tree, and fitted ΛCDM to CC+DESI+Pantheon++Planck – 1671 points, H₀ = 67.5, Ω_m = 0.313 – with
py.typedpresent and thetheoryextra correctly absent and correctly explaining itself.
That last check earned its place immediately: it found a fifth
false annotation, and then a sixth. Fitter.best_fit was declared
-> BestFitResult and returns scipy’s OptimizeResult;
Fitter.run_mcmc was declared -> MCMCResult and returns emcee’s
EnsembleSampler. Both were wrong the same way – named after the
object that lands on fitter.result rather than the one handed back
– and neither mypy nor the resolution test could see it, because
scipy and emcee both return Any.
So there is now a test that calls ten of Fitter’s methods and
compares what comes back against what was declared. With py.typed
shipped, a wrong annotation is no longer a private mistake; it is
something other people’s type checkers repeat with confidence.
GitHub Pages is enabled with Actions as the source, which fixes the
documentation URL, so [project.urls] gains a Documentation entry.
Run pages before publish and it is live rather than a 404.
main catches up¶
Sixty-four commits, and the reason this is 1.0.0 rather than 0.41.
main had been deliberately frozen at v0.22.0 since August while
everything went out as -dev pre-releases; it now carries all of it.
That freeze had one consequence nobody had noticed: a
workflow_dispatch workflow is only visible to GitHub if the file
exists on the default branch. tests worked because it is
push-triggered, but publish and pages returned 404 to every
attempt to run them. They could not have been run at all until
main moved – so the last item on the Roadmap turned out to be a
prerequisite for the two before it rather than a tidy-up after them.
The Changelog and Examples entries in [project.urls] move from
dev to main with it, as does the “edit this page” link in the
API reference. Development Status goes from 4 - Beta to
5 - Production/Stable.
And with main moved, both workflows could finally run:
the API reference is live at https://salihyesil59.github.io/CosmoFit/;
pip install cosmofitworks – https://pypi.org/project/cosmofit/.
Published to TestPyPI first, installed back down from that index into a clean environment, and checked to give the same fit to the digit before the real one was touched.
And the notebooks stop cloning the repository¶
All seventeen used to open in Colab by cloning dev and installing
it editable, which then needed a sys.path fix: pip runs in a
subprocess, so the already-running kernel never saw the .pth file
it wrote, and import CosmoFit could resolve to the bare checkout
directory instead – same name, and /content is on sys.path. Six
slightly different versions of that workaround had accumulated.
A published package removes all of it: pip install cosmofit, into
site-packages, which the kernel is already looking at. Each notebook
asks for the extras it actually needs, and the Colab badges point at
main rather than dev.
Two things fell out of checking rather than assuming. lscdm_mcmc
looked like it needed CAMB because it mentions planck_lite – in a
section explaining why the full spectra cannot be used there.
And dataset_zoo genuinely does need it, which the extras table in
examples/README.md had not said. dark_energy_evidence_audit was
the one notebook with no Colab badge at all, while that README
claimed every one of them was Colab-ready; it has one now.
v0.40.0¶
Everything the Roadmap listed for v1.0.0, worked through against what the repository actually measured rather than against recollection. No new physics: this is the release where the library stops being a research codebase that happens to be public.
The release history had stopped at v0.25.0¶
Fourteen releases – the whole of CosmoFit.theory, Bayesian
evidence, profile likelihoods, Fisher matrices, tension statistics,
the three holographic models, the performance pass, the examples
reorganization – existed only as GitHub release notes. The
repository did not record its own last month of work.
That history is now this file, and the README lost half its bytes (98 KB to 49 KB) getting it out of the way: the old “Project Status” section had grown to nearly a thousand lines, so a reader looking for “what is this library” scrolled through a year of release notes to reach the Roadmap.
A public API that is pinned¶
Nothing was holding it. Every other test imports what it happens to
need, so a name could be renamed, moved between subpackages or
dropped from __all__ and the suite would go on passing as long as
some path to the object still existed.
tests/test_public_api.py types the surface out by hand – 64 names
– and asserts the set in both directions. An extra name is a promise
nobody meant to make; taking it back out is then itself a break.
Writing it down found two names that had already drifted out of
CosmoFit.cosmology while every one of their siblings was
re-exported: ModelConfigurationError, which is the one
exception a user is asked to tell apart from an ordinary failure and
was reachable only as
from CosmoFit.cosmology.core.errors import ..., and
GrowthCalculator. Both are importable from the top level now, and
a further test asserts that each layer of cosmology is re-exported
whole, so the class of drift is closed rather than the two
instances of it.
Two more things the file checks. Importing the library must not
import an optional backend – which caught numba: kernels.py
imported it at module scope, so import CosmoFit paid 115 ms, 17% of
the whole import, for a kernel only a growth fit ever calls. It is a
spec lookup now and the compilation happens on first call; 0.66 s to
0.56 s, with the two stepping paths still agreeing to 1e-13. And
every annotation on the public surface resolves – these modules
use from __future__ import annotations, so a wrong annotation is
invisible until something calls get_type_hints, which nothing did.
Test coverage, where the measurement said to look¶
93% overall, from 79%, across 681 tests. The Roadmap’s
phrasing – “across the whole library, not only the newest parts” –
turned out to be exactly right: theory, the newest subpackage, was
at 86-94% while the oldest code was not.
was |
now |
|
|---|---|---|
|
16% |
94% |
|
37% |
100% |
|
58% |
97% |
|
64% |
92% |
|
67% |
96% |
|
70% |
100% |
|
73% |
100% |
|
78% |
93% |
|
85% |
100% |
The plotter is the largest module in the library and 16% of it had
ever run. Nothing anywhere called a single plotting method. It is
also the worst place for a gap, because a plot fails quietly – an
empty axis, a curve at the wrong scale, a band that collapsed to a
line. Nothing raises; the figure is saved and somebody looks at it.
So the assertions are about content: axis labels set, artists
carrying points, one trace panel per free parameter with one line per
walker, six axes for the three CMB spectra and their residual panels.
Three of them are not about drawing at all – that a missing dataset
names itself instead of raising AttributeError on a None, that
the band appears only once there is a chain, and that drawing a band
leaves the cosmology where it found it (a band evaluates hundreds
of posterior draws; without the restore, every number read off the
fitter after a plot would come from whichever draw happened to be
last).
Elsewhere the untested half was consistently the rejection paths
and the code that leaves the process: the chain-signature machinery
that decides whether a saved chain may be resumed (the quietest
scientific error the library can make – one array of samples drawn
from two different posteriors, with nothing to say so); the JSON
encoder standing between numpy and a json module that rejects
everything numpy produces; both guards on LogPosterior.chi2, which
exist because L-BFGS-B once evaluated the objective at
[nan, nan, nan]; and both paths through DenseCovariance.solve,
including the fallback that discards a precomputed inverse which
fails its own accuracy check.
The sharpest test of the batch is in cpl_diagnostics: the
Mahalanobis distance is not the significance. D^2 follows
chi-square with two degrees of freedom, so D = 2.20 is 1.70 sigma,
not 2.2 – and reporting distance as sigma overstates the tension
with LCDM, which is the direction a dark-energy paper would like it
to be wrong in.
An API reference, and ten docstrings that rendered wrong¶
pyproject.toml had declared a docs extra – sphinx and furo –
for a repository with no docs/ directory. There is one now:
autosummary over the public API, napoleon for the numpydoc
docstrings, intersphinx, and myst-parser so README.md and
CHANGELOG.md are included verbatim rather than copied into a
second, drifting version.
Building it produced 237 warnings, and they were not cosmetic:
indented equations are reStructuredText block quotes, and a continuation line beginning with
*,+or-becomes a bullet list inside one – so GEDE’sOmega_de(z), IDE’s continuity equations, GCG’s density evolution and the curvature branches ofDistanceCalculator.DMwere all being parsed as prose with lists in the middle;GCG,FRTLinearandFRHuSawickieach had a numpydocParameterssection whose body is a paragraph, which napoleon renders as a field list that the prose then breaks;|beta|,|p/rho|and|a - b|– absolute-value bars in prose – are reStructuredText substitution references, and each was an error for an undefined name.
The build is clean under -W now, and CI keeps it there.
Packaging, and three CI jobs¶
[project.urls] did not exist, so a PyPI page would have been a
rendered README and a dead end. Four classifiers that matter for a
scientific package were missing, including both
Scientific/Engineering topics – which is how somebody browsing for
a cosmology library would ever find this one – and 3.12/3.13, which
CI had been testing for months. The license moved to an SPDX
expression.
black came out of the dev extra. It was listed and nothing used
it, which is worse than it sounds: a contributor taking the extra at
its word would reformat 101 of the 117 files under src/ and
tests/, and this code is hand-wrapped. ruff is configured as a
linter only, with three rules ignored and a reason for each.
New CI jobs: lint (which found seven dead or shadowed names the
test suite could not see, and later refused a blind
pytest.raises(Exception) of mine), python -m build +
twine check – the half of a release that cannot be found by
running the code – and docs, building with warnings as errors.
Also¶
CONTRIBUTING.md and CITATION.cff. The first explains why there is
no formatter and what a finished change looks like here; the second
is what GitHub’s “Cite this repository” button reads, and it says
plainly that the papers behind whatever you used matter more than the
software entry.
v0.39.2¶
No new capability. This closes a gap in what was tested, corrects two error messages that had fallen behind, and walks back a Roadmap claim the previous release had itself added. 501 tests.
The two things a runtime-built model cannot do¶
Three routes now produce a Cosmology subclass that exists only in
the session that made it – define_model (an E(z) function),
model_from_expression (an E(z) string) and
CosmoFit.theory.Action.build (an action). Everything about fitting
works on them, with two exceptions, and both follow from one fact:
such a class cannot be pickled by reference, because there is no
importable name to pickle it to. run_mcmc(n_processes>1) sends the
model to worker processes; Fitter.from_chain re-imports the model
the chain was sampled with.
Both were already guarded, and guarded well – run_mcmc refuses up
front rather than letting the pickling failure surface from inside a
worker, where the traceback says nothing about the model. But neither
had a test, and neither message mentioned Action.build(), so
somebody who had written an action was told about two functions they
had never called.
Messages corrected, and tests added across all three routes. The one
worth more than the error text: the from_chain workaround actually
works – rebuilt the same way, the chain reloads and gives back the
same posterior to 1e-12. A message promising a fix that does not work
is worse than no message.
A Roadmap entry walked back¶
v0.39.1 rewrote the Roadmap, and one of the entries it added
claimed more than it should have. It said the models’ own E(z)
counts massive neutrinos inside Omega_m as pure matter – true as
far as it goes. But the three places E(z) is consumed above z ~ 100
are each already handled: the sound horizon integrates the exact
Fermi-Dirac density (and validates against CAMB’s rdrag to 5e-5);
the compressed Planck priors deliberately keep massive neutrinos
inside Omega_m at every redshift, because a prediction has to share
the compression’s definitions rather than improve on them; and the
growth ODE solves a matter-plus-dark-energy equation, so starting it
deep in that matter era is self-consistent. The item is real but far
narrower than it read, and doing it carelessly would break agreements
the library has already validated. The Roadmap now says so, rather
than leaving a trap for whoever picks it up.
v0.39.1¶
Neither commit adds a model: the first closes two ways a theory-built model could give a wrong answer with nothing said, and the second catches the documentation up with four releases of work. 491 tests.
Growth of structure, where a theory model was silent¶
Two failures of the same kind – the answer is wrong, and nothing says so.
A configuration error was being swallowed into infinity.
LogPosterior.chi2 turns exceptions into an infinite chi-squared,
which is right for a parameter the model cannot represent: a sampler
that merely proposed it should not crash, and treating the point as
the worst possible fit is exactly what excluding it means. It is wrong
for a configuration it cannot represent. The growth ODE starts at
z = 9999 and a field action’s history starts at z_init, default
3000 – so Fitter(model=<any field model>, datasets=["fsigma8"])
built fine and then returned chi2 = inf at every parameter value,
a fit that ran to completion having learned nothing and saying
nothing. The message existed and already named the fix; it just never
reached anyone. New ModelConfigurationError, deriving from
Exception rather than from any of the three caught there, so it
propagates – and it now names the growth ODE’s starting redshift,
which is the number needed to pick z_init.
Scalar-tensor gravity reported mu = 1. In that theory the field
sets the strength of gravity, so G_eff/G_N is neither 1 nor
constant. Fitting fsigma8 with mu = 1 gives General Relativity’s
growth attached to a modified background: finite chi-squared,
plausible posterior, no way to tell. growth="quasi_static" now
covers it, with the sub-horizon result of Boisseau,
Esposito-Farese, Polarski & Starobinsky (2000),
mu = (2F + 4 F_phi^2) / (F (2F + 3 F_phi^2)), evaluated on the
field’s own solution; F = 1 with a constant coupling gives exactly
1, which is the normalization it has to satisfy. And Fitter now
warns when a non-minimally coupled model meets growth data with mu
still 1 – the one case where the answer is silently wrong.
Checked along the way and found correct: compute_rd=True works for
every kind of theory model, since it uses the early-universe densities
rather than the late-time E(z) and so never reaches past z_init.
The documentation had fallen four releases behind¶
An audit rather than a feature, and it found things a reader would
have been misled by. The Roadmap listed five delivered features as
still missing – eBOSS DR16 ELG and Lyman-alpha, Planck lensing, ACT
DR6, Planck low-l, holographic dark energy – and was rewritten to
what is actually left. The Features list did not mention
CosmoFit.theory at all, the largest addition of four releases,
invisible where a reader starts. REFERENCES.md had nothing for the
theory module although the code cites three papers; added, with every
arXiv identifier verified against arXiv’s API rather than written from
recall, and saying which is validation the module is checked against
and which is a perturbation result it imports.
v0.38.1¶
Housekeeping over two capabilities the notes recorded as “should work, but untested” – and one of them turned out not to work at all. 484 tests.
Scalar-tensor gravity, which four places already said worked¶
The minisuperspace docstring, reduce_order’s error message,
fields.py and the README all stated that a non-minimally coupled
F(phi) R reduces correctly. The reduction does handle it. It could
not be written: the namespace the gravity expression is parsed
in carried the cosmological parameters and the geometry scalar and no
field names, so Action("(1 + xi*phi**2)*R", fields={"phi": ...})
failed with Unknown name(s): ['phi']. An entire model class –
Brans-Dicke, induced gravity, anything where the field sets the
strength of gravity rather than sitting on top of it – was
unreachable while being advertised.
Two things had to follow. The field has to become a function of time
in the gravitational sector too: arriving as a plain symbol,
F(phi) differentiates to zero, the 3 H dF/dt term vanishes, and
what is left is a rescaled General Relativity with a Friedmann
constraint that still looks entirely reasonable. That is the same
failure the field Lagrangian had two releases earlier, which is why
the test does not only check the ratio to the textbook form – it
asserts the constraint genuinely depends on the field’s velocity,
which a coupling that had quietly disappeared could not produce. And
a field cannot be named R, T or Q: it would be the same
symbol as the geometry scalar in every expression, and the scalar
would silently win.
Validated three ways: the derived constraint equals
3 F H^2 + 3 H dF/dt = rho symbolically, ratio exactly 1; xi = 0
returns LCDM to 1e-9 with the closure recovering 3 Omega_de0; and
xi of either sign moves E(z) by more than 1e-4 with constraint
drift below 1e-10.
Multi-field actions¶
The other untested claim, and this one already worked. Two exponential
quintessence fields reduce to two equations of motion, integrate
together, and leave the closure condition one parameter to solve for
– E(0) = 1 to 1e-9, drift 2.6e-13. Now tested rather than assumed.
v0.38.0¶
Removes the theory module’s most conspicuous refusal – the one you
were most likely to hit first, since "R + alpha*R**2" is the obvious
thing to type. A general f(R) builds, with nothing to ask for:
Action detects that the action is nonlinear in R and routes there
itself. 478 tests.
Why it needed a different reduction¶
The ordinary reduction removes the addot in the Einstein-Hilbert
term by integrating by parts, which is legitimate only while the
Lagrangian is linear in it. For a general f(R) it is not – the
term that would be dropped is not a total derivative, and dropping it
returns a different theory with nothing to show for it.
The way round is to stop treating R as shorthand for a combination
of a and its derivatives and make it an independent variable,
held to that combination by a Lagrange multiplier. Varying the
multiplier returns R = R_geom; varying R gives
lambda = N a^3 f'(R); substituting back leaves
L = (1/2) N a^3 [f(R) - f'(R) R + f'(R) R_geom], which is linear in
addot again. So reduce_order applies unchanged, at the cost of one
extra dynamical variable – and that variable is the theory’s fourth
order. It surfaces as a parameter, R_0, the Ricci scalar today:
an initial condition General Relativity does not have.
What comes out is smaller than “fourth-order” suggests. Varying R
returns R = 6(2H^2 + H H'), an explicit H'; the constraint is
linear in R', giving an explicit R'. Two first-order equations in
(H, R).
It integrates backwards, and theory.fields does not¶
Not an inconsistency, and measured rather than asserted. A scalar
field integrated backwards runs away – the Hubble friction that damps
it forwards becomes anti-friction. The f(R) scalaron does not: a
1e-8 relative kick to R_0 moves E(z) by less than that out to
z = 1100, amplification below 1. So there is no shooting here and no
early initial condition, and closure= is refused rather than
accepted and quietly ignored – there is nothing left for it to fix.
The accuracy measure had to change too¶
In theory.fields the Friedmann constraint is a first integral that
the integration never imposes after the initial conditions, so its
drift is an independent measure of the error. Here the constraint is
one of the two equations that define the right-hand side: its
residual is zero by construction and measures nothing at all. A drift
check copied across would have looked reassuring and said nothing.
What is left over is the third equation of motion, from varying a,
which follows from the other two by the Bianchi identity – it holds
on an exact solution and drifts on an approximate one. It comes out at
1e-15.
Validated three ways: the derived constraint equals the textbook
3 f_R H^2 = (f_R R - f)/2 - 3 H d(f_R)/dt + rho symbolically;
alpha -> 0 approaches LCDM smoothly rather than merely resembling
it at one point (6.4e-01, 1.8e-01, 4.2e-03 for alpha = 1e-1, 1e-3,
1e-5 over z <= 5); and the routing boundary holds – R - 2*Lam stays
on the ordinary path, any nonlinearity in R does not, and
f(T)/f(Q) are never diverted.
dR/dN carries 1/f''(R), which vanishes in the GR limit, so a model
very close to GR is stiff – slow rather than wrong: alpha = 1e-3
integrates in about 3 ms, alpha = 1e-9 in about 650 ms. Measured, and
said in the error message when the integrator gives up.
growth="quasi_static" is refused for f(R), whose mu is
scale-dependent – a Compton wavelength enters – so the scale-free
1/f' of the teleparallel sectors would be wrong rather than
approximate. FRHuSawicki already carries the standard form.
v0.37.3¶
One notebook, closing the gap the previous release ended by naming. Seventeen notebooks; 463 tests.
planck_lite – the CMB computed from scratch rather than compressed
to three numbers – had exactly one example, and it was a research
analysis. Fine if that is your question; no use at all for finding out
what the dataset is and whether you want it.
01-getting-started/cmb_from_scratch.ipynb is the ten-minute version,
and makes four points, each with a measurement.
What it costs: 0.2 ms per evaluation for the three compressed
numbers against 766 ms for the 613 computed bandpowers – three
orders of magnitude, measured live and on new parameter points so
the CAMB cache does not flatter the result. Which is why the notebook
uses best_fit and a Fisher matrix and says plainly that an MCMC here
takes days.
What it refuses: CAMB solves the perturbation equations of General Relativity, so a modified-gravity model is turned away rather than quietly handed GR perturbations.
Why it cannot pin tau on its own: plik_lite starts at l = 30
and reionization’s signature lives below it, so the high-l spectrum
measures A_s e^{-2tau} and not its two factors. Doubling tau alone
costs +4562 in chi-squared; doubling it with the amplitude
following costs -5 – nothing, and if anything preferred. That
contrast is the entire argument for adding "tau" or
"planck_lowe", and it is two lines of output.
That it works: six parameters from TT/TE/EE and a Gaussian tau
prior alone land inside 0.7 sigma of Planck 2018 on every one, and a
Fisher matrix reproduces Planck’s published uncertainty budget in 60
seconds – ratios 0.93 to 1.01 across all five – where a chain would
have taken days.
v0.37.2¶
About the examples. The library had gained models from an action,
scalar fields, evidence, profile likelihoods, tension statistics and
three holographic models, and examples/ showed none of it: nine
notebooks in a flat directory, covering the analyses the library had
grown out of rather than what it can do. Sixteen notebooks now, in
five sections with an index. 463 tests.
Seven new notebooks¶
Every one executed end to end against real data, with short chains (~3000 steps, except where the model itself is expensive and the notebook says why).
custom_models– the three routes to your ownE(z): an expression string, a function, aCosmologysubclass. Plusmu(a,k)for modified gravity, and what a singular Fisher matrix tells you.models_from_an_action– rederives LCDM to 2e-16 andFQExponential’s transcendental constraint to 1e-15, fits a power-lawf(T)the library does not provide, and demonstrates all three refusals with their actual messages.scalar_field_models– both Copeland-Liddle-Wands attractors reproduced to five decimals, the wall where a steep potential cannot reach the requestedOmega_m, and why the field’s initial conditions go early rather than today.holographic_family– HDE, ADE and RDE against arXiv:2607.09732, including that paper’s split between probes, which falls out without being used as an input: supernovae pullcto 1.183, BAO to 0.930, combined 0.999.evidence_and_model_selection– and the three tools disagreed, which was not staged. wCDM against LCDM on CC+DESI+Pantheon+:Delta chi2 = 3.46for one parameter (p = 0.063), AIC favours wCDM, BIC favours LCDM, andln B = -0.83prefers LCDM – the opposite direction fromDelta chi2. Nothing is wrong with any of them; they answer different questions.profile_likelihood_and_fisher– Fisher against MCMC (ratios 0.87 to 1.07 here), profile against marginal, and why a parameter on a boundary breaks the usualDelta chi2reading.tension_statistics– four definitions of “they disagree at 4 sigma”: Gaussian, sample-based, N-dimensional, and suspiciousness, which divides the prior dependence out.
dataset_zoo covers all 21 datasets¶
It covered eight, and said “all eight” – written when that was true
and left behind by thirteen additions since. Three things the old one
could not say, because everything it knew about was Gaussian: that
three datasets ship a likelihood surface rather than a mean and a
covariance; that a tabulated likelihood’s chi-squared has an
arbitrary zero point (planck_lowe reads 396 for 28 points and is
not a bad fit – only differences in it mean anything); and that there
are sixteen conflicting pairs, against the two the old notebook
named by hand, read from CONFLICTING_DATASETS itself so the notebook
cannot drift from the code.
Three bugs the examples turned up¶
All of the kind that only surface when someone follows the documentation rather than the tests.
A direct Cosmology subclass could not be constructed. The
distance integrator interpolates 1/E(z) with a Hermite spline built
from dEdz – that is what makes it exact to fourth order instead of
second – so it needs the derivative from __init__ onwards.
define_model installs a central-difference fallback; subclassing
deliberately does not, so a model can supply the exact derivative.
That choice was made when dEdz only fed the deceleration plot, and
the switch to Hermite splines silently promoted it to a hard
requirement. Nothing said so: the abstract methods raised a bare
NotImplementedError from inside __init__, nowhere near the missing
method – and the subclassing example in the README did not run.
Fixed in both directions.
An invalid value in an expression model arrived as
KeyError: '__import__'. model_from_expression evaluates with
builtins removed, and an optimizer restart landing where the
expression goes invalid is fine – E(z) comes back non-finite,
chi-squared does too, the point is rejected. What it could not survive
was numpy reporting it: emitting a RuntimeWarning runs
warnings.warn, which needs the builtins that were removed. Fixed by
evaluating inside np.errstate(all="ignore"); adding __import__
back would have opened the door the empty builtins dict exists to
close.
The eBOSS Lyman-alpha likelihood could not be evaluated at all under
SciPy >= 1.18. That release changed RectBivariateSpline.ev to
return a length-1 array where a scalar query previously gave a 0-d
one, so float(log_likelihood()) raised TypeError. The 3-D branch
already restored the caller’s shape; the 2-D one now does too.
v0.37.0¶
Finishes the piece the previous release deliberately left out.
CosmoFit.theory could already reduce an action with scalar fields,
but build() refused, because the coupled system has to be integrated
rather than solved redshift by redshift. It no longer refuses. Each
field adds two parameters: its value and dphi/dN at z_init. 458
tests.
Where the equations come from¶
Nothing further was typed in. The Friedmann constraint is a first
integral of the equations of motion, so rather than separately
varying a to get the acceleration equation, this differentiates the
constraint along the solution and solves that together with the field
equations. The two routes are equivalent by the Bianchi identity, and
this one reuses expressions already in hand – and leaves the
constraint as an independent check, imposed only in the initial
conditions and never again, so its drift measures the integration
error rather than restating the tolerance. It stays at 1.7e-13.
A design decision, got wrong first¶
The field’s state is set at z_init (default 3000), not today. The
first version set it at a = 1, which is far more convenient: H = 1
holds there by the definition of H0, so E(0) = 1 is algebraic and
no shooting is needed at all.
It is also wrong. Integrating backwards from a field at rest
today, the Hubble friction that damps the field forwards in time
becomes anti-friction, the generic past solution is kinetic-dominated,
and rho_phi grows as a^-6. For exponential quintessence normalized
to today’s dark-energy density that gives E(2.5) = 9.3, where LCDM
gives 3.7. Nothing about the calculation fails. It is the correct
past of those initial conditions, and those initial conditions are not
a universe. This is the same trap ADE was rewritten out of one
release earlier – worth recording as a pattern rather than an
incident.
So the state is given where a quintessence model actually gives it,
early and typically frozen, the system is integrated forwards, and
E(0) = 1 becomes a shooting condition on closure=. Forwards is
also the numerically stable direction.
Validation: Copeland, Liddle & Wands (1998)¶
Their late-time attractors for an exponential potential are exact
functions of the slope alone, so there is nothing to tune. On a matter
background there are two, selected by lambda^2 against 3, and
both are reproduced to five decimals, from the action: the
field-dominated attractor w = -1 + lambda^2/3 at lambda = 0.5, 1.0,
1.5, and the scaling attractor at 1.9 and 2.0, where the field
tracks the matter it scales against at a fixed fraction
3/lambda^2. That fraction is the sharp test – a pure prediction of
the slope with no freedom left anywhere in the model.
It is also why a steep potential cannot accelerate: ask such a model
for more dark energy than its attractor allows and H(0) saturates
below 1 with no potential scale reaching it. That now arrives as a
statement about the model rather than as a stalled integrator. A
constant potential pins the other end – a cosmological constant
written as a field, reproducing LCDM to 1e-10.
Two things measured rather than assumed¶
Comparing integrators at equal tolerance is the wrong comparison. Radau looked clearly best that way. At equal accuracy, which is the comparison that decides anything, DOP853 reaches a tighter drift in 3.4 ms against 79.
w and Omega_de must be read off the field’s Lagrangian, not
off the background. The generic route – subtract the fluids from
E(z)^2 – cancels catastrophically once matter dominates: at
z = 2000 it is 2.4e9 minus 2.4e9 to get 0.7, nine digits gone. It was
returning w = -5.8 where the answer is -1. With p = L and
rho = 2 X L_X - L there is nothing to cancel. The fieldless models
keep the background route, since a modified gravitational sector has
no Lagrangian pressure to read, and its docstring now says where it
stops being trustworthy instead of leaving that to be discovered.
About 37 ms per parameter set, down from 58 by warm-starting the
shooting from its previous solution and carrying the secant slope with
it. Grid density turned out to be free – DOP853’s dense output is
cheap and the cost is all in the steps – so it is set for accuracy,
5e-12 relative in E(z).
v0.36.0¶
Where the last release was entirely about speed, this one is about
what the library can say: a way to build a model from its action
rather than from an E(z), the inference tools needed to judge
whether a model’s improvement is real, and two new dark-energy models.
20 models (was 18), 439 tests (was 354).
Models from an action¶
Every model in cosmology.models encodes the result of somebody’s
derivation: an E(z) transcribed from a paper. define_model lowered
the barrier to adding one but not the work – it still wants E(z).
CosmoFit.theory takes the input instead. What comes back is an
ordinary Cosmology subclass, so every dataset, likelihood, sampler
and plot works on it unchanged; Action.constraint() returns the
derived Friedmann equation symbolically, if the derivation was the
point rather than the fit.
The lapse is the whole trick. FLRW is written with an explicit
N(t), the action is reduced to a point-like Lagrangian in a,
adot and N, and varying the lapse gives the Friedmann
constraint – N is a non-dynamical gauge degree of freedom, which
is precisely why its variation produces a constraint rather than an
evolution equation. Setting N = 1 afterwards recovers the familiar
form; writing N = 1 from the start loses the equation altogether.
It rederives what the library already had by hand: R - 2*Lam gives
LCDM including curvature to 1e-16 in E(z) and dE/dz;
Q*exp(lam*Q0/Q) gives FQExponential with an identical constraint
and agreement to 1e-13; and T + A0*(-T)**b at b = 0 gives
LCDM again through the torsion sector. Lam = 3 Omega_de0 is
written nowhere – it falls out of the closure condition. The f(Q)
row is the demanding one: its constraint is transcendental and the
hand-written model inverts a Lambert W, so the derivation has to
produce the transcendental form from the action alone and the solver
has to land on the branch W_0 picks rather than on any root of it.
It refuses rather than approximates. A general f(R) is refused
(fourth-order; the integration by parts that removes addot would be
discarding something that is not a total derivative). An action that
does not satisfy E(0) = 1 is refused – it would predict every
distance wrong by a constant factor while looking entirely healthy.
And growth="quasi_static" is opt-in, because mu = 1/f' is a
statement about perturbations, which a background action does not by
itself determine. The three geometry scalars carry different signs
across the literature, and choosing wrongly inverts every modification
built on one with nothing downstream to complain, so they are fixed by
requiring an undeformed f to reproduce General Relativity exactly –
asserted in all three sectors rather than trusted.
Two bugs the tests caught, both a correct-looking Friedmann equation
hiding wrong physics: the scalar field arrived as a plain sympy
Symbol rather than phi(t), so V(phi) differentiated to zero and
the Klein-Gordon equation was wrong while the constraint looked
perfectly reasonable; and a root-finder turned loose on the whole
redshift range can switch branches of a transcendental constraint and
return a discontinuous E(z) without failing, hence solving by
continuation out from z = 0 where the closure condition guarantees the
root.
sympy is a new optional extra; CI asserts the minimal install still
works without it.
Bayesian evidence, profile likelihoods and Fisher matrices¶
Three things the notebooks kept building by hand or disclaiming:
stats.nested (dynesty), Fitter.profile() and Fitter.fisher().
And the first thing they did was change a verdict. LsCDM’s
Delta chi2 = 5.40 over LCDM had looked like real evidence; its Bayes
factor is ln B = +0.494 +/- 0.319 – inconclusive. The reason is
visible in the posterior: the transition redshift’s 95% interval runs
to [2.5, 96.6], so most of its prior volume does nothing and the
Occam factor eats the improvement. That prior sensitivity is now
measured rather than asserted – narrowing the range gives
ln B = +1.158. stats.tension adds Gaussian 1-D/N-D, sample-based
and suspiciousness statistics.
Four bugs these surfaced, three of them found by an answer that was impossible rather than merely surprising:
best_fit()settled in local minima – found by LsCDM fitting worse than the LCDM it contains, byDelta chi2 = -0.63.best_fit(restarts=n)added.best_fit()left the cosmology at the last evaluation, not at the best fit – which made a whole chi-squared breakdown table read +0.00 everywhere.A NaN parameter vector crashed from inside
refresh(). L-BFGS-B started where chi-squared was inf, computedinf - inffor the gradient, and evaluated at[nan, nan, nan];_apply()sat outsidechi2’s try block.likelihood_ratio_testreported an impossible result as “no evidence” –chi2.sf(negative) = 1.0andnorm.isf(1.0) = -inf, indistinguishable at a glance from a genuine null. It now warns, with a tolerance so nested-sampling convergence noise does not trip it.
A free neutrino mass now warns when nothing can see it¶
The compressed Planck distance priors are blind to m_nu by
construction: their z_star is a fitting formula calibrated at the
fiducial 0.06 eV and returns the same z_star, Omega_r and
r_s(z*) anywhere from 0 to 0.8 eV. A free m_nu there runs to 0.82
eV meaninglessly, and a posterior for it is not a neutrino-mass
measurement. The fit now says so and names what to add instead. Run
properly with the full CMB, the 95% profile bound is 0.039 eV against
a published Bayesian 0.064 eV, and the three candidate reasons for the
gap are flagged as unresolved rather than papered over.
Agegraphic and Ricci dark energy¶
Two more holographic models, validated against the same paper as
HDE. RDE’s gamma = 0.538 and Omega_m0 = 0.2173 sit inside both
published intervals; ADE’s n is ~2% high, and the honest reading is
that the model is right and the fit differs – n = 2.80 predicts
Omega_m = 0.280, exactly the pair that paper reports.
ADE has one fewer free parameter than LCDM. Its early-time
condition fixes the background from n alone, so Omega_m is not fit
but derived. New generic DERIVED_PARAMS declaration: a model
lists what it computes, and the fitter warns if one is set free, since
sampling it would just be reintroducing the prior.
Both were first written wrong in ways the tests caught. ADE was
integrated backwards from today with Omega_m free, which is
silently a different model. And RDE’s Omega_de(z) returned the
second term of E^2, but the Ricci density carries a matter-like
piece, so reading the two terms as “matter” and “dark energy”
misplaces about a third of the matter density. A third, from the
previous release: HDE.Omega_de(z) returned the density parameter
rho_DE/rho_crit(z) where the rest of the library means
rho_DE/rho_crit(0) – the two agree at z = 0, which is why it
passed, and are off by 0.59x at z = 0.5.
v0.32.0¶
Five commits, all of them performance. No new datasets, no new models, no API changes – the same 21 datasets, 18 models and 354 tests, running between 1.8x and 29x faster depending on the fit. Two of the changes also made the code more accurate, which is the part worth reading.
The profile was not where I expected¶
A growth fit with 68 data points cost 27 times what one with 1869 did. That was the whole finding, and everything else followed from measuring rather than guessing.
The growth ODE. solve_ivp at rtol=1e-8 called back into Python
for every stage of every step: 19,040 scalar right-hand-side
evaluations per solve, each evaluating E(z) three times. But the
equation is linear, smooth and non-stiff, so adaptivity was only ever
rediscovering a step size that never needed to change. It is now a
fixed 300-step RK4 with the coefficients evaluated for the whole grid
in four array calls.
The stepping itself, twice. RK4 is sequential, so there was no
array operation left to hand NumPy – 493 microseconds of pure loop.
Two ways round it, both implemented. Without numba, the equation
being linear means one RK4 step is a fixed 2x2 matrix and
composing them is a prefix product, which pairwise doubling does in
log2(n) rounds of batched work: 493 to 215 microseconds. With numba,
the same loop compiled: 5.2 microseconds. The speed extra was
already declared and nothing used it; it does now. The two agree to
8.9e-16, which a test asserts by running both, and CI covers both
paths.
x ** 3 goes through pow. NumPy special-cases x ** 2 and
x ** 0.5; it does not special-case cubes. On a 505-element array
that is 5.02 microseconds against 0.79 for x * x * x – and
Omega_m * (1 + z) ** 3 is the most-evaluated expression in the
library. Applied across seventeen files; E(z) dropped from 9.37 to
5.23 microseconds, and the rewrite differs from pow by one unit in
the last place.
Building the Hermite splines directly.
CubicHermiteSpline.__init__ validates, sorts and normalizes axes
before assembling coefficients that are four lines of algebra: 43
microseconds against 13. cosmology.refresh() – paid by every fit on
every step – went from 99 to 48.
Two places this got more accurate, not less¶
The distance integral. chi(z) was a plain trapezoid fitted with
a shape-preserving Pchip, which spent most of its build time deriving
slopes that are known exactly, since chi'(z) = 1/E(z), and got them
slightly wrong. It is now a corrected (Euler-Maclaurin) trapezoid and
a Hermite spline built from that same derivative. Against quad, the
maximum relative error on the default grid falls from 4.5e-06 to
1.3e-10.
The recombination and sound-horizon integrals.
scipy.integrate.simpson accepts an arbitrary x and pays for that
generality whether or not the grid is uniform – 36 microseconds of a
75 microsecond integral. Every grid here comes from linspace or
logspace, so uniformity is known at the call site. r_s(z*) goes
from 2.5e-07 to 2.8e-09 and r_d from 3.2e-09 to 3.4e-11,
both roughly twice as fast. A uniform-grid rule on the substituted
integrand is simply a better rule than a general one on the original.
What was measured and rejected¶
jax – its win comes from JIT-ing and fusing a whole computation,
which would mean rewriting every model’s E(z) in jax primitives.
That is a rewrite, not an optimization. A Cholesky factorization of
the supernova precision matrix – exact and worth 1.23x, but it
costs 106 ms at dataset load against 131 ms for the entire load, so it
makes short runs and the test suite slower to make long chains faster.
float32 for that matrix – 1.3x, for 1.8e-05 absolute in
chi-squared; not for sale in a library whose recent conclusions turned
on a Delta chi2 of 0.30. A symmetric dsymv – 15x slower,
because SciPy copies the C-contiguous matrix to Fortran order first.
A wrong turn, recorded¶
cProfile attributed ~150 microseconds per evaluation to simpson. I
wrote a direct replacement, found it worth 1.4, concluded the profiler
had been inflated by its own per-call overhead, and reverted. That
conclusion was wrong, and the check was the reason: I had timed
simpson(y, dx=h), a different and three times faster code path than
the simpson(y, x=grid) the callers actually used. The profiler had
been right, and finding that later produced the last commit in this
release – the one that also improved the accuracy.
v0.31.0¶
Mostly about BAO that is not a Gaussian, one new model, and four bugs – three of which were found by an answer that was impossible rather than merely surprising. 21 datasets, 18 models, 317 tests.
BAO released as a likelihood surface¶
Every BAO dataset here used to be a mean and a covariance. Three of eBOSS DR16’s are not, and in each case the collaboration released a grid because a Gaussian would misrepresent the measurement.
eboss_elg – D_V/r_d at z = 0.845, a 399-point curve. The BAO
feature is a 1.4 sigma detection, so the likelihood is asymmetric
and still rising at the low edge of the released table. About a
tenth of its probability sits below D_V/r_d = 16.5, where a Gaussian
at 18.33 +/- 0.6 puts a thousandth – two orders of magnitude, in
exactly the tail that decides whether a low expansion rate at
z ~ 0.85 is excluded.
eboss_lya – (D_M/r_d, D_H/r_d) at z = 2.334, a 50x50 surface;
the highest-redshift BAO in the library outside the CMB, and
independent of DESI’s.
eboss_elg_fs – the ELG sample again, full shape: a
100x100x100 grid. Three dimensions is the point rather than an
inconvenience, since fsigma8 is degenerate with the Alcock-Paczynski
distortion and a grid carries that exactly where two error bars could
not.
All validated against the papers with the published numbers used
nowhere as inputs. The ELG pair is more than a central-value check: it
identifies which interval the paper quotes – that table’s standard
deviation is 1.05 and its 68% credible interval is +0.52/-1.21, and
only the Delta chi2 = 1 crossings give the published asymmetric
pair. The Lyman-alpha check does double duty: eBOSS quote the combined
constraint from a joint fit but ship the auto- and cross-correlation
surfaces separately, so this multiplies them, which assumes
independence – recovering the published errors from the product is
what makes that defensible, since a neglected correlation would have
come out as errors too tight.
Two things this cost, both now tests. Interpolating the released probability directly does not work – thirty orders of magnitude, so a cubic is dominated by the peak and undershoots to negative values that have no logarithm. And for the 3-D grid, splining the log also fails: it ships floored 200 below its peak and a cubic rings at the step where that plateau starts, reaching a log of +146 against a node maximum of 0. That path is linear, and a test asserts the no-overshoot property directly.
sdss_fsbao, and a gap it uncovered¶
The SDSS BAO + full-shape consensus: D_M/r_d, D_H/r_d and
fsigma8 at four redshifts, with the covariance between them.
Reproduces Alam et al. (2021) Table 3 exactly, and the errors come
from a covariance file that is a separate product, so that also checks
the two were paired correctly. The real content is the correlation:
within a bin, D_M/r_d and fsigma8 are correlated at +0.388,
+0.389, +0.185, +0.636.
Which is the gap. sdss_bao + fsigma8 had been reachable without
a warning all along – the growth compilation includes BOSS z = 0.38
and the eBOSS quasars, so the pair covers the same galaxies twice
while treating geometry and growth as independent. Now registered as
conflicting, with the joint product named as the alternative.
DESI full-shape is not here and cannot be on this library’s terms: DESI publish MCMC chains and the full modelling-pipeline inputs but no compressed Gaussian summary, so using it properly means implementing an EFT model with its nuisance parameters. That is well outside a background-plus-linear-growth library and could not be validated cheaply.
Holographic dark energy¶
The holographic principle bounds the energy in a region by its
boundary area, giving rho_DE = 3c^2 M_p^2 / L^2; Li (2004) showed
L has to be the future event horizon for the universe to
accelerate at all. Architecturally it is the first background model
here without a closed-form E(z) – Omega_DE obeys an ODE,
solved and splined on every refresh().
Validated against the model’s definition rather than its equation of
motion, which is the point: for an ODE model there is nothing to check
by inspection, and a wrong equation gives a smooth, plausible,
entirely wrong history. The test computes the future event horizon by
quadrature from the solved E(z), with the ODE appearing nowhere in
it, and checks H(a) L(a) = c / sqrt(Omega_DE(a)). It holds to 2e-3
for c from 0.6 to 1.3, and a second test deliberately corrupts the
ODE and asserts the check notices, so it cannot quietly become
vacuous. That check is also why the solution runs forward to
a = 1e5: nothing asks for z < 0, but a model defined by the future
should be answerable about it. Flat only, and it says so – curvature
changes the causal structure the holographic bound is applied to.
Four bugs¶
best_fit() settling in local minima; a NaN parameter vector crashing
from inside refresh(); likelihood_ratio_test reporting an
impossible result as “no evidence”; and that warning then firing on
convergence noise – a 32-fit scan returned five negatives between
-8e-06 and -5e-04, every one at the nested limit where the two
optimizers reach the same minimum by different routes, and warning
about those trains the reader to ignore the warning.
Two notebooks¶
The LsCDM result in lscdm_mcmc.ipynb is a DESI result. Section 8
had found the transition-redshift constraint as a cliff –
chi-squared falling by 28 between adjacent grid points, straddling
DESI’s Lyman-alpha bin at z = 2.33 – and could not say whether that
was a property of the universe or of one measurement. With eBOSS’s
independent Lyman-alpha at essentially the same redshift, it can: the
Delta chi2 = 5.40 preference becomes 0.30, and the 28.04-unit
step becomes a 1.6-unit ripple. The mechanism turned out not to be
what the cliff suggested – broken down by dataset, DESI’s 5.40 is
2.72 in Planck and 2.12 in DESI’s own BAO, and with eBOSS the Planck
term is already 0.10: no tension, nothing to relieve.
dark_energy_evidence_audit.ipynb – the same question asked
twenty ways. LsCDM’s improvement is a compound of all three choices:
0.00 with no BAO, 0.30 with eBOSS, and – the sharpest – 5.40 drops
to 0.81 when r_d is fitted rather than computed. The evidence lives
in a CMB-calibrated BAO ruler disagreeing with the CMB’s own distance
priors; remove the calibration and there is nothing to relieve. CPL’s
is smaller and does not depend on any single choice, and all twenty
cells land on w0 > -1 and wa < 0.
v0.28.0¶
v0.25.0 computed the CMB; this finishes it, adds a second
telescope, and closes the loophole that let the whole thing be tested
against nothing. 230 tests (from 188), 17 datasets.
The Planck 2018 likelihood, all four parts¶
v0.25.0 shipped one component of four. The CMB is now complete –
652 data points, each part validated against a published number
rather than against itself: Commander low-l TT (data shipped, never
read), plik_lite high-l TT/TE/EE (already there), SimAll low-l EE
(stood in for by a Gaussian tau prior) and lensing (missing
entirely).
Low-l EE is the real thing, not the tau = 0.0544 +/- 0.0073
shorthand. Below l = 30 there are only 2l+1 modes on the sky, the
distribution is strongly skewed, and that regime carries essentially
all of the CMB’s information about tau – so Planck ships a
probability table and the likelihood is a lookup, with no mean and no
covariance. It is the library’s first non-Gaussian likelihood, and
BaseLikelihood no longer assumes every dataset has a covariance to
invert. Validated by reconstruction: profiling tau against that
table plus the high-l bandpowers returns 0.0541 +/- 0.0072 against
Planck’s published 0.0544 +/- 0.0073, with that number used nowhere as
an input, and the fitted amplitude tracks tau with slope 2.00, which
is the A_s e^{-2tau} degeneracy exactly.
Lensing is the CMB’s own growth measurement – every other CMB dataset here constrains recombination and reaches the present only through a distance. It is also the part that is easy to drop: the reconstruction is normalized against an assumed cosmology, so the linear correction propagating that dependence vanishes at the fiducial by construction. A chi-squared check at Planck’s best fit therefore passes whether or not it was implemented, and the tests check it away from there.
ACT DR6 lensing¶
A second, independent reconstruction – different telescope, different
sky, different pipeline, and tighter than Planck’s: 2.3% on the
lensing amplitude. Fitting a single scaling of the theory at
Planck’s best-fit LCDM returns A_lens = 1.017 +/- 0.026 against
ACT’s published 1.013 +/- 0.023, with Planck’s own lensing at the same
cosmology giving 0.998 +/- 0.025.
That test earns its place because the failure mode is a smooth
rescaling. ACT’s products are built on the lensing convergence,
Planck’s on the potential, and the two differ by 2 pi / 4. Get it
wrong and nothing crashes: the theory comes out uniformly off, a fit
absorbs it into the amplitude, and the posterior looks ordinary and
sits somewhere else – A_lens would read 0.65 or 1.6 instead of 1.
sigma8 derived instead of fitted¶
sigma8 was defined twice – the free parameter the growth
machinery normalizes with, and CAMB’s derived value from the primordial
amplitude – and nothing made them agree.
Fitter(derive_sigma8=True) takes the amplitude from the Boltzmann
code and drops it from the sampled parameters. This is what makes the
CMB predict growth rather than accommodate it: with sigma8 free,
an S8 measurement is reproduced to chi-squared ~ 0 and nothing has
been tested, because a free parameter slid onto it.
best_fit() was returning its starting point¶
The most serious bug in this release, and it was silent. L-BFGS-B
estimates gradients by finite differences with a step of ~1.5e-8. For
a likelihood whose own numerical noise exceeds the chi-squared change
such a step produces – which is every CAMB-based likelihood – the
estimated gradient is zero and the optimizer returns the starting
point while reporting success=True. The caller gets their initial
guess back labelled “best fit”, every downstream number is computed
from it, and nothing anywhere says so. On the full Planck CMB it cost
366 in chi-squared.
best_fit() now detects the stall – movement below 1e-6 of each
parameter’s own prior width, which is the only scale-free way to ask,
given that H0 and Omega_b differ by three orders of magnitude –
and retries with Nelder-Mead on a prior-scaled simplex, keeping the
better of the two. Passing eps= or method= suppresses the rescue:
the caller has taken control. Deliberately narrow, because a larger
step is not uniformly better – on CPL with CC+DESI+Planck it finds
a minimum 2.75 worse.
Infrastructure¶
Continuous integration, finally. Two jobs: the full suite with
every extra across Python 3.11-3.13, and a minimal job installing
core dependencies only – the configuration a plain
pip install cosmofit produces. The second is not redundant: CAMB and
Streamlit are optional and every test needing them skips, so it is the
only thing checking that the skip paths skip rather than erroring.
It asserts both extras really are absent first, so a leaked transitive
dependency cannot turn it into a duplicate of the first.
The neutrino-mass path is now pinned. It worked when it was built
and no test touched it. It has two consumers that never talk to each
other – in the sound horizon the neutrinos are relativistic at the
drag epoch and raise r_d; in CAMB they free-stream out of the
small-scale power and lower sigma8 – and both read the same
Omega_m. Getting the subtraction right in one place and wrong in the
other would not raise; it would move the two in opposite directions by
a fraction of a percent, comfortably inside the range where a fit
still looks plausible and is wrong.
examples/s8_tension_cmb.ipynb¶
The S8 tension posed from the CMB side, using everything above: all
652 Planck points, sigma8 derived, and neither weak-lensing
measurement in the fit. The fit reproduces Planck’s published
parameters to within 0.16 sigma, which is the bar it had to clear
before anything else means anything, and predicts
S8 = 0.8307 +/- 0.0104 – 2.9 sigma from KiDS-1000, 2.7 sigma from
DES Y3. ACT DR6’s lensing never entered the fit, so its A_lens is
an out-of-sample check on the whole chain, Boltzmann code included.
The sigma8-free version of the same question returns chi-squared =
0.000000 and the tension is invisible. And the growth compilation
gives chi-squared/N = 0.79 against the same prediction – the
disagreement is in weak lensing, not in redshift-space distortions.
v0.25.0¶
A pass over the graphical interface, on the principle that a tool which cannot be used wrongly is worth more than one with more knobs.
The app had grown to fourteen datasets and seventeen models presented as a flat checkbox list and a flat dropdown, with no indication of what any of them was. That is a problem specific to this domain: a wrong combination does not error, it returns a perfectly ordinary-looking posterior with error bars that are too small, or a “measurement” of a parameter nothing in the fit constrains.
Everything now explains itself. Each dataset says what it
measures, over what redshift range, how many points, what it actually
constrains, and where it comes from – grouped by probe, since a fit
is built by taking one from each family rather than by ticking
everything. Each model says what it is, what its extra parameters
mean, and which parameter values reduce it to ΛCDM, plus capability
badges for w(z), growth, and whether the full CMB spectra can be
computed for it.
Six presets configure a whole analysis at once. “DESI DR2 + BBN → H₀ without the CMB” sets the datasets, selects DR2, and turns on the computed sound horizon – none of the three is useful without the other two, and expecting someone to discover that from three separate widgets was optimistic.
The parameter table now shows only the parameters this fit uses.
The shared container carries every parameter any model needs; for an
LCDM fit against CC and BAO, all but four are inert. Relevance is a
property of the fit rather than the model, so it follows the datasets
too: rd appears only with BAO, sigma8 only with growth data,
n_s/τ only with the CMB spectra. The hidden ones still reach the
Fitter – they are inert, not absent.
Warnings fire before the run, not after. One is a hard error the
app can see coming (a modified-gravity model with the full CMB
spectra). The rest are quieter and matter more, because the fit will
run, converge, and produce something that looks like a result: a
modified-gravity model with no growth data ticked, a model whose own
parameters are left fixed, a free sigma8 nothing constrains, a
computed r_d with Ω_b held fixed.
Three things the GUI simply could not show before:
χ² per dataset, which turns a total χ² into a statement about which dataset is in tension. On the Hubble-tension preset it reads DESI 19.6/13, Planck 7.2/3, local H₀ 24.5/1 – the whole argument, in one table.
Derived quantities (
q₀,z_t,r_dfrom the densities) with real error bars. These have existed inCosmoFit.stats.derivedsince v0.13.0 and were reachable only from Python.Dataset versions. DESI DR2, the SH0ES 2024 and TDCOSMO H₀ measurements, DES Y3’s
S₈and the Cooke BBN prior all shipped in v0.23.0 and were unreachable from the GUI, which never passeddataset_kwargs. Each dataset with more than one version now has a picker.
A 📖 Guide panel lists every dataset and model in one browsable table with the conflict rules and a short walkthrough. Both tables are generated from the same dictionaries the widgets use, so a dataset added to the library without a note shows up as a blank row rather than silently.
Two bugs, and the first GUI tests¶
st.checkbox was being given both a value= and a session-state
entry under the same key – the one thing Streamlit explicitly warns
about, since the two disagree about which is authoritative. And the
preset button called st.rerun(), which is the obvious idiom and was
never needed here (the button sits above the widgets it writes to, so
the values land on the same pass); it was also an infinite loop
anywhere a button’s click state outlives the rerun.
That second one was found by the new tests/test_gui.py, which is the
point of it. Streamlit’s AppTest runs the app in-process and exposes
the rendered element tree, so 12 tests now drive the flows a user
actually takes: a preset writes the configuration it claims to, a fit
runs end to end and renders its tables, the parameter table follows
the datasets, and each warning appears when it should. 159 tests
total, ~18 s.
Two small public additions came out of this, both because the GUI
needed them and reaching into private state to get them would have
been worse: dataset_reference(dataset, version) returns a dataset’s
citation without loading its files, and
cosmology.boltzmann.supports_cmb_spectra(model) answers “can CAMB do
this model?” as a value rather than by raising – the backend now
routes its own check through it, so the two cannot drift apart.
v0.24.0¶
Computes the BAO sound horizon r_d from the
physical densities instead of fitting it.
What was wrong with fitting it¶
Nothing, exactly – treating r_d as a free nuisance parameter is a
defensible and common choice, and it makes BAO immune to any
assumption about the early universe. It is also the reason this
library could not measure H0 from BAO. H0 and r_d enter every
BAO observable only through the product H0 * r_d, so with r_d
free they are perfectly degenerate and BAO constrains neither.
SoundHorizon was, until now, a nine-line stub that returned the
free parameter. It now does the integral:
r_d = int_{z_d}^inf c_s(z)/H(z) dz,
c_s = c / sqrt(3(1 + R_b)), R_b = 3 omega_b/(4 omega_gamma) a
What is actually computed, and what is not¶
Computed from first principles: photon density from T_CMB;
massless neutrinos; the baryon loading; the integral itself.
Massive neutrinos, exactly. They are relativistic at the drag
epoch (y = m/kT ~ 0.34 for 0.06 eV) and matter-like today, and the
transition matters. The usual [1 + (Ay)^p]^(1/p) approximation
costs 0.05% in r_d at Sum m_nu = 0.06 eV and 0.15% at 0.6 eV –
the latter half of DESI’s best BAO precision, spent on an
approximation with no reason to be there. Instead the Fermi-Dirac
energy density integral is tabulated once at import (a few ms) and
splined afterwards. The familiar Sum m_nu / omega_nu h^2 = 93.14 eV
shorthand is then not an input at all: the calculation derives
93.0378 eV, where CAMB gets 93.04.
Fitted, and honestly labelled: z_drag alone. The drag epoch is
defined by a Thomson-drag optical depth over a full recombination
history, which is a Boltzmann code’s job. This takes the same route
the library already took for z_star in v0.19.0 – a fit calibrated
directly against CAMB, over a 5850-point grid spanning omega_b in
[0.018, 0.026], omega_cb in [0.09, 0.20], N_eff in [2.0, 5.0] and
Sum m_nu in [0, 0.6] eV.
Eisenstein & Hu (1998)’s
z_dragformula is bundled for comparison and is not usable here: it gives 1020.7 where CAMB gives 1059.9, 3.7% low, which putsr_d2.5% high – ten times DESI DR2’s best BAO error bar. That is not a flaw in EH98; theirz_dwas calibrated jointly with their own closed-formr_s, and the two halves are not separately meaningful. Splicing one of them onto a modern integral is exactly the convention error v0.19.0 documents at length, in a different place.
Accuracy¶
Against CAMB’s rdrag over that whole grid:
max error |
|
|---|---|
the integral alone, given CAMB’s |
2.2e-6 |
the |
6.7e-5 |
end to end |
5.0e-5 |
end to end, within realistic priors |
1.4e-5 |
DESI DR2’s single best BAO bin is a 0.24% measurement, so the worst case is ~50 times smaller than the best data’s error bar and the typical case ~500 times. A trimmed copy of that grid ships as a test fixture, so the comparison runs without CAMB installed.
r_d depends on less than you might expect¶
The integral runs entirely through the radiation- and
matter-dominated eras, so r_d does not depend on H0, on
curvature, or on the dark-energy model at all – only on
omega_b, omega_cb, N_eff and m_nu. Verified against CAMB,
which returns the same rdrag to 1e-7 across H0 from 60 to 75.
Two consequences. It works identically for every model in the
library, including the modified-gravity ones a Boltzmann code will
not accept. And the cache keys on the densities alone, so an MCMC
step that moves only w0 reuses it – which matters, because the BAO
likelihoods ask for r_d once per data point.
Using it¶
Off by default: switching a fitted nuisance parameter into a derived quantity changes every BAO prediction, and that is a choice about the analysis.
fit = Fitter(
model=LCDM,
datasets=["desi", "omega_b"], # BAO + the BBN prior
dataset_kwargs={"desi": {"version": "desi2025"}},
free_params=["H0", "Omega_m", "Omega_b"],
initial={"H0": 68.0, "Omega_m": 0.30, "Omega_b": 0.0493},
compute_rd=True,
)
rd must not be in free_params alongside it – the likelihood
would ignore the sampled value and its “posterior” would be its
prior – and the constructor says so rather than letting it happen.
Omega_b becomes the parameter BAO now needs and cannot pin down on
its own, which is what the "omega_b" BBN dataset is for. Running
that fit (DESI DR2 + BBN, flat ΛCDM) gives
H0 = 68.55 +0.59 -0.60
Omega_m = 0.2977 +0.0087 -0.0085
Omega_b = 0.0472 +0.0008 -0.0008
r_d = 148.10 +1.59 -1.63 Mpc (derived)
which lands where published DESI BAO+BBN constraints do – an end-to-end check of the whole chain that no single unit test provides.
r_d is a derived quantity now, so it leaves summary() and joins
z_t and q0 in CosmoFit.stats.derived:
from CosmoFit.stats import derived
derived.summarize(derived.sound_horizon(fit))
That works whether or not the fit used it. With compute_rd=False
it returns what the early-universe physics would have predicted for
the same densities, and comparing it against the fitted r_d is a
real consistency test: a mismatch is the standard signature of new
physics before recombination, and one of the main ways the Hubble
tension is diagnosed.
compute_rd is part of the chain signature, so a chain sampled one
way cannot be resumed the other. The GUI gets a checkbox, which
un-ticks rd for you and points at the BBN dataset if it is missing.
One bug this surfaced¶
Nothing in the growth machinery: Fitter now also refuses
compute_rd=True together with a free rd, which was previously
expressible and would have produced a rd “posterior” identical to
its prior with no indication anything was wrong.
Seventeen new tests (147 total) cover the neutrino thermodynamics, the CAMB comparison, the independence claims above, quadrature convergence, and the wiring.
v0.23.0¶
The largest single addition since the library began: six new datasets, eight new cosmological models, the first test suite, and – the headline – the CMB computed from scratch rather than compressed.
Planck, uncompressed¶
Until now “Planck” meant three numbers: the distance priors (R, l_A, omega_b h^2). That is fast, dependency-free and works for every model, and v0.19.0 documents at length how badly it can go wrong, because a compression carries the conventions of whoever produced it and the theory prediction has to share them exactly.
"planck_lite" is the other end of that trade. It uses the actual
measured spectra: 613 binned TT/TE/EE bandpowers over
l = 30-2508 with their full 613x613 covariance, compared against a
C_l spectrum computed by a Boltzmann code. No compression, no
summary statistic, no borrowed convention – the theory prediction
is the same object the data is.
CosmoFit does not implement a Boltzmann hierarchy, and should not:
that is thousands of coupled ODEs per wavenumber through
recombination, and a pure-Python version would be far too slow for
an MCMC. A new cosmology/boltzmann.py translates a Cosmology
into CAMB’s parameter conventions and calls it, as an optional
dependency (pip install "cosmofit[cmb]"). Nothing else in the
library imports it.
Three details that decide whether this is right or subtly wrong:
Which models can go through it. LCDM maps onto CAMB directly. Any model exposing a
w(z)(wCDM, CPL, JBP, BA, GCG, and the new PEDE/GEDE/IDE) is passed through CAMB’s PPF dark-energy module as a tabulatedw(a)– exact at background level, and stable across thew = -1crossing that CPL posteriors routinely visit. The modified-gravity models are refused outright.FRHuSawickiwould have run happily – its background is LCDM’s by construction – and returned LCDM’s C_l withf_R0doing nothing, which is worse than an error.Massive neutrinos. CosmoFit counts them inside
Omega_m; CAMB counts them separately. Soomch2isOmega_m h^2 - Omega_b h^2 - Omega_nu h^2– subtracting. Adding instead shiftsOmega_c h^2by ~0.0006, about half a sigma of Planck’s constraint on it, with no other symptom.A CAMB API trap, found by testing.
pars.DarkEnergy = objcopies the object into CAMB’s Fortran state, so setting thew(a)table on the Python instance afterwards is silently discarded – and CAMB then returns a perfectly valid cosmological-constant spectrum for a w0-wa model. Caught by asserting that CPL atw0 = -1, wa = 0reproduces LCDM and that CPL atw0 = -0.9, wa = -0.4does not.
Validation. Against the reference spectrum shipped with
planck-lite-py, this implementation reproduces its published
log-likelihood exactly: -291.33481235418026 for TTTEEE
(bit-identical) and -101.58123068722571 vs -101.58123068722583 for
TT (1e-13, matrix-inversion roundoff – Cobaya’s own plik_lite
differs from planck-lite-py by 2e-13 on the same number).
Independently, at Planck’s best-fit LCDM it returns chi2 = 585 for
613 bandpowers.
What it costs. One CAMB call is ~0.7 s against ~1 ms for the
whole rest of a joint likelihood, so a chain including this is
roughly three orders of magnitude slower per step. That is not an
implementation flaw – it is why full CMB chains run on clusters and
why compressed priors exist. Budget hours, use n_processes, and
save the chain. A CMB spectrum also needs ln1e10As, n_s and
tau_reio, which no background fit ever did; and because
plik_lite starts at l = 30, tau is degenerate with the amplitude
unless the new "tau" dataset is included.
The primordial amplitude is
ln1e10As, notA_s– that name was already taken in this library by the Generalized Chaplygin Gas parameter, an unrelated quantity that shares the symbol in its own literature. Renaming the GCG one would invalidate every saved chain that names it.
Six new datasets¶
DESI DR2 BAO (2025),
dataset_kwargs={"desi": {"version": "desi2025"}}– three years of observations, >14 million galaxies and quasars, twice the DR1 sample, and the measurement the strengthened evolving-dark-energy claim rests on. Identical file format to DR1, so it drops into the same loader. Not to be stacked with DR1: DR2 contains every DR1 galaxy.Union3 (
"union3") – 2087 supernovae fit with the UNITY1.5 hierarchical model and released as 22 binned distance moduli. The third of the three compilations the DESI dark-energy results are argued with, and they disagree about how far the data sits from a cosmological constant: DES-SN5YR pulls hardest, Pantheon+ least, Union3 in between. A library that can only fit one of them cannot reproduce that comparison, which is the actual state of the evidence.Low-z BAO (
"bao_lowz") – 6dFGS (z = 0.106) and SDSS DR7 MGS (z = 0.15), the only BAO leverage below z = 0.2 (DESI starts at 0.295, BOSS at 0.38). Independent of both, so unlike DESI-vs-SDSS these can be added to either. Two details are handled rather than papered over: 6dFGS reportsr_s/D_V, kept as its own observable rather than inverted (inverting a Gaussian gives something that is neither Gaussian nor centred on 1/mean), and its measurement is calibrated against an Eisenstein-Hu fitting-formula sound horizon, so the theoryr_dis rescaled by 153.9/149.8 – 2.7% on a 4.5%-precision point, the same class of definitional mismatch v0.19.0 documents."h0","omega_b","tau"– external single-number measurements: SH0ES 2022/2024 and TDCOSMO 2025 time-delay lensing for H0, BBN (Schoeneberg 2024 or Cooke 2018) for omega_b h^2, and Planck lowE for tau.These are datasets, not priors, and the distinction is deliberate. The posterior is identical either way, but a prior is a statement of belief before seeing data, while “SH0ES measured 73.04 +- 1.04” is data from a telescope with a systematic error budget. As a dataset it shows up in the per-dataset chi2 breakdown, in the degrees-of-freedom count AIC/BIC use, in figure legends, and in the chain metadata that decides whether a saved chain may be resumed. A fit that quietly assumed the local distance ladder should not look, from the outside, like a fit that did not. It also makes the H0 tension askable in the form it is argued: run the same model with and without
"h0"and compare what each dataset contributes.
The BBN prior is more than an extra data point: BAO measures
D/r_d, and with a BBN constraint on omega_b a BAO-only fit gains
a CMB-independent route to H0 – exactly how the DESI “BAO + BBN”
constraints are produced.
Overlapping datasets now warn¶
Every “don’t combine X and Y” rule in this README was, until now,
written down only in a docstring, where it protected nobody who did
not go looking. Fitter now checks and warns, naming the reason.
The failure it guards against is silent: overlapping data treated as
independent gives a perfectly ordinary-looking posterior with error
bars that are too small. It stays a warning, not an error –
quantifying how much an overlap matters is a legitimate thing to
want to do.
Two new rules join the existing ones: Union3 must not be combined
with Pantheon+ or DES-SN5YR, and "planck" must not be combined with
"planck_lite" (the distance priors are a compression of exactly
those bandpowers – that is the entire CMB dataset twice).
Eight new models¶
Grouped by what they actually change, since that decides what can be done with them:
Model |
Extra parameters |
Reduces to |
|---|---|---|
LogarithmicDE |
none (reuses |
wCDM at |
PEDE |
none at all |
— |
GEDE |
|
LCDM at |
LsCDM |
|
LCDM below the transition |
IDE |
|
wCDM at |
RunningVacuum |
|
LCDM at |
Cardassian |
|
LCDM at |
DGP |
none at all |
— |
Every reduction in that last column is a test
(tests/test_models.py), asserted to machine precision, not a
docstring claim. That matters more than it sounds: the Friedmann
closure E(0) = 1 pins some normalizations but not all of them, and
a limit check pins the rest.
Two of these – PEDE and DGP – have exactly LCDM’s parameter count and a completely different expansion history. That makes the model comparison unusually clean: identical AIC/BIC penalties, so a chi2 difference is a difference in fit and nothing else.
DGP also overrides mu(a, k) with the standard Koyama-Maartens
result, so it joins the three modified-gravity models in predicting a
growth history that differs from GR’s at fixed background. On the
self-accelerating branch gravity is weaker (mu = 0.72 today,
matching the literature), and the bundled fsigma8 data feels it
directly.
A matter-scaling bug this surfaced¶
RunningVacuum and IDE both change how matter dilutes – that is
their whole content. But GrowthCalculator read Omega_m(a) off
Omega_m (1+z)^3 for every model, so those two would have been given
LCDM’s growth source term alongside their own E(z): internally
inconsistent, and silent. Cosmology now has an overridable
Omega_matter(z) hook that both models implement and every other
model inherits unchanged. No existing result changes; the two new
models get a growth history that is actually theirs.
The first test suite¶
The library had no tests. It now has 130, running in ~10 s, and they are aimed at the failure mode this project actually has: physics that is plausibly wrong rather than broken.
test_models.py– Friedmann closure for every model (flat and curved),dEdzagainst a central finite difference (two independent hand transcriptions of the same algebra), every known limit, published signatures (PEDE’sw(0) = -1.145, DGP’sOmega_rcand growth suppression), and a bound on the error the distance integrator’s grid makes on LsCDM’s genuine discontinuity.test_datasets.py– every dataset loads, every likelihood’s covariance matches its data vector, and every chi2 per data point is O(1) at a concordance cosmology. The two deliberate exceptions are the tensions themselves:"h0"disagrees at ~5 sigma and"s8"at ~2.9 sigma, and the bounds are set just above those so a further error would still be caught rather than hidden.test_planck_lite.py– the binning and covariance algebra against published log-likelihoods, and the CAMB translation separately against physics, so a failure says which half is wrong.
Run them with pip install -e ".[dev]" && pytest.
v0.22.0¶
A figure-typography pass: everything a plot says is now written the way a paper would write it, and a few labels that were quietly wrong are fixed.
Parameter names reach every axis and corner-plot title as LaTeX
($\Omega_b$, $r_d$, $\sigma_8$) instead of as Python
identifiers – the labels were always declared on the parameter
container, the plots just weren’t reading them. Model names do the
same: legends show ΛCDM, wCDM, f(R) Hu-Sawicki via a new
Cosmology.MODEL_LABEL / plot_label() (with plain_name() for
tables, dropdowns and JSON, where raw LaTeX would be shown
literally), and define_model(..., label=...) lets a custom model
supply its own. Corner-plot titles now size their precision per
parameter, so Omega_b reads 0.0491 +0.0011 -0.0011 instead of
corner’s default 0.05 +0.00 -0.00.
Three labels were misleading rather than merely plain:
The Pantheon+ Hubble diagram’s y axis claimed to be
mu = m_B - M_B. It is neither: the plotted data is the corrected apparent magnitudem_b_corr(~11-27 mag, not a distance modulus’s ~33-46), andM_Bis analytically marginalized out by default, so it cannot appear in an axis label. Now$m_B$ [mag], with the model curve’s legend entry saying where its normalization came from (Model ($M_B$ marginalized)).The BAO panels were titled
D_V/r_swhile their y axis saidr_d. Both denote the sound horizon at the drag epoch; thersspelling comes from DESI’s own data file, whichMODEL_MAP’s keys keep, but the code divides byrdand now so do the titles.The Planck pull plot’s ticks were the raw dataset identifiers (
lA,omega_b_h2); they now render as$\ell_A$,$\omega_b h^2$.
Dataset combinations in legends also spell themselves out
(CC + DESI + Pantheon+, via stats.dataset_label) rather than
joining registry keys. No numerical result changes from any of this –
it is labels, titles and legends only.
The DES-SN5YR Hubble diagram is also legible for the first time. That
release ships 81 supernovae (of 1820) with mu_err between 5 and 468
mag – entries the survey de-weights rather than removes – and
matplotlib autoscales an errorbar plot to contain every whisker, so
the panel spanned +/-500 mag with the actual 35-45 mag Hubble diagram
compressed into a sliver of it. Panels are now scaled by the
measurements rather than by their largest error bars, and a bar may
only stretch the view if it is extreme both as a fraction of the
data’s spread and as a multiple of that dataset’s median error –
which caps nothing at all on CC, DESI, Pantheon+ or fsigma8, where
matplotlib’s own limits are reproduced exactly. The 81 points stay on
the plot, drawn without the whiskers that no longer fit and counted
in the legend.
One functional bug surfaced while testing the above and is fixed here
too: FitResult.save_json() (and the GUI’s JSON download) failed
for any fit that used run_mcmc(save=...), with
TypeError: Object of type int64 is not JSON serializable. Reading a
chain’s step count back through an HDF5 attribute yields np.int64
rather than int, and json rejects it (np.float64 slips through
only because it subclasses float). The counters are now cast at the
source, and both JSON writers coerce numpy scalars rather than
failing on a save.
v0.21.0¶
Adds the w0-wa dark-energy plane
(fitter.plots.w0_wa_plane(), and compare_w0_wa_plane() for
several posteriors at once): 2D credible contours over the
phantom/quintessence/quintom-A/quintom-B regions with ΛCDM marked at
(-1, 0) – the figure the DESI evolving-dark-energy results are
argued in. The classification behind it is available on its own as
cpl_diagnostics.classify_region() /
cpl_diagnostics.region_fractions(), which turns “the contours sit
in the quintom-B region” into a posterior probability. Contour levels
are stated as 2D credible probabilities (68%/95% of the samples), not
sigmas, since in two dimensions the familiar 1D contours enclose only
39%/86.5%.
The same release fixes a reporting bug in the GUI’s w0-wa
diagnostics: it printed the Mahalanobis distance D of the ΛCDM point
with a σ suffix. In 2D, D is not a number of sigma (D² follows χ² with
2 degrees of freedom), so this overstated the tension – D = 2.20
reads as “2.2σ” but is really 1.70σ. The library was corrected in
v0.19.0; the GUI now reports sigma (with the confidence level, and D
labelled as what it is) too.
v0.20.0¶
Makes MCMC chains persistent. Until now a chain
lived only in memory: closing the notebook, or adding one more plot
to the bottom of a script, meant sampling the whole posterior again
from step zero – hours, to recompute samples that hadn’t changed.
run_mcmc(save="chains/fit.h5") now writes the chain to HDF5 as it
is sampled (emcee’s own HDFBackend, plus a cosmofit metadata
group), and picks it back up on the next call instead of re-running
it. nsteps counts the total length to reach, so re-running an
unchanged script samples nothing, raising nsteps samples only the
difference, and an interrupted run keeps every step it had already
taken. Fitter.from_chain("chains/fit.h5") reopens a finished run in
a later session – model, datasets, free parameters, priors and fixed
values all come back out of the file – and
CosmoFit.stats.chains.open_chain() reads the posterior with no
dataset loaded at all.
Resuming is exact, not approximate: emcee’s proposal RNG state is
stored with the walkers, so a chain sampled in four sittings is
bit-identical to the same chain sampled in one (verified directly, as
is multi-core n_processes writing through the same file). The
metadata is also what keeps it safe – a resume whose model,
datasets, free parameters, prior bounds or fixed parameter values
differ from the stored chain’s is refused, naming exactly what
differs, rather than silently welding samples from two different
posteriors into one array. The GUI uses the same machinery
(fitter.chain_id(), a stable hash of the posterior, as the
filename), so adding a model to a comparison no longer re-runs the
models already fitted. h5py is now a dependency.
v0.19.0¶
Fixes a real scientific error in the Planck
distance-prior likelihood. Evaluated at Planck’s own best-fit
LCDM – where a correct implementation must return chi2 ~ 0 – the
old code returned chi2 ~ 100 for 3 data points, with l_A off by
-8.9 sigma (R and omega_b_h2 were fine at 0.13 and 0.07
sigma, which is what localized it to the sound horizon r_s(z*)).
The cause was not a bug in the physics but a definitional
mismatch, which is the classic trap with compressed likelihoods.
These priors are not a measurement of the sky; they are a summary of
Planck’s own fit, computed by Chen, Huang & Wang (2019) under a
specific set of conventions. CosmoFit was computing a more detailed
prediction than the compression assumed – radiation as photons plus
3.046 massless neutrinos (omega_r = 4.18e-5) where CHW19 define
Omega_r = Omega_m/(1+z_eq) (0.8% lower, massive neutrinos left in
Omega_m), and z* from the Hu & Sugiyama (1996) fitting formula
where CHW19 take it from the Planck chains, i.e. from CAMB. HS96 was
calibrated against 1990s recombination physics and runs 0.22% high
for Planck-like parameters (1091.9 vs CAMB’s 1089.9); l_A is
sensitive enough to z* that this alone is a ~4 sigma shift. Being
more physical than the data’s own definitions is still wrong when
the data is a compression.
RecombinationCalculator now follows CHW19 Eqs. (1)-(6) exactly, and
z* comes from a fit calibrated directly against CAMB 2.0.1 over
a 12x14 grid in (omega_b, omega_cb) covering far more than the
Planck posterior, accurate to 0.0018% in z* (~0.04 sigma of the
l_A prior). The radiation term is also renormalized properly:
adding Omega_r(1+z)^4 on top of a model’s E(z) left
E(0)^2 = 1 + Omega_r, over-closing the universe; the correction
reuses each model’s own dark-energy evolution, so it stays exact for
curved and non-LCDM models alike. The same fiducial check now gives
l_A +0.54 sigma, R -0.03 sigma, chi2 = 0.39.
Cross-checked two independent ways: against CAMB (z* to 0.002%),
and against a separate scipy.quad implementation of the CHW19
recipe across flat/open/closed LCDM and CPL (R and l_A to
<0.01 sigma). Per-evaluation cost is unchanged.
This changes results. The bias did not show up as a bad fit – the sampler absorbed it by shifting parameters, which is exactly what makes this kind of error dangerous. Refitting CC+DESI+Pantheon+ + Planck:
old |
new |
|
|---|---|---|
LCDM |
68.04 |
67.39 |
LCDM |
0.3118 |
0.3149 |
CPL |
-0.973 |
-0.881 |
CPL |
-0.000 |
-0.298 |
The CPL case is the headline: the old code put wa at essentially
zero – perfectly consistent with a cosmological constant – while
the corrected code prefers evolving dark energy, in the same
direction and of comparable size to DESI’s own published w0waCDM
result. Any CPL/JBP/BA conclusion drawn from a Planck-including fit
made with v0.18.0 or earlier should be regenerated.
Note: don’t combine two different
"s8"versions in the same fit (default is"kids1000"; passdataset_kwargs={"s8": {"version": "des_y3"}}for the other) – they’re independent survey constraints, not a joint one. See theS8Likelihooddocstring.
v0.17.0¶
Adds growth-of-structure: the “phase 2” v0.16.0
explicitly deferred, since a background-only treatment left
FRHuSawicki’s f_R0/n genuinely untestable (its background is
LCDM’s by construction). A new cosmology.calculators.growth.GrowthCalculator
solves the standard sub-horizon, quasi-static linear growth ODE
(D'' + (2+dlnH/dN)D' - (3/2)Omega_m(a) mu(a,k) D = 0) for every
model via a Cosmology.mu(a,k) hook (default 1, i.e. standard GR
growth – LCDM/wCDM/CPL/JBP/BA/GCG all get this for free), exposing
fitter.cosmology.background.{growth_rate,sigma8,fsigma8}(z), plus
a new sigma8 cosmological parameter and two new datasets/likelihoods:
"fsigma8" (the “Gold-2018” RSD growth-rate compilation, Sagredo,
Nesseris & Sapone 2018, arXiv:1806.10822, 22 points, with the same
Alcock-Paczynski correction the reference likelihood applies) and
"s8" (a single Gaussian S8 = sigma8*sqrt(Omega_m/0.3) constraint,
KiDS-1000 or DES Y3). Each of the three modified-gravity models now
has a real, cited mu(a,k): FQExponential uses the settled
sub-horizon result G_eff/G_N = 1/f_Q (Barros, Barreiro, Koivisto &
Nunes 2020, arXiv:2004.07867); FRTLinear uses
mu = 1 + 3*beta (the same coupling already in its own E(z)^2,
stated explicitly as a simplification rather than a full covariant
perturbation derivation – see the class docstring); and
FRHuSawicki gets the standard chameleon-screened, scale- and
time-dependent mu(a,k) (Pogosian & Silvestri 2008, arXiv:0709.0296),
derived here directly from this model’s own background rather than
transcribed, held at a fixed fiducial pivot k=0.1 h/Mpc since
CosmoFit’s fsigma8 data are single per-z points, not a P(k) shape –
f_R0/n are still inert for background-only fits, but now
genuinely shape "fsigma8"/"s8" predictions (verified end-to-end:
a short FRHuSawicki fit against "fsigma8" alone now gives f_R0 a
real, visibly informative posterior, not a flat one spanning the
whole prior). fitter.plots.growth()/compare_growth() add the
fsigma8(z) diagram alongside the existing figures, and the GUI picks
up both new datasets/the new plot automatically through the same
DATASET_REGISTRY/EXTRA_PARAMS mechanism every other dataset/model
already goes through.
v0.16.0¶
Adds modified-gravity models – FQExponential
(f(Q) gravity), FRTLinear (f(R,T) gravity), and FRHuSawicki
(f(R) gravity) – a real category beyond dark-energy-on-top-of-GR
reparametrizations (LCDM/wCDM/CPL/JBP/BA) or a unified fluid (GCG):
these modify the gravitational field equations themselves.
Background (E(z)) level only – CosmoFit’s datasets (CC, BAO,
SNe, Planck distance priors) are all background/expansion-history
probes, and growth-of-structure data (fσ8/RSD) plus the perturbation
machinery to use it isn’t implemented, so that’s the honest ceiling
of what’s testable here for now. Both FQExponential’s and FRTLinear’s
Friedmann equations were derived and verified directly against
primary sources (Anagnostopoulos, Basilakos & Saridakis 2021,
arXiv:2104.15123, for f(Q); Harko, Lobo, Nojiri & Odintsov 2011,
arXiv:1104.2669, for f(R,T)) rather than taken from a single
secondary source – one transcription (a sign error in FQExponential’s
Lambert-W closure formula) was caught this way, by a numerical
closure self-check (E(z=0) should equal 1 by construction; it
didn’t, until the sign was fixed) that’s now a permanent part of the
model’s test coverage. FRHuSawicki’s background is identical to
LCDM’s by construction – the standard “designer f(R)” approach
builds f(R) to reproduce an assumed target background, so f_R0/n
are present as parameters but don’t affect E(z) at all; fitting
them against these datasets won’t meaningfully constrain them. This
is stated explicitly in the class docstring and shown as a visible
warning next to the model picker in the GUI – included for
completeness rather than silently omitted, but not silently
overstated either. All three plug into the existing EXTRA_PARAMS
mechanism (built for define_model()/custom models), so Fitter,
every plot including compare_*, and the GUI’s multi-model
comparison all work with them with no further changes.
v0.15.0¶
Fixes n_processes on Python 3.14: it changed
multiprocessing’s default start method on Linux from fork to
forkserver (matching what macOS/Windows already used), which made
run_mcmc(n_processes=...) crash outright as a plain script
(forkserver/spawn require every process-spawning call to sit
behind an if __name__ == "__main__": guard) and, even inside a
Jupyter kernel where that particular crash doesn’t surface, measured
no speedup at all. Fitter._mcmc_pool now explicitly requests the
fork context rather than relying on whatever the interpreter’s
default happens to be – still available on Linux/macOS even where
it’s no longer automatic, and with neither of the above problems.
This version also brings the graphical interface up to parity with
cpl_mcmc_tfd42.ipynb: the GUI can now configure and fit multiple
models at once (built-in or custom) sharing one set of datasets, with
a statistical comparison tab (AIC/BIC, and a likelihood-ratio test
when exactly two models are properly nested), every compare_*
figure alongside the existing single-model ones, and the CPL-family
w(z)=-1-crossing/LCDM-distance posterior diagnostics – so everything
that notebook does is now also reachable with zero code. Every figure
also gets a download button (SVG/PNG/PDF, picked once per session and
applied to all of them) – the browser’s own save dialog is what lets
you choose where it goes.
Resolved (v0.18.0): the “no speedup inside Jupyter” limitation was
a misdiagnosis. Multiprocessing was never the problem, and it was
never specific to notebooks. The real cause was the per-evaluation
cost: every likelihood evaluation solved the Pantheon+ covariance
with a Cholesky triangular solve (cho_solve), and a triangular
solve is an inherently sequential recurrence – each element depends
on the one before it – so BLAS cannot thread it and worker processes
contend on memory bandwidth instead of scaling. The whole MCMC was
therefore pinned near one core’s throughput in every environment;
a plain script only looked better because that is where
n_processes was actually being passed.
The covariance is constant, so DenseCovariance now precomputes an
explicit inverse once (validated against the original matrix, and
falling back to the Cholesky path if it fails that check) and
solve() is a symmetric mat-vec, which BLAS does thread. Measured
on the bundled 1624x1624 Pantheon+ covariance:
per solve |
8-process scaling |
|
|---|---|---|
|
1.70 ms |
4.8x |
mat-vec, 1 BLAS thread |
0.80 ms |
7.5x |
mat-vec, threaded BLAS |
0.18 ms |
– |
End result for a 3-dataset CPL chain on an 8-core machine: the
single-process path went from 1.1x to 7.9x core utilization and
roughly halved in wall time, without any multiprocessing at all –
and it measures the same in a plain script, in Jupyter Lab, in VS
Code, and under nbconvert. chi2 is unchanged to ~1e-11 relative.
n_processes now also defaults to "auto", so notebooks get
multi-core behaviour without passing anything: it uses every core the
process is allowed to run on (os.sched_getaffinity, not
os.cpu_count() – the two differ inside a container, cgroup, or
SLURM allocation, and oversizing the pool there makes things slower),
but only when the run is long enough to earn back worker startup, and
it silently stays single-process for a define_model() model rather
than raising. The chain is unaffected: the proposal RNG lives in the
main process, so a given seed gives bit-identical results at any
n_processes (verified).
v0.14.0¶
Adds multi-core MCMC: fitter.run_mcmc(n_processes=...)
evaluates the ensemble’s walkers across multiple CPU cores. This
isn’t emcee’s own naive recipe (pairing a raw multiprocessing.Pool
with Fitter’s already-built, data-carrying log-posterior) – for a
dataset like Pantheon+ (a ~1600x1600 dense covariance matrix),
re-pickling and sending that to a worker on every single step
measured slower than one process (~19ms to pickle vs. ~2ms to
evaluate). Instead, each worker process builds its own Fitter once
(from a small, cheap-to-pickle recipe), via the pool’s initializer,
and only a length-ndim float vector crosses the process boundary
per evaluation after that; workers also pin their own BLAS thread
pool to 1 (via the new threadpoolctl dependency) to avoid
oversubscribing the machine. Net effect, measured on an 8-core
machine for a 4-dataset CPL fit: ~2.4x, well under n_processesx
(per-step IPC has its own cost) but a real, significant speedup. Only
works for models picklable by reference (every built-in model; not a
dynamically-built define_model()/model_from_expression() model,
which raises a clear error rather than an obscure pickling failure
if n_processes is given), and is most reliable on Linux/macOS
(multiprocessing from a Windows notebook is fragile for reasons
outside this library’s control). The new examples/05-case-studies/cpl_mcmc_tfd42.ipynb
notebook is a publication-scale variant of cpl_mcmc_analysis.ipynb
using this: all four datasets (including Planck, which makes rd
and Omega_b constrainable and lets them join the free parameters
too) and a much longer chain (nwalkers=64, nsteps=12000), run in
parallel across every available core.
v0.13.0¶
Adds model comparison plots: every
fitter.plots.* figure (hz, deceleration, w_of_z,
hubble_diagram, des_hubble_diagram, bao_distances,
sdss_bao_distances) gets a compare_* counterpart that overlays
this fit’s curve with one or more other models’ curves on the same
data/axes – the “model A vs model B” figures cosmology papers use,
rather than only being able to look at models one at a time or
compare them statistically (AIC/BIC/LRT) without a picture of what
the difference actually looks like. other_fits=None (the default)
auto-compares against a quick best-fit-only LCDM reference built
from the same datasets; passing an already-fit Fitter, or a list
of them, compares against exactly those instead, for an arbitrary
N-model figure. Every compare_* method reuses the corresponding
single-model method’s existing evaluation logic (posterior-predictive
bands, per-fit analytic SN offsets, …) rather than duplicating it,
and works for any model – built-in or
custom – since it only depends on
Fitter/Cosmology’s existing generic interface. The
cpl_mcmc_analysis notebook’s CPL vs. LCDM section now includes
these alongside its existing AIC/BIC/likelihood-ratio comparison.
v0.12.0¶
Adds a graphical interface: a
Streamlit app (app/streamlit_app.py,
optional pip install -e ".[gui]") for ticking datasets, picking a
built-in model or writing a custom E(z) as an expression, editing
free parameters/bounds in a table, and running the fit + rendering
plots with one click – a pure UI layer over the existing
Fitter/FitPlotter API, with no changes to either. The one new
library addition is model_from_expression()
(cosmology.custom), a thin wrapper around define_model() that
takes E/w/dEdz as expression strings (e.g.
"sqrt(Omega_m*(1+z)**3 + ...)") instead of Python callables –
evaluated with builtins stripped and only whitelisted numpy math
plus the model’s own parameters reachable, appropriate for a
locally-run tool.
v0.11.0¶
Adds support for custom, not-in-the-literature
models: define_model() (cosmology.custom) builds a usable
Cosmology subclass from a single E(z) function – which alone is
enough to fit against every built-in dataset/likelihood and produce
every plot except w_of_z()/deceleration() (an optional dEdz=
enables the latter, else a numerical finite-difference fallback is
used) – plus any new parameters the model needs beyond the standard
set (H0, Omega_m, Omega_k, w0, wa, …), each declared
inline with a default and prior bounds. The same mechanism is also
available by subclassing Cosmology directly and declaring an
EXTRA_PARAMS class attribute (Cosmology.__init_subclass__ builds
a matching parameter dataclass and property automatically); this is
what define_model does under the hood. Required generalizing
Fitter to read the parameter container off the model
(model.PARAMS_CLASS) instead of hardcoding
CosmologyParameters – every built-in model is unaffected (none
declare EXTRA_PARAMS), verified against all six.
v0.10.0¶
Splits MCMC sampling out of Fitter and into a
dedicated stats.sampler module: EnsembleSampler (the existing
emcee-backed walker initialization/run logic, unchanged in
behavior) now implements a small BaseSampler interface, so the
sampling backend is a swappable component rather than logic
inlined in Fitter.run_mcmc, and run_mcmc() gains a moves=
argument to pass through custom emcee proposals (e.g.
emcee.moves.DEMove() for strongly correlated posteriors). It also
adds a consolidated result interface (stats.results): fitter.result
returns a FitResult bundling the best-fit point (BestFitResult)
and MCMC posterior summary/convergence (MCMCResult) that were
previously only available piecemeal via best_fit_params,
best_fit_chi2, summary(), convergence(), with a single
readable repr and FitResult.save_json() / .load_json() for
keeping a fit’s headline numbers without pickling the emcee
sampler. Both are purely additive – every existing Fitter
method/attribute (run_mcmc(), best_fit(), summary(),
convergence(), samples_dict(), .sampler, .best_fit_result,
…) is unchanged.
v0.9.0¶
A performance pass, driven by profiling rather
than guesswork. The big one: evaluating PlanckLikelihood was
dominated by two integrals (the sound horizon and the comoving
distance to z*) computed with scipy.integrate.quad – an adaptive
scalar quadrature that calls the (already fully vectorized)
integrand at one z at a time, several hundred times per call. Both
are now evaluated on a fixed, vectorized grid (scipy.integrate.simpson)
instead – one array-valued call instead of hundreds of scalar ones.
Getting this right took two tries: the first grid (linear in the
substituted variable) looked fine across randomized-parameter testing
but turned out to badly under-resolve the sound-horizon integral’s
approach to its asymptote at realistic (Planck-fiducial-like)
parameter values specifically – caught by comparing end-to-end
PlanckLikelihood output at a fixed point against the original
quad-based result, not just spot-checking the integral in
isolation. A log-spaced grid fixes it, verified to <1e-6 relative
error against quad across dozens of randomized and
literature-realistic parameter sets, across all 6 models. Net
effect: ~8x faster evaluation of a joint CC+DESI+Pantheon+Planck
likelihood. The distance-integrator’s interpolation grid
(cosmology.numerics.integrals) was also shrunk several-fold
(verified: still >100x more accurate than any dataset’s measurement
precision needs) for a smaller additional gain that applies to every
fit, Planck or not.
v0.8.0¶
Adds the SDSS BAO likelihood: BOSS DR12
(z=0.38, 0.51) + eBOSS DR16 LRG (z=0.698) + eBOSS DR16 QSO (z=1.48),
combined into one dataset with a block-diagonal covariance (each
component is an independent, non-overlapping-redshift measurement).
Data downloaded directly from
CobayaSampler/bao_data
(the same repository that is, independently, also the exact source of
this project’s existing DESI 2024 files – confirmed byte-for-byte)
and cross-checked against the published eBOSS DR16 LRG/QSO
uncertainties. BOSS DR12’s usual third bin (z=0.61) is deliberately
omitted, since it overlaps the eBOSS DR16 LRG redshift range. The
DESI/SDSS-BAO model()/residuals()/chi2() logic (previously
duplicated in DESILikelihood) was factored into a shared
BAODistanceLikelihood base class.
Note: don’t combine
"desi"and"sdss_bao"in the same fit – DESI targets much of the same sky BOSS/eBOSS did, so treating them as independent double-counts structure. See theSDSSBAOLikelihooddocstring.
v0.7.0¶
Adds the DES-SN5YR (Dark Energy Survey 5-year)
supernova likelihood, downloaded directly from the official
des-science/DES-SN5YR
data release and cross-validated against its own reference likelihood
implementation (the distance-modulus formula and analytic
marginalization both match it exactly). Its covariance is shipped as a
precision (inverse covariance) matrix rather than a covariance matrix,
so this version also adds PrecisionCovariance, used directly (no
inversion round-trip) instead of forcing it through the existing
Cholesky-based DenseCovariance. The Pantheon+ and DES-SN5YR
marginalization logic (previously duplicated) was factored into a
shared AnalyticOffsetMixin.
Note: don’t combine
"pantheon"and"des_sn5yr"in the same fit – DES-SN5YR’s low-z anchor sample (~11% of it) is also compiled into Pantheon+, so fitting both double-counts those supernovae. See theDESSN5YRLikelihooddocstring.
v0.6.0¶
Adds three dark-energy models: JBP and BA
(alternative w0-wa parametrizations to CPL, reusing the same w0/wa
parameters) and GCG (Generalized Chaplygin Gas, a genuinely
different unified dark-matter/dark-energy fluid, adding two new shared
parameters A_s/alpha). All three have closed-form E(z)/dE/dz
(no per-step numerical integration), verified against finite-difference
derivatives and independent numerical integration of the continuity
equation, so they’re exactly as fast in an MCMC as LCDM/wCDM/CPL.
v0.5.0¶
Moves the package to a standard src layout
(src/CosmoFit/...) with a unified public API, so from CosmoFit import CPL, Fitter, ... now works instead of importing the five
subpackages (cosmology, data, likelihoods, stats, plots)
separately from the repository root.
v0.4.0¶
Fixes a Pantheon+ distance-modulus bug (the
zHD/zHEL redshift distinction from Brout et al. 2022 was not
applied) and a circular import between data.loader and
likelihoods, roughly halves the per-step cost of an MCMC run
that includes Pantheon+, adds the wCDM model, adds an
autocorrelation-time MCMC convergence check
(Fitter.convergence()), and moves all plotting into a dedicated
plots.FitPlotter (fitter.plots), with new Hubble diagram, H(z),
BAO distance, Planck pull, w(z) evolution and deceleration-parameter
figures alongside the existing chain/corner plots.
v0.3.0¶
Fixes a curvature bug in the LCDM/CPL Friedmann equations and in the transverse comoving distance (Omega_k was previously a fittable-but-inert parameter), and adds a Planck 2018 CMB distance-prior likelihood (shift parameter R, acoustic scale l_A, omega_b_h2), backed by a radiation-aware sound-horizon calculation and the Hu & Sugiyama (1996) z_star fitting formula.