fastcmb 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastcmb/__init__.py +23 -0
- fastcmb/camb_oracle.py +134 -0
- fastcmb/cobaya.py +133 -0
- fastcmb/compress.py +148 -0
- fastcmb/emulator.py +247 -0
- fastcmb/gp.py +305 -0
- fastcmb/metric.py +66 -0
- fastcmb/online.py +363 -0
- fastcmb-0.3.0.dist-info/METADATA +264 -0
- fastcmb-0.3.0.dist-info/RECORD +13 -0
- fastcmb-0.3.0.dist-info/WHEEL +5 -0
- fastcmb-0.3.0.dist-info/licenses/LICENSE +21 -0
- fastcmb-0.3.0.dist-info/top_level.txt +1 -0
fastcmb/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fastcmb: an online emulator of Boltzmann codes for CMB analyses.
|
|
3
|
+
|
|
4
|
+
:class:`CMBEmulator` wraps a Boltzmann code -- an *oracle*, such as
|
|
5
|
+
:class:`CAMBOracle` -- and answers with a Gaussian-process prediction
|
|
6
|
+
wherever its error, in units of the log-likelihood, is below a
|
|
7
|
+
tolerance; elsewhere the code runs, and the point is learned. It needs
|
|
8
|
+
only numpy and scipy, and fits in any analysis code:
|
|
9
|
+
|
|
10
|
+
* CosmoFit: ``theory: {background: ..., fastcmb: ...}`` in place of
|
|
11
|
+
``camb`` (built into CosmoFit, with ``fastcmb`` installed);
|
|
12
|
+
* cobaya: the theory ``fastcmb.cobaya.FastCAMB`` in place of ``camb``;
|
|
13
|
+
* anything else: call a :class:`CMBEmulator`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .camb_oracle import CAMBOracle
|
|
17
|
+
from .emulator import CMBEmulator, Result
|
|
18
|
+
from .gp import GP
|
|
19
|
+
from .online import OnlineEmulator
|
|
20
|
+
|
|
21
|
+
__all__ = ["GP", "CAMBOracle", "CMBEmulator", "OnlineEmulator", "Result", "__version__"]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.3.0"
|
fastcmb/camb_oracle.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAMB as an oracle for :class:`~fastcmb.emulator.CMBEmulator`, with
|
|
3
|
+
parameters named as cobaya and most codes name them.
|
|
4
|
+
|
|
5
|
+
Parameters (missing ones take the default in brackets):
|
|
6
|
+
|
|
7
|
+
* ``H0`` -- or ``cosmomc_theta`` (``100 theta_MC``, as ``theta_MC_100``);
|
|
8
|
+
* ``ombh2``, ``omch2``; ``omk`` [0];
|
|
9
|
+
* ``mnu`` [0.06 eV, one massive species], ``nnu`` [3.044];
|
|
10
|
+
* ``ln1e10As`` (or ``logA``) or ``As``; ``ns``; ``tau``;
|
|
11
|
+
* ``w`` [-1], ``wa`` [0]: a CPL dark energy through CAMB's PPF module.
|
|
12
|
+
|
|
13
|
+
CAMB is imported on first use.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
__all__ = ["CAMBOracle"]
|
|
23
|
+
|
|
24
|
+
_DEFAULTS = {"omk": 0.0, "mnu": 0.06, "nnu": 3.044, "w": -1.0, "wa": 0.0}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CAMBOracle:
|
|
28
|
+
"""
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
lmax : int
|
|
32
|
+
Highest multipole of the spectra.
|
|
33
|
+
lens_potential_accuracy : int
|
|
34
|
+
CAMB's; 1 is enough for Planck's CMB spectra, 4 for its lensing.
|
|
35
|
+
halofit_version : str
|
|
36
|
+
CAMB's non-linear model for the lensing potential.
|
|
37
|
+
TCMB : float
|
|
38
|
+
CMB temperature [K].
|
|
39
|
+
accuracy : dict, optional
|
|
40
|
+
Passed to ``CAMBparams.set_accuracy`` (``AccuracyBoost``, ...).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, lmax: int = 2508, lens_potential_accuracy: int = 1,
|
|
44
|
+
halofit_version: str = "mead2020", TCMB: float = 2.7255,
|
|
45
|
+
accuracy: dict | None = None):
|
|
46
|
+
|
|
47
|
+
self.lmax = int(lmax)
|
|
48
|
+
self.lens_potential_accuracy = int(lens_potential_accuracy)
|
|
49
|
+
self.halofit_version = halofit_version
|
|
50
|
+
self.TCMB = float(TCMB)
|
|
51
|
+
self.accuracy = dict(accuracy or {})
|
|
52
|
+
self._camb = None
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def tag(self) -> str:
|
|
56
|
+
accuracy = ",".join(f"{k}={v}" for k, v in sorted(self.accuracy.items()))
|
|
57
|
+
return (f"camb|lmax={self.lmax}|lens={self.lens_potential_accuracy}"
|
|
58
|
+
f"|halofit={self.halofit_version}|T={self.TCMB}|{accuracy}")
|
|
59
|
+
|
|
60
|
+
def _import(self):
|
|
61
|
+
if self._camb is None:
|
|
62
|
+
import camb
|
|
63
|
+
self._camb = camb
|
|
64
|
+
return self._camb
|
|
65
|
+
|
|
66
|
+
def parameters(self, params: dict):
|
|
67
|
+
"""The ``CAMBparams`` for ``params``."""
|
|
68
|
+
|
|
69
|
+
camb = self._import()
|
|
70
|
+
p = {**_DEFAULTS, **params}
|
|
71
|
+
|
|
72
|
+
pars = camb.CAMBparams()
|
|
73
|
+
|
|
74
|
+
theta = p.get("cosmomc_theta", p.get("theta_MC_100"))
|
|
75
|
+
cosmology = {
|
|
76
|
+
"ombh2": p["ombh2"], "omch2": p["omch2"], "omk": p["omk"], "mnu": p["mnu"], "nnu": p["nnu"],
|
|
77
|
+
"num_massive_neutrinos": 1 if p["mnu"] > 0 else 0, "TCMB": self.TCMB, "tau": p["tau"],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if theta is not None:
|
|
81
|
+
pars.set_cosmology(cosmomc_theta=float(theta) / 100.0, **cosmology)
|
|
82
|
+
else:
|
|
83
|
+
pars.set_cosmology(H0=p["H0"], **cosmology)
|
|
84
|
+
|
|
85
|
+
if "As" in p:
|
|
86
|
+
As = p["As"]
|
|
87
|
+
else:
|
|
88
|
+
As = math.exp(p.get("ln1e10As", p.get("logA"))) * 1.0e-10
|
|
89
|
+
|
|
90
|
+
pars.InitPower.set_params(As=As, ns=p["ns"])
|
|
91
|
+
|
|
92
|
+
if p["w"] != -1.0 or p["wa"] != 0.0:
|
|
93
|
+
pars.set_dark_energy(w=p["w"], wa=p["wa"], dark_energy_model="ppf")
|
|
94
|
+
|
|
95
|
+
pars.set_matter_power(redshifts=[0.0], kmax=2.0, silent=True)
|
|
96
|
+
pars.set_for_lmax(self.lmax + 500, lens_potential_accuracy=self.lens_potential_accuracy)
|
|
97
|
+
pars.NonLinearModel.set_params(halofit_version=self.halofit_version)
|
|
98
|
+
|
|
99
|
+
if self.accuracy:
|
|
100
|
+
pars.set_accuracy(**self.accuracy)
|
|
101
|
+
|
|
102
|
+
return pars
|
|
103
|
+
|
|
104
|
+
def __call__(self, params: dict) -> dict | None:
|
|
105
|
+
|
|
106
|
+
camb = self._import()
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
results = camb.get_results(self.parameters(params))
|
|
110
|
+
|
|
111
|
+
total = results.get_cmb_power_spectra(
|
|
112
|
+
lmax=self.lmax, spectra=("total",), CMB_unit="muK", raw_cl=True,
|
|
113
|
+
)["total"]
|
|
114
|
+
pp = results.get_lens_potential_cls(lmax=self.lmax)[:, 0]
|
|
115
|
+
|
|
116
|
+
sigma8 = float(results.get_sigma8_0())
|
|
117
|
+
sigma8_cb = float(results.get_sigmaR(8.0, z_indices=[-1], var1="delta_nonu",
|
|
118
|
+
var2="delta_nonu")[0])
|
|
119
|
+
|
|
120
|
+
except camb.CAMBError:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
Cl = {
|
|
124
|
+
"ell": np.arange(self.lmax + 1),
|
|
125
|
+
"tt": total[:, 0], "ee": total[:, 1], "bb": total[:, 2], "te": total[:, 3], "pp": pp,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# CAMB can return NaN without raising; not a point to learn.
|
|
129
|
+
if not all(np.all(np.isfinite(v)) for v in Cl.values()) or not (
|
|
130
|
+
math.isfinite(sigma8) and math.isfinite(sigma8_cb)
|
|
131
|
+
):
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
return {"Cl": Cl, "sigma8": sigma8, "sigma8_cb": sigma8_cb}
|
fastcmb/cobaya.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fastcmb as a cobaya theory.
|
|
3
|
+
|
|
4
|
+
In a cobaya input, in place of ``camb``::
|
|
5
|
+
|
|
6
|
+
theory:
|
|
7
|
+
fastcmb.cobaya.FastCAMB:
|
|
8
|
+
tolerance: 0.05
|
|
9
|
+
training_file: chains/emu/lcdm
|
|
10
|
+
|
|
11
|
+
It provides ``Cl`` -- what cobaya's CMB likelihoods ask for -- and the
|
|
12
|
+
derived ``sigma8``, from the parameters :mod:`fastcmb.camb_oracle`
|
|
13
|
+
names (``H0`` or ``cosmomc_theta``, ``ombh2``, ``omch2``, ``logA`` or
|
|
14
|
+
``As``, ``ns``, ``tau``, and optionally ``mnu``, ``nnu``, ``omk``,
|
|
15
|
+
``w``, ``wa``). Where the emulator is not sure, CAMB runs.
|
|
16
|
+
|
|
17
|
+
cobaya is imported here only, so ``fastcmb`` itself does not need it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from cobaya.theory import Theory
|
|
24
|
+
|
|
25
|
+
from .camb_oracle import CAMBOracle
|
|
26
|
+
from .emulator import CMBEmulator
|
|
27
|
+
|
|
28
|
+
__all__ = ["FastCAMB"]
|
|
29
|
+
|
|
30
|
+
_INPUTS = ("H0", "cosmomc_theta", "theta_MC_100", "ombh2", "omch2", "omk", "mnu", "nnu",
|
|
31
|
+
"logA", "ln1e10As", "As", "ns", "tau", "w", "wa")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FastCAMB(Theory):
|
|
35
|
+
|
|
36
|
+
tolerance: float = 0.05
|
|
37
|
+
sigma8_tolerance: float = 1e-3
|
|
38
|
+
min_train: int = 30
|
|
39
|
+
retrain_fraction: float = 0.1
|
|
40
|
+
max_train: int = 800
|
|
41
|
+
training_file: str | None = None
|
|
42
|
+
check_every: int = 200
|
|
43
|
+
lens_potential_accuracy: int = 1
|
|
44
|
+
halofit_version: str = "mead2020"
|
|
45
|
+
TCMB: float = 2.7255
|
|
46
|
+
lmax: int = 0
|
|
47
|
+
|
|
48
|
+
def initialize(self):
|
|
49
|
+
self._lmax = int(self.lmax)
|
|
50
|
+
self._lens = int(self.lens_potential_accuracy)
|
|
51
|
+
self.emulator = None
|
|
52
|
+
|
|
53
|
+
def get_can_support_params(self):
|
|
54
|
+
return list(_INPUTS)
|
|
55
|
+
|
|
56
|
+
def get_can_provide(self):
|
|
57
|
+
return ["Cl"]
|
|
58
|
+
|
|
59
|
+
def get_can_provide_params(self):
|
|
60
|
+
return ["sigma8"]
|
|
61
|
+
|
|
62
|
+
def must_provide(self, **requirements):
|
|
63
|
+
|
|
64
|
+
Cl = requirements.get("Cl")
|
|
65
|
+
if Cl:
|
|
66
|
+
self._lmax = max(self._lmax, *(int(v) for v in Cl.values()))
|
|
67
|
+
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
def initialize_with_provider(self, provider):
|
|
71
|
+
|
|
72
|
+
super().initialize_with_provider(provider)
|
|
73
|
+
|
|
74
|
+
if self._lmax == 0:
|
|
75
|
+
self._lmax = 2508
|
|
76
|
+
|
|
77
|
+
oracle = CAMBOracle(lmax=self._lmax, lens_potential_accuracy=self._lens,
|
|
78
|
+
halofit_version=self.halofit_version, TCMB=self.TCMB)
|
|
79
|
+
|
|
80
|
+
self.emulator = CMBEmulator(
|
|
81
|
+
oracle, inputs=sorted(self.input_params), tolerance=self.tolerance,
|
|
82
|
+
sigma8_tolerance=self.sigma8_tolerance, min_train=self.min_train,
|
|
83
|
+
retrain_fraction=self.retrain_fraction, max_train=self.max_train,
|
|
84
|
+
training_file=self.training_file, check_every=self.check_every,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def calculate(self, state, want_derived=True, **params_values_dict):
|
|
88
|
+
|
|
89
|
+
result = self.emulator(params_values_dict)
|
|
90
|
+
|
|
91
|
+
if result is None:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
state["Cl"] = result.Cl
|
|
95
|
+
state["emulated"] = result.emulated
|
|
96
|
+
|
|
97
|
+
if want_derived:
|
|
98
|
+
state["derived"] = {"sigma8": result.sigma8}
|
|
99
|
+
|
|
100
|
+
def get_Cl(self, ell_factor=False, units="FIRASmuK2"):
|
|
101
|
+
"""As cobaya's ``camb`` returns them: ``tt``, ``te``, ``ee``, ``bb``, ``pp``."""
|
|
102
|
+
|
|
103
|
+
Cl = self.current_state["Cl"]
|
|
104
|
+
|
|
105
|
+
# cobaya's convention: the dimensionless spectra times u^2, with
|
|
106
|
+
# u from its table; the spectra here are in muK^2 at TCMB.
|
|
107
|
+
units_factor = {
|
|
108
|
+
"1": 1.0, "muK2": self.TCMB * 1e6, "K2": self.TCMB,
|
|
109
|
+
"FIRASmuK2": 2.7255e6, "FIRASK2": 2.7255,
|
|
110
|
+
}
|
|
111
|
+
if units not in units_factor:
|
|
112
|
+
raise ValueError(f"Units {units!r} not recognized; use one of {list(units_factor)}.")
|
|
113
|
+
|
|
114
|
+
factor = (units_factor[units] / (self.TCMB * 1e6)) ** 2
|
|
115
|
+
|
|
116
|
+
ell = Cl["ell"].astype(float)
|
|
117
|
+
out = {"ell": Cl["ell"]}
|
|
118
|
+
|
|
119
|
+
scale = np.ones_like(ell)
|
|
120
|
+
if ell_factor:
|
|
121
|
+
scale = ell * (ell + 1.0) / (2.0 * np.pi)
|
|
122
|
+
|
|
123
|
+
for name in ("tt", "te", "ee", "bb"):
|
|
124
|
+
out[name] = Cl[name] * factor * scale
|
|
125
|
+
out["et"] = out["te"]
|
|
126
|
+
|
|
127
|
+
# pp is [L(L+1)]^2 C_L / 2 pi already; raw C_L without ell_factor.
|
|
128
|
+
pp = Cl["pp"].copy()
|
|
129
|
+
if not ell_factor:
|
|
130
|
+
pp[1:] /= (ell[1:] * (ell[1:] + 1.0)) ** 2 / (2.0 * np.pi)
|
|
131
|
+
out["pp"] = pp
|
|
132
|
+
|
|
133
|
+
return out
|
fastcmb/compress.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What is emulated, and how it is made smooth and small.
|
|
3
|
+
|
|
4
|
+
Each spectrum is a *block*. From ``l = 2``:
|
|
5
|
+
|
|
6
|
+
* ``tt``, ``ee``, ``bb``, ``pp``: ``ln C_l`` -- positive, and spanning
|
|
7
|
+
orders of magnitude;
|
|
8
|
+
* ``te``: the correlation ``C_TE / sqrt(C_TT C_EE)``, which crosses zero
|
|
9
|
+
where ``C_TE`` does and stays within [-1, 1];
|
|
10
|
+
* ``scalars``: ``ln sigma8`` (all matter) and ``ln sigma8`` of the cold
|
|
11
|
+
matter.
|
|
12
|
+
|
|
13
|
+
A block of spectra is projected on its leading principal components:
|
|
14
|
+
as many as it takes to reproduce every training spectrum to
|
|
15
|
+
:data:`PCA_TOLERANCE` in each multipole and, where the block has
|
|
16
|
+
likelihood weights (:mod:`fastcmb.metric`), to :data:`PCA_LOGL` in
|
|
17
|
+
the log-likelihood.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
__all__ = ["BLOCKS", "PCA", "PCA_TOLERANCE", "from_blocks", "to_blocks"]
|
|
25
|
+
|
|
26
|
+
BLOCKS = ("tt", "te", "ee", "bb", "pp", "scalars")
|
|
27
|
+
|
|
28
|
+
#: Largest error, over the training set, a block's PCA may make: in
|
|
29
|
+
#: ``ln C_l`` (so 1e-3 is 0.1 %) or in the TE correlation.
|
|
30
|
+
PCA_TOLERANCE = 1e-3
|
|
31
|
+
|
|
32
|
+
#: Largest log-likelihood error, ``sqrt(delta^T F delta)``, a block's
|
|
33
|
+
#: PCA may make on a training spectrum.
|
|
34
|
+
PCA_LOGL = 0.01
|
|
35
|
+
|
|
36
|
+
#: Most components a block keeps.
|
|
37
|
+
MAX_COMPONENTS = 60
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_blocks(Cl: dict, sigma8: float, sigma8_cb: float) -> dict:
|
|
41
|
+
"""A CosmoFit ``Cl`` dict and the two ``sigma8`` as block vectors."""
|
|
42
|
+
|
|
43
|
+
tt, ee, te = Cl["tt"][2:], Cl["ee"][2:], Cl["te"][2:]
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"tt": np.log(tt),
|
|
47
|
+
"te": te / np.sqrt(tt * ee),
|
|
48
|
+
"ee": np.log(ee),
|
|
49
|
+
"bb": np.log(Cl["bb"][2:]),
|
|
50
|
+
"pp": np.log(Cl["pp"][2:]),
|
|
51
|
+
"scalars": np.log([sigma8, sigma8_cb]),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def from_blocks(blocks: dict, lmax: int) -> tuple[dict, float, float]:
|
|
56
|
+
"""The inverse of :func:`to_blocks`: ``(Cl, sigma8, sigma8_cb)``."""
|
|
57
|
+
|
|
58
|
+
zeros = np.zeros(2)
|
|
59
|
+
|
|
60
|
+
tt = np.exp(blocks["tt"])
|
|
61
|
+
ee = np.exp(blocks["ee"])
|
|
62
|
+
|
|
63
|
+
Cl = {
|
|
64
|
+
"ell": np.arange(lmax + 1),
|
|
65
|
+
"tt": np.concatenate([zeros, tt]),
|
|
66
|
+
"ee": np.concatenate([zeros, ee]),
|
|
67
|
+
"bb": np.concatenate([zeros, np.exp(blocks["bb"])]),
|
|
68
|
+
"te": np.concatenate([zeros, blocks["te"] * np.sqrt(tt * ee)]),
|
|
69
|
+
"pp": np.concatenate([zeros, np.exp(blocks["pp"])]),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
sigma8, sigma8_cb = np.exp(blocks["scalars"])
|
|
73
|
+
|
|
74
|
+
return Cl, float(sigma8), float(sigma8_cb)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _svd(Z):
|
|
78
|
+
"""
|
|
79
|
+
The thin SVD of ``Z`` ``(n, l)``, from the eigenvectors of the
|
|
80
|
+
smaller of its two Gram matrices -- for ``n`` a few hundred points of
|
|
81
|
+
``l`` ~ 2500 multipoles, much faster than LAPACK's SVD of ``Z``. The
|
|
82
|
+
components it loses to rounding are ~1e-8 of the largest, far below
|
|
83
|
+
what any tolerance here keeps.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
n, l = Z.shape
|
|
87
|
+
|
|
88
|
+
if n <= l:
|
|
89
|
+
w, U = np.linalg.eigh(Z @ Z.T)
|
|
90
|
+
order = np.argsort(w)[::-1]
|
|
91
|
+
s = np.sqrt(np.maximum(w[order], 0.0))
|
|
92
|
+
U = U[:, order]
|
|
93
|
+
keep = s > s[0] * 1e-12
|
|
94
|
+
Vt = (U[:, keep].T @ Z) / s[keep, None]
|
|
95
|
+
return U[:, keep], s[keep], Vt
|
|
96
|
+
|
|
97
|
+
w, V = np.linalg.eigh(Z.T @ Z)
|
|
98
|
+
order = np.argsort(w)[::-1]
|
|
99
|
+
s = np.sqrt(np.maximum(w[order], 0.0))
|
|
100
|
+
V = V[:, order]
|
|
101
|
+
keep = s > s[0] * 1e-12
|
|
102
|
+
U = (Z @ V[:, keep]) / s[keep]
|
|
103
|
+
return U, s[keep], V[:, keep].T
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PCA:
|
|
107
|
+
"""Principal components of a block's training vectors ``(n, l)``."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, Y, weights=None, tolerance: float = PCA_TOLERANCE,
|
|
110
|
+
logl: float = PCA_LOGL, max_components: int = MAX_COMPONENTS):
|
|
111
|
+
|
|
112
|
+
Y = np.asarray(Y, dtype=float)
|
|
113
|
+
|
|
114
|
+
self.mean = Y.mean(0)
|
|
115
|
+
Z = Y - self.mean
|
|
116
|
+
|
|
117
|
+
if Y.shape[1] <= 2:
|
|
118
|
+
# Nothing to compress: the scalars are their own components.
|
|
119
|
+
self.components = np.eye(Y.shape[1])
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
U, s, Vt = _svd(Z)
|
|
123
|
+
|
|
124
|
+
top = min(max_components, Vt.shape[0])
|
|
125
|
+
|
|
126
|
+
# The fewest components that reproduce every training vector; the
|
|
127
|
+
# residual loses one component at a time. A block that has not
|
|
128
|
+
# varied keeps none: its mean is the prediction.
|
|
129
|
+
residual = Z.copy()
|
|
130
|
+
k = 0
|
|
131
|
+
for k in range(1, top + 1):
|
|
132
|
+
residual -= np.outer(U[:, k - 1] * s[k - 1], Vt[k - 1])
|
|
133
|
+
if np.abs(residual).max() >= tolerance:
|
|
134
|
+
continue
|
|
135
|
+
if weights is None or np.sqrt((residual ** 2 @ weights).max()) < logl:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
self.components = Vt[:k]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def size(self) -> int:
|
|
142
|
+
return self.components.shape[0]
|
|
143
|
+
|
|
144
|
+
def project(self, Y):
|
|
145
|
+
return (np.asarray(Y, dtype=float) - self.mean) @ self.components.T
|
|
146
|
+
|
|
147
|
+
def reconstruct(self, coefficients):
|
|
148
|
+
return self.mean + coefficients @ self.components
|
fastcmb/emulator.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The emulator as a function: parameters in, spectra out.
|
|
3
|
+
|
|
4
|
+
:class:`CMBEmulator` wraps any Boltzmann code -- an *oracle*, a function
|
|
5
|
+
from a parameter dict to spectra. At each call it predicts, and answers
|
|
6
|
+
with the prediction when its error, in units of ``ln L``, is below the
|
|
7
|
+
tolerance; otherwise it runs the oracle and learns the point.
|
|
8
|
+
|
|
9
|
+
This is all an analysis code needs: CosmoFit's ``fastcmb`` theory, the
|
|
10
|
+
cobaya theory in :mod:`fastcmb.cobaya`, or a script of one's own::
|
|
11
|
+
|
|
12
|
+
from fastcmb import CAMBOracle, CMBEmulator
|
|
13
|
+
|
|
14
|
+
emulator = CMBEmulator(CAMBOracle(lmax=2508))
|
|
15
|
+
result = emulator({"H0": 67.4, "ombh2": 0.0224, "omch2": 0.12,
|
|
16
|
+
"ln1e10As": 3.044, "ns": 0.965, "tau": 0.054})
|
|
17
|
+
result.Cl["tt"], result.sigma8, result.emulated
|
|
18
|
+
|
|
19
|
+
An oracle returns a dict with ``Cl`` -- ``ell``, ``tt``, ``te``, ``ee``,
|
|
20
|
+
``bb`` (``C_l`` in muK^2) and ``pp`` (``[L(L+1)]^2 C_L / 2 pi``), from
|
|
21
|
+
``l = 0`` to ``lmax`` -- and ``sigma8`` and ``sigma8_cb``; or ``None``
|
|
22
|
+
when it cannot compute the point.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
from .compress import from_blocks, to_blocks
|
|
34
|
+
from .online import OnlineEmulator
|
|
35
|
+
|
|
36
|
+
__all__ = ["CMBEmulator", "Result"]
|
|
37
|
+
|
|
38
|
+
log = logging.getLogger("fastcmb")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Result:
|
|
43
|
+
Cl: dict
|
|
44
|
+
sigma8: float
|
|
45
|
+
sigma8_cb: float
|
|
46
|
+
#: Whether the spectra are the emulator's (else the oracle's).
|
|
47
|
+
emulated: bool
|
|
48
|
+
#: The predicted ``ln L`` error, when there was a prediction.
|
|
49
|
+
error: float | None = None
|
|
50
|
+
#: Whether this is a prediction above the tolerance, given because
|
|
51
|
+
#: the call allowed it (``loose_tolerance``): good enough to tell
|
|
52
|
+
#: that a point is far worse than another, not to be kept.
|
|
53
|
+
approximate: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CMBEmulator:
|
|
57
|
+
"""
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
oracle : callable
|
|
61
|
+
``oracle(params) -> dict | None``, as described in the module.
|
|
62
|
+
It may also be given per call, to :meth:`__call__`.
|
|
63
|
+
lmax : int, optional
|
|
64
|
+
Highest multipole; by default the oracle's ``lmax``.
|
|
65
|
+
inputs : list of str, optional
|
|
66
|
+
The parameters the spectra depend on, in a fixed order. By
|
|
67
|
+
default the keys of the first call's parameters, sorted. A
|
|
68
|
+
parameter not listed is ignored -- it must not change the
|
|
69
|
+
spectra.
|
|
70
|
+
tag : str, optional
|
|
71
|
+
What the spectra are computed for (model, accuracy); points
|
|
72
|
+
stored under another tag are not read. By default the oracle's
|
|
73
|
+
``tag``.
|
|
74
|
+
tolerance : float
|
|
75
|
+
Largest predicted ``ln L`` error the emulator answers with.
|
|
76
|
+
The measure is that of an idealized Planck (see
|
|
77
|
+
:mod:`fastcmb.metric`).
|
|
78
|
+
sigma8_tolerance : float
|
|
79
|
+
Largest predicted relative error of ``sigma8``.
|
|
80
|
+
min_train, retrain_fraction, max_train, training_file
|
|
81
|
+
As :class:`~fastcmb.online.OnlineEmulator`'s ``min_train``,
|
|
82
|
+
``retrain_fraction``, ``max_train`` and ``path``.
|
|
83
|
+
check_every : int
|
|
84
|
+
After this many predictions, the oracle runs as well and the
|
|
85
|
+
error is measured; a prediction off by more than three times
|
|
86
|
+
the tolerance is learned. 0 never. While the checks find the
|
|
87
|
+
error within the tolerance, the interval between them doubles,
|
|
88
|
+
up to ten times ``check_every``; a check that does not brings
|
|
89
|
+
it back.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, oracle: Callable | None = None, lmax: int | None = None,
|
|
93
|
+
inputs: list[str] | None = None, tag: str | None = None,
|
|
94
|
+
tolerance: float = 0.05, sigma8_tolerance: float = 1e-3,
|
|
95
|
+
min_train: int = 30, retrain_fraction: float = 0.1, max_train: int = 800,
|
|
96
|
+
training_file: str | None = None, check_every: int = 200):
|
|
97
|
+
|
|
98
|
+
self.oracle = oracle
|
|
99
|
+
self.lmax = lmax if lmax is not None else getattr(oracle, "lmax", None)
|
|
100
|
+
self.inputs = None if inputs is None else list(inputs)
|
|
101
|
+
self.tag = tag if tag is not None else str(getattr(oracle, "tag", ""))
|
|
102
|
+
self.tolerance = float(tolerance)
|
|
103
|
+
self.sigma8_tolerance = float(sigma8_tolerance)
|
|
104
|
+
self.check_every = int(check_every)
|
|
105
|
+
self._options = {"min_train": min_train, "retrain_fraction": retrain_fraction,
|
|
106
|
+
"max_train": max_train, "path": training_file}
|
|
107
|
+
|
|
108
|
+
self.online: OnlineEmulator | None = None
|
|
109
|
+
|
|
110
|
+
self.n_oracle = 0
|
|
111
|
+
self.n_emulated = 0
|
|
112
|
+
#: Predictions given as approximate (see :meth:`__call__`).
|
|
113
|
+
self.n_approximate = 0
|
|
114
|
+
self.check_errors: list[float] = []
|
|
115
|
+
self._check_interval = self.check_every
|
|
116
|
+
self._next_check = self.check_every
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def _vector(self, params) -> np.ndarray:
|
|
121
|
+
|
|
122
|
+
if self.inputs is None:
|
|
123
|
+
self.inputs = sorted(params)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
return np.array([float(params[name]) for name in self.inputs])
|
|
127
|
+
except KeyError as error:
|
|
128
|
+
raise KeyError(f"fastcmb: the emulator's input {error} is missing from the parameters.") from None
|
|
129
|
+
|
|
130
|
+
def _online(self) -> OnlineEmulator:
|
|
131
|
+
|
|
132
|
+
if self.online is None:
|
|
133
|
+
if self.lmax is None:
|
|
134
|
+
raise ValueError("fastcmb: give lmax, or an oracle that has one.")
|
|
135
|
+
self.online = OnlineEmulator(self.inputs, self.lmax, tag=self.tag, **self._options)
|
|
136
|
+
|
|
137
|
+
return self.online
|
|
138
|
+
|
|
139
|
+
def __call__(self, params: dict, oracle: Callable | None = None,
|
|
140
|
+
loose_tolerance: float | None = None) -> Result | None:
|
|
141
|
+
"""
|
|
142
|
+
The spectra at ``params``; ``None`` if the oracle cannot compute them.
|
|
143
|
+
|
|
144
|
+
With ``loose_tolerance``, a prediction whose error is above the
|
|
145
|
+
tolerance but below this is returned, marked ``approximate``,
|
|
146
|
+
instead of running the oracle. A sampler uses it to reject,
|
|
147
|
+
without the oracle, a point the prediction already shows to be
|
|
148
|
+
far worse than the current one; one it might accept, it asks
|
|
149
|
+
for again without.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
oracle = oracle or self.oracle
|
|
153
|
+
if oracle is None:
|
|
154
|
+
raise ValueError("fastcmb: no oracle to compute the spectra with.")
|
|
155
|
+
|
|
156
|
+
x = self._vector(params)
|
|
157
|
+
online = self._online()
|
|
158
|
+
|
|
159
|
+
prediction = online.predict(x, tolerance=self.tolerance, scalar_tolerance=self.sigma8_tolerance)
|
|
160
|
+
|
|
161
|
+
trusted = (
|
|
162
|
+
prediction is not None
|
|
163
|
+
and prediction.error <= self.tolerance
|
|
164
|
+
and prediction.scalar_error <= self.sigma8_tolerance
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if not trusted and loose_tolerance is not None and prediction is not None:
|
|
168
|
+
|
|
169
|
+
# The error above was bounded, stopping early; the exact
|
|
170
|
+
# one may be lower.
|
|
171
|
+
prediction = online.predict(x)
|
|
172
|
+
trusted = (prediction.error <= self.tolerance
|
|
173
|
+
and prediction.scalar_error <= self.sigma8_tolerance)
|
|
174
|
+
|
|
175
|
+
if not trusted and prediction.error <= loose_tolerance:
|
|
176
|
+
self.n_approximate += 1
|
|
177
|
+
Cl, sigma8, sigma8_cb = from_blocks(prediction.blocks, online.lmax)
|
|
178
|
+
return Result(Cl, sigma8, sigma8_cb, emulated=True, error=prediction.error,
|
|
179
|
+
approximate=True)
|
|
180
|
+
|
|
181
|
+
if not trusted:
|
|
182
|
+
return self._run(oracle, params, x, learn=True)
|
|
183
|
+
|
|
184
|
+
self.n_emulated += 1
|
|
185
|
+
|
|
186
|
+
if not (self.check_every and self.n_emulated >= self._next_check):
|
|
187
|
+
Cl, sigma8, sigma8_cb = from_blocks(prediction.blocks, online.lmax)
|
|
188
|
+
return Result(Cl, sigma8, sigma8_cb, emulated=True, error=prediction.error)
|
|
189
|
+
|
|
190
|
+
# A check: the oracle too, and the error measured.
|
|
191
|
+
result = self._run(oracle, params, x, learn=False)
|
|
192
|
+
self._next_check = self.n_emulated + self._check_interval
|
|
193
|
+
|
|
194
|
+
if result is not None:
|
|
195
|
+
|
|
196
|
+
truth = to_blocks(result.Cl, result.sigma8, result.sigma8_cb)
|
|
197
|
+
error = online.error_between(prediction.blocks, truth)
|
|
198
|
+
self.check_errors.append(error)
|
|
199
|
+
|
|
200
|
+
# Checks grow rarer while they pass.
|
|
201
|
+
if error <= self.tolerance:
|
|
202
|
+
self._check_interval = min(2 * self._check_interval, 10 * self.check_every)
|
|
203
|
+
else:
|
|
204
|
+
self._check_interval = self.check_every
|
|
205
|
+
self._next_check = self.n_emulated + self._check_interval
|
|
206
|
+
|
|
207
|
+
if error > 3.0 * self.tolerance:
|
|
208
|
+
log.warning(
|
|
209
|
+
"fastcmb: a prediction was off by %.3g in ln L (predicted %.3g); "
|
|
210
|
+
"the point is learned.", error, prediction.error,
|
|
211
|
+
)
|
|
212
|
+
online.add(x, truth)
|
|
213
|
+
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
def _run(self, oracle, params, x, learn: bool) -> Result | None:
|
|
217
|
+
|
|
218
|
+
self.n_oracle += 1
|
|
219
|
+
|
|
220
|
+
out = oracle(params)
|
|
221
|
+
|
|
222
|
+
if out is None:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
result = Result(out["Cl"], float(out["sigma8"]), float(out["sigma8_cb"]), emulated=False)
|
|
226
|
+
|
|
227
|
+
if learn:
|
|
228
|
+
self.online.add(x, to_blocks(result.Cl, result.sigma8, result.sigma8_cb))
|
|
229
|
+
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def stats(self) -> dict:
|
|
235
|
+
"""Calls to the oracle and the emulator, and what the checks found."""
|
|
236
|
+
|
|
237
|
+
errors = np.array(self.check_errors)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"oracle": self.n_oracle,
|
|
241
|
+
"emulated": self.n_emulated,
|
|
242
|
+
"approximate": self.n_approximate,
|
|
243
|
+
"training_points": 0 if self.online is None else self.online.size,
|
|
244
|
+
"checks": errors.size,
|
|
245
|
+
"check_error_median": float(np.median(errors)) if errors.size else None,
|
|
246
|
+
"check_error_max": float(errors.max()) if errors.size else None,
|
|
247
|
+
}
|