Source code for cosmofit.core.legacy

"""
The existing models and datasets, as components of the new core.

Temporary by design. :class:`LegacyCosmology` wraps a
:class:`~cosmology.core.base.Cosmology` subclass as a theory;
:class:`LegacyLikelihood` wraps any dataset in
:data:`~stats.fitter.DATASET_REGISTRY` as a likelihood. Together they
let the new core run every model and dataset the library has *today*,
through exactly the code ``Fitter`` runs -- so the two can be compared
point by point, and every later port (a model rewritten as a native
theory, a dataset as a native likelihood) has a reference it must
reproduce.

In an input, a dataset name is enough to reach these::

    theory:
      legacy_cosmology: {model: CPL, compute_rd: true}
    likelihood:
      desi: {version: desi2025}
      pantheon:

Every parameter of the model's parameter class -- cosmological and
nuisance alike (``MB``, ``A_planck``), since the old likelihoods read
those off the cosmology -- is an input of the theory; any not declared
in ``params`` keeps the class default, as with ``Fitter``.
"""

from __future__ import annotations

from .component import ComponentError, Likelihood, Theory


__all__ = ["LegacyCosmology", "LegacyLikelihood"]


def _resolve_model(model):
    """A ``Cosmology`` subclass from a class, a name, or an import path."""

    from cosmofit.cosmology.core.base import Cosmology

    if isinstance(model, type) and issubclass(model, Cosmology):
        return model

    if not isinstance(model, str):
        raise ComponentError(f"Not a cosmological model: {model!r}.")

    if ":" in model or "." in model:

        from .registry import _import

        cls = _import(model)

    else:

        import cosmofit

        cls = getattr(cosmofit, model, None)

    if not (isinstance(cls, type) and issubclass(cls, Cosmology)):
        raise ComponentError(
            f"Unknown model {model!r}: give a built-in name (LCDM, CPL, "
            f"...) or an import path to a Cosmology subclass."
        )

    return cls


[docs] class LegacyCosmology(Theory): """ A ``Cosmology`` subclass as a theory. Options ------- model : str or type ``"LCDM"``, ``"CPL"``, ..., an import path, or the class. compute_rd : bool Compute ``rd`` from the densities, as ``Fitter(compute_rd=True)``. ``rd`` is then not an input. derive_sigma8 : bool Take ``sigma8`` from the Boltzmann code, as ``Fitter(derive_sigma8=True)``. ``sigma8`` is then not an input. Provides -------- ``cosmology`` (the live object the wrapped likelihoods read), ``E``, ``H``, ``DM``, ``DH``, ``DV``, ``DA`` (each ``z=...``), ``rd``, ``fsigma8`` (``z=...``). Derived parameters: ``rd_computed`` and ``z_drag``. """ #: One mutable cosmology object, so one state. cache_size = 1
[docs] def initialize(self) -> None: self.model_cls = _resolve_model(self.info.get("model", "LCDM")) self.compute_rd = bool(self.info.get("compute_rd", False)) self.derive_sigma8 = bool(self.info.get("derive_sigma8", False)) unknown = set(self.info) - {"model", "compute_rd", "derive_sigma8"} if unknown: raise ComponentError( f"{self.name}: unknown option(s) {sorted(unknown)}." ) params_cls = self.model_cls.PARAMS_CLASS values = dict(params_cls.defaults()) # H0 and Omega_m have no defaults; any value builds the object, # and the first point overwrites them. values.setdefault("H0", 70.0) values.setdefault("Omega_m", 0.3) self.param_names = list(params_cls.names()) self.cosmology = self.model_cls( params_cls(**{n: values[n] for n in self.param_names}) ) self.cosmology.compute_rd = self.compute_rd
# ---------------------------------------------------------
[docs] def accepts(self, name: str) -> bool: if self.compute_rd and name == "rd": return False if self.derive_sigma8 and name == "sigma8": return False return name in self.param_names
[docs] def get_can_provide(self) -> list[str]: return ["cosmology", "E", "H", "DM", "DH", "DV", "DA", "rd", "fsigma8"]
[docs] def get_derived_params(self) -> list[str]: return ["rd_computed", "z_drag"]
[docs] def initialize_complete(self) -> None: if not self.derive_sigma8: return from cosmofit.cosmology.boltzmann import CAMBBackend if CAMBBackend.attached(self.cosmology) is None: raise ComponentError( f"{self.name}: derive_sigma8 needs a likelihood that runs " f"CAMB (planck_lite, planck_lensing, planck_lowe, " f"act_lensing) -- there is nothing else to derive it from." ) # Only now: the backend is created by the CMB likelihood. self.cosmology.derive_sigma8 = True
# ---------------------------------------------------------
[docs] def calculate(self, state: dict, want_derived: bool = True, **params): self.cosmology.params.update(**params) self.cosmology.refresh() if want_derived: horizon = self.cosmology.sound_horizon state["derived"] = { "rd_computed": float(horizon.rd_computed()), "z_drag": float(horizon.z_drag()), } return True
# --------------------------------------------------------- def get_cosmology(self): return self.cosmology def get_E(self, z): return self.cosmology.E(z) def get_H(self, z): return self.cosmology.H(z) def get_DM(self, z): return self.cosmology.distance.DM(z) def get_DH(self, z): return self.cosmology.distance.DH(z) def get_DV(self, z): return self.cosmology.distance.DV(z) def get_DA(self, z): return self.cosmology.distance.DA(z) def get_rd(self): return float(self.cosmology.rd) def get_fsigma8(self, z): return self.cosmology.background.fsigma8(z)
[docs] class LegacyLikelihood(Likelihood): """ A dataset of :data:`~stats.fitter.DATASET_REGISTRY` as a likelihood: ``ln L = -chi2 / 2`` of the wrapped class. Options ------- dataset : str, optional Registry key; defaults to the name the likelihood is listed under, so ``desi:`` in an input is enough. Any other option is passed to the wrapped class's constructor (``version``, ``marginalize_MB``, ``include_cepheid``, ...). """
[docs] def initialize(self) -> None: from cosmofit.stats.fitter import DATASET_REGISTRY self.options = dict(self.info) self.dataset = self.options.pop("dataset", self.name) if self.dataset not in DATASET_REGISTRY: raise ComponentError( f"Unknown dataset {self.dataset!r}; known: " f"{sorted(DATASET_REGISTRY)}." ) self.legacy_cls = DATASET_REGISTRY[self.dataset] self.legacy = None
[docs] def get_requirements(self) -> dict: return {"cosmology": None}
[docs] def initialize_with_provider(self, provider) -> None: super().initialize_with_provider(provider) self.legacy = self.legacy_cls(provider.get_cosmology(), **self.options)
[docs] def logp(self, **params) -> float: return -0.5 * float(self.legacy.chi2())
# ---------------------------------------------------------
[docs] def check_model(self, model) -> None: """ The dataset-combination warnings ``Fitter`` gives, once per model (from whichever wrapped likelihood is listed first). """ wrapped = [ lk for lk in model.likelihoods.values() if isinstance(lk, LegacyLikelihood) ] if wrapped[0] is not self: return from cosmofit.stats import fitter as old names = [lk.dataset for lk in wrapped] sampled = model.sampled_params cosmologies = [ t for t in model.theories.values() if isinstance(t, LegacyCosmology) ] compute_rd = any(t.compute_rd for t in cosmologies) old._warn_conflicting_datasets(names) old._warn_inconsistent_amplitude(names, sampled) old._warn_blind_neutrino_mass(names, sampled, compute_rd) old._warn_blind_neff(names, sampled) old._warn_free_rd_with_early_universe(names, sampled, compute_rd) old._warn_calibrated_twice([lk.legacy for lk in wrapped]) old._warn_tau_counted_twice([lk.legacy for lk in wrapped]) for theory in cosmologies: old._warn_derived_parameters(theory.model_cls, sampled) old._warn_ungrounded_coupling(theory.model_cls, names)