"""Transfer-function models: the :class:`TransferModel` kind and its models.
A transfer model turns a cosmology into the transfer function T(k) at z = 0 of every
matter species, normalised to T -> 1 as k -> 0. It has no cosmology of its own: its
:meth:`~TransferModel.solve` takes the cosmology and the wavenumber accuracy, and
returns a :class:`TransferSolution`, which evaluates ln T(k) on plain arrays (k in
h/Mpc). The :class:`~hmf.core.transfer.Transfer` stage wraps that in a unit-checked
public API.
The matter species are ``"cb"`` (CDM + baryons) and ``"tot"`` (total matter,
including massive neutrinos). The Boltzmann codes (:class:`CAMB`, :class:`CLASS`)
compute both in one run. The fitting formulae have no massive neutrinos, so they give
the same transfer function for both.
Models computed by a Boltzmann code, or given as a table (:class:`FromArray`,
:class:`FromFile`), are interpolated and extrapolated by
:class:`~hmf.core._kernels.transfer.TabulatedTransfer`: smoothly in value and slope
beyond the table's largest wavenumber, following the shape of the EH98 fit.
CAMB and classy are only imported when a model that needs them is solved, so this
module imports without them.
"""
from __future__ import annotations
import abc
import functools
import math
from collections.abc import Callable, Mapping
from pathlib import Path
from types import MappingProxyType
from typing import Any, ClassVar, Literal
import astropy.units as u
import attrs
import numpy as np
import numpy.typing as npt
from astropy.cosmology import FLRW
from . import _references as refs
from ._arrays import dimensionless_floats
from ._boltzmann import (
BoltzmannRun,
camb_cosmology_input,
check_camb_settings,
class_cosmology_input,
get_run,
)
from ._cosmology_models import _BoltzmannBacked, _CosmologyModel
from ._fields import field
from ._kernels import transfer as kt
from ._validators import check_finite_positive, check_increasing, check_table, positive
from .accuracy import KAccuracy
from .cache import DiskCache
from .domain import Domain
from .model import Model
from .species import CAMB_COLUMNS, MATTER_SPECIES, check_species
from .units import h_Mpc, quantity_field, unit_boundary
__all__ = [
"BBKS",
"CAMB",
"CLASS",
"EH",
"EH_BAO",
"BondEfs",
"EH_NoBAO",
"FromArray",
"FromFile",
"TransferModel",
"TransferSolution",
]
Array = npt.NDArray[np.float64]
#: The wavenumber, in 1/Mpc (no h), at which the Boltzmann codes evaluate the growth
#: factor of each species (see :mod:`hmf.core.growth_models`).
BOLTZMANN_GROWTH_K_REF = 0.01
[docs]
@attrs.frozen(eq=False)
class TransferSolution:
"""The transfer function of every matter species, from one evaluation of a model.
This is the kernel-level result of :meth:`TransferModel.solve`: its methods take
and return plain arrays, with k in h/Mpc. They are pure.
"""
#: ln T(k) of each species, as a function of k in h/Mpc.
ln_t_functions: Mapping[str, Callable[[Array], Array]]
#: The Boltzmann run behind it, if any.
run: BoltzmannRun | None = None
#: The largest wavenumber (h/Mpc) of the table, above which T(k) is extrapolated;
#: ``None`` for a fitting formula.
k_max_table: float | None = None
#: The smallest wavenumber (h/Mpc) of the table, below which T(k) is extrapolated;
#: ``None`` for a fitting formula.
k_min_table: float | None = None
@property
def species(self) -> tuple[str, ...]:
"""The species this solution has."""
return tuple(self.ln_t_functions)
[docs]
def ln_transfer(self, k: Array, species: str = "cb") -> Array:
"""Ln T(k) of ``species``, with k in h/Mpc."""
return self.ln_t_functions[check_species(species)](np.asarray(k, dtype=float))
[docs]
def transfer(self, k: Array, species: str = "cb") -> Array:
"""T(k) of ``species``, with k in h/Mpc."""
out: Array = np.exp(self.ln_transfer(k, species))
return out
def _all_species(fn: Callable[[Array], Array]) -> Mapping[str, Callable[[Array], Array]]:
"""The same function for every species (for models without massive neutrinos)."""
return MappingProxyType(dict.fromkeys(MATTER_SPECIES, fn))
def _positive_quantity(instance: Any, attribute: attrs.Attribute[Any], value: u.Quantity) -> None:
"""Validate that a scalar Quantity field is finite and > 0 (in its stored unit)."""
check_finite_positive(attribute.name, value.value, where=type(instance).__name__)
def _pairs(
value: Mapping[str, Any] | tuple[tuple[str, Any], ...] | None,
) -> tuple[tuple[str, Any], ...]:
"""Convert a mapping of settings to a sorted tuple of pairs (hashable)."""
if value is None:
return ()
items = value.items() if isinstance(value, Mapping) else value
out = tuple(sorted((str(k), v) for k, v in items))
for k, v in out:
if not isinstance(v, (bool, int, float, str)):
raise TypeError(
f"Setting {k!r} must be a bool, int, float or str, not {type(v).__name__}."
)
return out
[docs]
@attrs.frozen(kw_only=True)
class TransferModel(_CosmologyModel, Model, kind=True):
"""The kind of transfer-function models.
Subclasses implement :meth:`solve`, and may check that they apply to a cosmology
in :meth:`check_cosmology`.
"""
#: Where the model can be evaluated: any k > 0. The
#: :class:`~hmf.core.transfer.Transfer` stage checks k against the model's class's
#: domain, so a model can narrow it.
valid_domain: ClassVar[Domain] = Domain(
{"k": (0 * h_Mpc, None, "(]")}, source="T(k) is defined for k > 0."
)
#: Whether the model's table was supplied by the user, so that extrapolating it
#: (beyond :attr:`TransferSolution.k_min_table` and ``k_max_table``) warns. The
#: Boltzmann codes' tables are extrapolated by design, without a warning.
_user_table: ClassVar[bool] = False
[docs]
@abc.abstractmethod
def solve(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> TransferSolution:
"""Compute the transfer function of every species for a cosmology.
Parameters
----------
cosmology
The cosmology.
k_accuracy
The wavenumber accuracy; it sets the sampling of the Boltzmann codes.
disk_cache
Where to cache Boltzmann-code output on disk, if anywhere.
Returns
-------
TransferSolution
"""
# ---------------------------------------------------------------------------------
# Fitting formulae
# ---------------------------------------------------------------------------------
def _eh98_scales(cosmology: FLRW) -> kt.EH98Scales:
return kt.eh98_scales(
h=cosmology.h,
omega_m=cosmology.Om0,
omega_b=cosmology.Ob0 or 0.0,
t_cmb=cosmology.Tcmb0.to_value(u.K) or 2.7255,
)
[docs]
@attrs.frozen(kw_only=True)
class EH_BAO(TransferModel, alias="EH_BAO"):
"""The Eisenstein & Hu (1998) fit, with baryon acoustic oscillations.
EH98 Eqs. 16-24, for the CDM + baryon field. The cosmology must have baryons.
A cosmology without a CMB temperature (``Tcmb0 = 0``) is taken to have 2.7255 K.
"""
references: ClassVar[tuple[str, ...]] = (refs.EH98,)
parameter_source: ClassVar[str] = "Eisenstein & Hu 1998, ApJ 496, 605, Eqs. 2-24"
[docs]
def check_cosmology(self, cosmology: FLRW) -> None:
"""Raise unless the cosmology has baryons."""
if not cosmology.Ob0:
raise ValueError(f"{type(self).__name__} needs a cosmology with baryons (Ob0 > 0).")
[docs]
def solve(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> TransferSolution:
"""Compute the transfer function (see :meth:`TransferModel.solve`)."""
self.check_cosmology(cosmology)
return TransferSolution(
_all_species(functools.partial(kt.ln_t_eh98, scales=_eh98_scales(cosmology)))
)
[docs]
@attrs.frozen(kw_only=True)
class EH(EH_BAO, alias="EH"):
"""The Eisenstein & Hu (1998) fit with BAO: another name for :class:`EH_BAO`."""
[docs]
@attrs.frozen(kw_only=True)
class EH_NoBAO(TransferModel, alias="EH_NoBAO"):
"""The Eisenstein & Hu (1998) "no-wiggle" fit, without BAO.
EH98 Eqs. 26-31. The shape parameter q is that of their Eq. 28 (v3 used
``k / (13.4 k_eq)``, which differs from it by 4e-4).
"""
references: ClassVar[tuple[str, ...]] = (refs.EH98,)
parameter_source: ClassVar[str] = "Eisenstein & Hu 1998, ApJ 496, 605, Eqs. 26-31"
[docs]
def solve(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> TransferSolution:
"""Compute the transfer function (see :meth:`TransferModel.solve`)."""
return TransferSolution(
_all_species(functools.partial(kt.ln_t_eh98_no_wiggle, scales=_eh98_scales(cosmology)))
)
[docs]
@attrs.frozen(kw_only=True)
class BBKS(TransferModel, alias="BBKS"):
r"""The Bardeen et al. (1986) fit, with a baryon correction to the shape parameter.
.. math:: T(k) = \frac{\ln(1+aq)}{aq}
\left(1 + bq + (cq)^2 + (dq)^3 + (eq)^4\right)^{-1/4},
\qquad q = k / \Gamma,
with k in h/Mpc and :math:`\Gamma = \Omega_{m,0} h` (BBKS Eq. G3). The baryon
correction multiplies :math:`\Gamma` by
* ``"sugiyama95"`` (the default): :math:`\exp(-\Omega_b(1 + \sqrt{2h}/\Omega_m))`,
the published form of Sugiyama (1995), as quoted by Meiksin, White & Peacock
(1999, Eq. 4) and Liddle & Lyth (2000, Eq. 5.14);
* ``"sugiyama95_preprint"``: :math:`\exp(-\Omega_b(1 + 1/\Omega_m))`, the form of
the preprint (astro-ph/9412025, Eq. 3.9);
* ``"none"``: 1.
The two Sugiyama forms agree at h = 0.5. (In hmf v3 these were the
``use_liddle_baryons`` and ``use_sugiyama_baryons`` flags.)
"""
references: ClassVar[tuple[str, ...]] = (refs.BBKS86, refs.SUGIYAMA95)
parameter_source: ClassVar[str] = "Bardeen et al. 1986, ApJ 304, 15, Eq. G3"
a: float = field(default=2.34, converter=float, doc="Coefficient a of Eq. G3.")
b: float = field(default=3.89, converter=float, doc="Coefficient b of Eq. G3.")
c: float = field(default=16.1, converter=float, doc="Coefficient c of Eq. G3.")
d: float = field(default=5.46, converter=float, doc="Coefficient d of Eq. G3.")
e: float = field(default=6.71, converter=float, doc="Coefficient e of Eq. G3.")
baryons: Literal["sugiyama95", "sugiyama95_preprint", "none"] = field(
default="sugiyama95",
validator=attrs.validators.in_(("sugiyama95", "sugiyama95_preprint", "none")),
doc="The baryon correction to the shape parameter (see above).",
)
[docs]
@unit_boundary(returns=h_Mpc)
def shape_parameter(self, cosmology: FLRW) -> Any:
r"""The shape parameter Gamma, including the baryon correction.
Parameters
----------
cosmology
The cosmology.
Returns
-------
astropy.units.Quantity
:math:`\Gamma`, a scalar Quantity in h/Mpc.
"""
return self._shape_parameter(cosmology)
def _shape_parameter(self, cosmology: FLRW) -> float:
""":meth:`shape_parameter` as a float in h/Mpc, for :meth:`solve`."""
om, ob, h = cosmology.Om0, cosmology.Ob0 or 0.0, cosmology.h
gamma = om * h
if self.baryons == "sugiyama95":
gamma *= math.exp(-ob * (1 + math.sqrt(2 * h) / om))
elif self.baryons == "sugiyama95_preprint":
gamma *= math.exp(-ob * (1 + 1 / om))
return float(gamma)
[docs]
def solve(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> TransferSolution:
"""Compute the transfer function (see :meth:`TransferModel.solve`)."""
fn = functools.partial(
kt.ln_t_bbks,
gamma=self._shape_parameter(cosmology),
a=self.a,
b=self.b,
c=self.c,
d=self.d,
e=self.e,
)
return TransferSolution(_all_species(fn))
[docs]
@attrs.frozen(kw_only=True)
class BondEfs(TransferModel, alias="BondEfs"):
r"""The Bond & Efstathiou (1984) fit, in the shape-parameter form of EBW92.
.. math:: T(k) = \left[1 + \left(aq + (bq)^{3/2} + (cq)^2\right)^\nu\right]^{-1/\nu},
\qquad q = k/\Gamma, \qquad \Gamma = \Omega_{m,0} h,
with k in h/Mpc (Efstathiou, Bond & White 1992, Eq. 7). The defaults are EBW92's,
which the GIF and Virgo simulations used. The form has no baryon suppression; use
:class:`EH_NoBAO` for that.
"""
references: ClassVar[tuple[str, ...]] = (refs.BE84, refs.EBW92)
parameter_source: ClassVar[str] = "Efstathiou, Bond & White 1992, MNRAS 258, 1P, Eq. 7"
a: float = field(default=6.4, converter=float, doc="Coefficient a, in Mpc/h at Gamma = 1.")
b: float = field(default=3.0, converter=float, doc="Coefficient b, in Mpc/h at Gamma = 1.")
c: float = field(default=1.7, converter=float, doc="Coefficient c, in Mpc/h at Gamma = 1.")
nu: float = field(default=1.13, converter=float, doc="The exponent nu.")
[docs]
def solve(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> TransferSolution:
"""Compute the transfer function (see :meth:`TransferModel.solve`)."""
fn = functools.partial(
kt.ln_t_bond_efs,
gamma=float(cosmology.Om0 * cosmology.h),
a=self.a,
b=self.b,
c=self.c,
nu=self.nu,
)
return TransferSolution(_all_species(fn))
# ---------------------------------------------------------------------------------
# Tabulated models
# ---------------------------------------------------------------------------------
#: The least number of nodes of a transfer table (see
#: :func:`~hmf.core._kernels.transfer.tabulate_transfer`).
_MIN_TABLE_SIZE = 4
@attrs.frozen(kw_only=True)
class _Tabulated(TransferModel, abstract=True):
"""A transfer model given by a table, interpolated by TabulatedTransfer."""
tail_decay_ln_k: float = field(
default=1.0,
converter=float,
validator=positive,
doc=(
"Above the table's largest wavenumber, T(k) follows the EH98 no-wiggle shape, "
"rescaled to match the table's value and logarithmic slope at the join. The "
"tilt correction decays over this many e-folds in k."
),
)
@abc.abstractmethod
def _table(
self, cosmology: FLRW, k_accuracy: KAccuracy, disk_cache: DiskCache | None
) -> tuple[Array, Mapping[str, Array], BoltzmannRun | None]:
"""The table: k (h/Mpc), T of each species, and the run it came from."""
def solve(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> TransferSolution:
"""Compute the transfer function (see :meth:`TransferModel.solve`)."""
k, transfers, run = self._table(cosmology, k_accuracy, disk_cache)
scales = _eh98_scales(cosmology)
tables = {}
for species, t in transfers.items():
tables[species] = kt.tabulate_transfer(
np.asarray(k, dtype=float),
np.abs(np.asarray(t, dtype=float)),
scales,
decay_ln_k=self.tail_decay_ln_k,
)
return TransferSolution(
MappingProxyType({s: tab.ln_t for s, tab in tables.items()}),
run=run,
k_max_table=min(tab.k_max for tab in tables.values()),
k_min_table=max(tab.k_min for tab in tables.values()),
)
[docs]
@attrs.frozen(kw_only=True)
class FromArray(_Tabulated, alias="FromArray"):
"""A transfer function given as arrays.
``t`` is the transfer function of the CDM + baryon field; ``t_tot``, if given,
that of total matter (else the same as ``t``). Neither needs to be normalised.
The table has at least 4 nodes, at finite, positive, strictly increasing k, and
finite, positive T; it is checked when the model is built (a :class:`ValueError`
otherwise), and used as given, every node included. Beyond the table, T(k) is
extrapolated as for every table (see
:class:`~hmf.core._kernels.transfer.TabulatedTransfer`), and the
:class:`~hmf.core.transfer.Transfer` stage emits an
:class:`~hmf.exceptions.HMFExtrapolationWarning` (once per stage instance).
"""
_user_table: ClassVar[bool] = True
k: u.Quantity = quantity_field(
h_Mpc,
ndim=1,
doc=(
"Wavenumbers (at least 4), positive and strictly increasing: a Quantity in "
"h-units (e.g. k * hmf.core.units.h_Mpc), stored in h/Mpc."
),
)
t: tuple[float, ...] = field(
converter=dimensionless_floats, doc="The CDM + baryon transfer function at k."
)
t_tot: tuple[float, ...] | None = field(
default=None,
converter=attrs.converters.optional(dimensionless_floats),
doc="The total-matter transfer function at k; None means the same as t.",
)
@functools.cached_property
def _k(self) -> Array:
"""The wavenumbers, as plain floats in h/Mpc."""
return np.asarray(self.k.value)
@k.validator
def _check_k(self, attribute: attrs.Attribute[u.Quantity], value: u.Quantity) -> None:
(k,) = check_table({"k": value.value}, where="FromArray", min_size=_MIN_TABLE_SIZE)
check_finite_positive("k", k, where="FromArray")
check_increasing("k", k, where="FromArray")
@t.validator
def _check_t(
self, attribute: attrs.Attribute[tuple[float, ...]], value: tuple[float, ...]
) -> None:
_, t = check_table({"k": self.k.value, "t": value}, where="FromArray")
check_finite_positive("t", t, where="FromArray")
@t_tot.validator
def _check_t_tot(
self, attribute: attrs.Attribute[Any], value: tuple[float, ...] | None
) -> None:
if value is not None:
_, t_tot = check_table({"k": self.k.value, "t_tot": value}, where="FromArray")
check_finite_positive("t_tot", t_tot, where="FromArray")
def _table(
self, cosmology: FLRW, k_accuracy: KAccuracy, disk_cache: DiskCache | None
) -> tuple[Array, Mapping[str, Array], BoltzmannRun | None]:
t = np.array(self.t)
return (
np.array(self._k),
{"cb": t, "tot": t if self.t_tot is None else np.array(self.t_tot)},
None,
)
[docs]
@attrs.frozen(kw_only=True)
class FromFile(_Tabulated, alias="FromFile"):
"""A transfer function read from a text file.
The file has either two columns, k (in h/Mpc) and T, used for both species; or
the columns of a CAMB transfer-function file, of which column 0 is k (h/Mpc),
column 6 total matter and column 7 (if present) CDM + baryons. The file is read
when the model is solved; its content is not part of the model's value. Beyond
the table, T(k) is extrapolated with a warning, as for :class:`FromArray`.
"""
_user_table: ClassVar[bool] = True
fname: Path = field(converter=Path, doc="The file to read.")
def _table(
self, cosmology: FLRW, k_accuracy: KAccuracy, disk_cache: DiskCache | None
) -> tuple[Array, Mapping[str, Array], BoltzmannRun | None]:
data = np.atleast_2d(np.genfromtxt(self.fname))
k = data[:, 0]
if data.shape[1] == 2:
return k, {"cb": data[:, 1], "tot": data[:, 1]}, None
tot_col, cb_col = CAMB_COLUMNS["tot"], CAMB_COLUMNS["cb"]
if data.shape[1] <= tot_col:
raise ValueError(
f"{self.fname} has {data.shape[1]} columns: expected 2 (k, T) or a CAMB "
"transfer-function file."
)
cb = data[:, cb_col] if data.shape[1] > cb_col else data[:, tot_col]
return k, {"cb": cb, "tot": data[:, tot_col]}, None
@attrs.frozen(kw_only=True)
class _Boltzmann(_BoltzmannBacked, _Tabulated, abstract=True):
"""A transfer model computed by a Boltzmann code."""
k_max: u.Quantity = quantity_field(
h_Mpc,
default=20.0 * h_Mpc,
ndim=0,
validator=_positive_quantity,
doc=(
"The largest wavenumber the code computes, a Quantity in h-units (stored in "
"h/Mpc). Above it (strictly: above the code's last output node) T(k) is "
"extrapolated smoothly (see tail_decay_ln_k)."
),
)
z_max: float = field(
default=20.0,
converter=float,
validator=positive,
doc=(
"The largest redshift at which the run also computes the growth factor, for "
"a growth stage that shares the run."
),
)
@functools.cached_property
def _k_max(self) -> float:
"""``k_max`` as a plain float in h/Mpc."""
return float(self.k_max.value)
@abc.abstractmethod
def run_input(self, cosmology: FLRW, k_accuracy: KAccuracy) -> dict[str, Any]:
"""The input of the Boltzmann code: plain values that fully define its run.
Parameters
----------
cosmology
The cosmology.
k_accuracy
The wavenumber accuracy.
Returns
-------
dict
"""
def run(
self, cosmology: FLRW, k_accuracy: KAccuracy, *, disk_cache: DiskCache | None = None
) -> BoltzmannRun:
"""The output of the Boltzmann code for a cosmology: every species, one run.
Runs are memoised in this process by the content hash of
:meth:`run_input`, and cached on disk if ``disk_cache`` is given.
"""
self.check_cosmology(cosmology)
assert self.backend is not None
return get_run(self.backend, self.run_input(cosmology, k_accuracy), disk_cache=disk_cache)
def _table(
self, cosmology: FLRW, k_accuracy: KAccuracy, disk_cache: DiskCache | None
) -> tuple[Array, Mapping[str, Array], BoltzmannRun | None]:
"""The run's table, without a spurious low-k turn-up of any species.
The table is cut, for every species, above the highest turn-up of any (see
:func:`~hmf.core._kernels.transfer.low_k_turn_up_end`).
"""
run = self.run(cosmology, k_accuracy, disk_cache=disk_cache)
k = np.asarray(run.k, dtype=float)
transfers = {s: np.abs(np.asarray(t, dtype=float)) for s, t in run.transfer.items()}
scales = _eh98_scales(cosmology)
start = max(kt.low_k_turn_up_end(k, t, scales) for t in transfers.values())
return k[start:], {s: t[start:] for s, t in transfers.items()}, run
#: How many redshifts the CAMB run computes the growth factor at.
_CAMB_GROWTH_N_Z = 201
#: The ``KAccuracy.dln_k`` below which CAMB samples k more finely than its own default
#: (see :func:`_camb_k_per_logint`) and runs with ``high_precision``. At or above it,
#: CAMB runs with its own sampling. It is a constant of its own, so that the
#: ``KAccuracy`` defaults can change without changing CAMB's precision.
_CAMB_FINE_K_BELOW_DLN_K = 0.02
#: The ``KAccuracy.dln_k`` below which CLASS samples k more finely than its own
#: default (see :func:`_class_k_per_decade`). At or above it, CLASS uses its own
#: ``k_per_decade_for_pk``. Fixed for the same reason as
#: ``_CAMB_FINE_K_BELOW_DLN_K``.
_CLASS_FINE_K_BELOW_DLN_K = 0.02
def _camb_k_per_logint(k_accuracy: KAccuracy) -> int:
"""CAMB's ``Transfer.k_per_logint`` for a wavenumber accuracy.
0 (CAMB's own choice, about 0.05 in ln k at high k and coarser at low k) for
``dln_k >= _CAMB_FINE_K_BELOW_DLN_K`` (the default and fast accuracies);
otherwise a fifth of 1 / dln_k per e-fold, since T(k) is interpolated relative to
EH98 (see :class:`TabulatedTransfer`) and needs far fewer nodes than the grid of
the mass variance.
"""
if k_accuracy.dln_k >= _CAMB_FINE_K_BELOW_DLN_K:
return 0
return math.ceil(0.2 / k_accuracy.dln_k)
[docs]
@attrs.frozen(kw_only=True)
class CAMB(_Boltzmann, alias="CAMB"):
"""The transfer function computed by CAMB.
The cosmology must be a LambdaCDM, wCDM or w0waCDM (or a flat one) with the
baryon density and the CMB temperature set. Massive neutrinos are passed species
by species (see :func:`~hmf.core._boltzmann.camb_cosmology_input`). Both species
come from one run: ``"cb"`` is CAMB's ``Transfer_nonu`` and ``"tot"`` its
``Transfer_tot``. The run also computes the growth factor of both species at
k = 0.01/Mpc, for a :class:`~hmf.core.growth.Growth` stage that shares it.
"""
backend: ClassVar[str | None] = "camb"
references: ClassVar[tuple[str, ...]] = (refs.CAMB,)
dark_energy_model: Literal["fluid", "ppf"] = field(
default="fluid",
validator=attrs.validators.in_(("fluid", "ppf")),
doc="CAMB's dark-energy model ('ppf' can cross w = -1).",
)
settings: tuple[tuple[str, Any], ...] = field(
factory=tuple,
converter=_pairs,
doc=(
"Further CAMBparams settings, as a mapping from attribute path to value, e.g. "
"{'Accuracy.AccuracyBoost': 2}. They are applied last. Each path must be a "
"setting of CAMBparams (checked when the model is built, which imports CAMB "
"if there are settings): a ValueError otherwise."
),
)
@settings.validator
def _check_settings(
self, attribute: attrs.Attribute[Any], value: tuple[tuple[str, Any], ...]
) -> None:
if value:
check_camb_settings(name for name, _ in value)
#: CLASS input parameters set from the cosmology or by the model.
_CLASS_FIXED_PARAMS = frozenset(
{
"h", "H0", "100*theta_s", "theta_s_100", "omega_b", "Omega_b", "omega_cdm",
"Omega_cdm", "omega_m", "Omega_m", "Omega_k", "T_cmb", "Omega_g", "omega_g",
"N_ur", "Omega_ur", "omega_ur", "N_ncdm", "m_ncdm", "deg_ncdm", "T_ncdm",
"Omega_ncdm", "omega_ncdm", "Omega_Lambda", "Omega_fld", "w0_fld", "wa_fld",
"Omega_scf", "gauge", "output", "P_k_max_h/Mpc", "P_k_max_1/Mpc", "z_max_pk",
}
) # fmt: skip
def _class_k_per_decade(k_accuracy: KAccuracy) -> int | None:
"""CLASS's ``k_per_decade_for_pk`` for a wavenumber accuracy.
None (CLASS's own default) for ``dln_k >= _CLASS_FINE_K_BELOW_DLN_K``;
otherwise a fifth of 1 / dln_k per e-fold, as for CAMB.
"""
if k_accuracy.dln_k >= _CLASS_FINE_K_BELOW_DLN_K:
return None
return math.ceil(0.2 * math.log(10) / k_accuracy.dln_k)
[docs]
@attrs.frozen(kw_only=True)
class CLASS(_Boltzmann, alias="CLASS"):
r"""The transfer function computed by CLASS.
Needs the optional ``classy`` package (``pip install hmf[class]``). The cosmology
is converted as described in
:func:`~hmf.core._boltzmann.class_cosmology_input`. The transfer function is
:math:`-\delta(k)/k^2` at z = 0 in the synchronous gauge: ``"tot"`` from CLASS's
``d_m``, and ``"cb"`` the density-weighted mean of ``d_cdm`` and ``d_b``. The run
also computes the growth factor of both species at k = 0.01/Mpc (from CLASS's
P(k, z)), for a :class:`~hmf.core.growth.Growth` stage that shares it.
"""
backend: ClassVar[str | None] = "class"
references: ClassVar[tuple[str, ...]] = (refs.CLASS_I, refs.CLASS_II)
class_params: tuple[tuple[str, Any], ...] = field(
factory=tuple,
converter=_pairs,
doc=(
"Further CLASS input parameters (e.g. precision parameters, or YHe), as a "
"mapping. Those that describe the cosmology, the gauge, the output and the "
"k and z range are set by the model and can't be given here."
),
)
@class_params.validator
def _check_class_params(
self, attribute: attrs.Attribute[Any], value: tuple[tuple[str, Any], ...]
) -> None:
clash = sorted({k for k, _ in value} & _CLASS_FIXED_PARAMS)
if clash:
raise ValueError(
f"class_params can't set {clash}: they are set from the cosmology and the "
"model's fields."
)