classy-szlite 0.2.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.
@@ -0,0 +1,50 @@
1
+ """classy_szlite — fast, differentiable cosmology in pure JAX.
2
+
3
+ A standalone Python package providing JIT-compiled, ``jax.grad``-friendly
4
+ access to:
5
+
6
+ * CMB angular power spectra (TT, TE, EE)
7
+ * Linear and non-linear matter Pk
8
+ * Distances (H(z), chi, Da)
9
+ * Derived parameters (σ8, Ω_m, S8)
10
+ * Halo-model tSZ Cl^yy (Arnaud 2010 GNFW)
11
+
12
+ Backed by the high-accuracy ede-v2 CosmoPower emulators (Bolliet et al.).
13
+ LCDM-equivalent cosmology is obtained by leaving ``fEDE = 0.001`` (the
14
+ default) — no separate emulator path needed.
15
+
16
+ Runtime deps: ``jax``, ``numpy``, ``mcfit``.
17
+
18
+ Quick start:
19
+
20
+ >>> import classy_szlite as csl
21
+ >>> cosmo = csl.CosmoParams()
22
+ >>> d = csl.derived(cosmo)
23
+ >>> ell_cls = csl.cl_TTTEEE(cosmo)
24
+ >>> k, pk = csl.Pk(cosmo, [0., 0.5, 1., 2.])
25
+ >>> Hz, chi, Da = csl.distances(cosmo, [0.1, 0.5, 1.0])
26
+ >>> profile = csl.ProfileParamsA10(P0=8.13, beta=5.48, B=1.25)
27
+ >>> import jax.numpy as jnp
28
+ >>> cl_1h, cl_2h = csl.cl_yy(cosmo, profile, ell=jnp.geomspace(2, 9000, 80))
29
+ >>> ev = csl.cl_yy_factory(cosmo, ell=jnp.geomspace(2, 9000, 80)) # fast path
30
+ >>> cl_1h, cl_2h = ev(profile)
31
+ """
32
+ __version__ = "0.2.0"
33
+
34
+ from .params import CosmoParams, ProfileParamsA10
35
+ from .api import (
36
+ derived,
37
+ cl_TTTEEE,
38
+ Pk, Pnl,
39
+ distances,
40
+ cl_yy,
41
+ cl_yy_factory,
42
+ cosmo_to_dict,
43
+ )
44
+
45
+ __all__ = [
46
+ "__version__",
47
+ "CosmoParams", "ProfileParamsA10",
48
+ "derived", "cl_TTTEEE", "Pk", "Pnl", "distances", "cl_yy", "cl_yy_factory",
49
+ "cosmo_to_dict",
50
+ ]
@@ -0,0 +1,180 @@
1
+ """Pure-JAX CosmoPower emulator forward pass.
2
+
3
+ Loads ``.npz`` files produced by the CosmoPower training pipeline and
4
+ exposes a JAX-traceable ``predict`` function. Supports two architectures:
5
+
6
+ 1. **Plain NN** (TT, DER, HZ, DAZ, PKL, PKNL, S8Z, etc.):
7
+ output = feature_train_std * NN(p_in) + feature_train_mean
8
+
9
+ 2. **NN + PCA** (TE and similar that decompress PCA coefficients):
10
+ output = ((NN(p_in) @ pca_transform_matrix) + pca_mean) * pca_std
11
+ * feature_train_std + feature_train_mean
12
+ (exact ordering inferred from cosmopower/classy_szfast convention)
13
+
14
+ The NN itself uses the "α-β sigmoid" activation introduced in
15
+ Spurio Mancini, Piras, Alsing et al. (2022, MNRAS 511, 1771) — a
16
+ trainable smooth interpolation between identity and sigmoid:
17
+
18
+ h_out = (β + sigmoid(α * z) * (1 - β)) * z
19
+
20
+ No keras / tensorflow dependency: the .npz files are read with numpy,
21
+ and the forward pass is a handful of jnp ops.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from dataclasses import dataclass, field
27
+ from typing import Any
28
+
29
+ import numpy as np
30
+ import jax
31
+ import jax.numpy as jnp
32
+
33
+ jax.config.update("jax_enable_x64", True)
34
+
35
+
36
+ def _sigmoid(x):
37
+ return 1.0 / (1.0 + jnp.exp(-x))
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Emulator:
42
+ """JAX-traceable CosmoPower emulator.
43
+
44
+ Hold the static weights as jnp arrays; ``predict(params_dict)`` runs
45
+ the forward pass and returns the output (already de-standardised, and
46
+ PCA-decompressed if applicable).
47
+
48
+ Attributes
49
+ ----------
50
+ parameters : list[str]
51
+ Input parameter names, in training order.
52
+ modes : np.ndarray
53
+ Output mode labels (ell, k-index, etc. — interpretation is
54
+ emulator-specific). Length = output dimension AFTER PCA decompression.
55
+ is_log : bool
56
+ If True, the emulator output is log10 of the physical quantity.
57
+ Caller can apply ``10**`` if needed; we leave that to the caller
58
+ because the convention varies (e.g. TE doesn't apply log10 since it
59
+ can go negative).
60
+ """
61
+
62
+ # JAX-side state
63
+ weights: tuple = field(repr=False) # tuple of (W, b) per layer
64
+ alphas: tuple = field(repr=False) # α per hidden layer
65
+ betas: tuple = field(repr=False) # β per hidden layer
66
+ param_mean: jnp.ndarray = field(repr=False)
67
+ param_std: jnp.ndarray = field(repr=False)
68
+ feature_mean: jnp.ndarray = field(repr=False)
69
+ feature_std: jnp.ndarray = field(repr=False)
70
+ pca_matrix: jnp.ndarray | None = field(repr=False, default=None)
71
+ pca_mean: jnp.ndarray | None = field(repr=False, default=None)
72
+ pca_std: jnp.ndarray | None = field(repr=False, default=None)
73
+
74
+ # Metadata
75
+ parameters: tuple[str, ...] = ()
76
+ modes: np.ndarray = field(default=None, repr=False)
77
+
78
+ @classmethod
79
+ def from_npz(cls, path: str) -> "Emulator":
80
+ """Load an emulator from a CosmoPower-style ``.npz`` file."""
81
+ raw = np.load(path, allow_pickle=True)
82
+ # CosmoPower saves a single object array containing a dict-like
83
+ d = raw["arr_0"].item()
84
+
85
+ # weights_ and biases_ are SEPARATE lists (one entry per layer);
86
+ # pair them up so the forward pass can iterate over (W, b) tuples.
87
+ weights = tuple(
88
+ (jnp.asarray(W, dtype=jnp.float64),
89
+ jnp.asarray(b, dtype=jnp.float64))
90
+ for W, b in zip(d["weights_"], d["biases_"])
91
+ )
92
+ # alphas_ / betas_: per hidden layer (one entry per hidden layer,
93
+ # i.e. len = n_layers - 1; final linear layer has no α-β activation)
94
+ alphas = tuple(jnp.asarray(a, dtype=jnp.float64) for a in d["alphas_"])
95
+ betas = tuple(jnp.asarray(b, dtype=jnp.float64) for b in d["betas_"])
96
+
97
+ param_mean = jnp.asarray(d["param_train_mean"], dtype=jnp.float64)
98
+ param_std = jnp.asarray(d["param_train_std"], dtype=jnp.float64)
99
+ feature_mean = jnp.asarray(d["feature_train_mean"], dtype=jnp.float64)
100
+ feature_std = jnp.asarray(d["feature_train_std"], dtype=jnp.float64)
101
+
102
+ # PCA fields are present only for PCA emulators (e.g. TE)
103
+ has_pca = "pca_transform_matrix" in d
104
+ pca_matrix = jnp.asarray(d["pca_transform_matrix"], dtype=jnp.float64) if has_pca else None
105
+ pca_mean = jnp.asarray(d["pca_mean"], dtype=jnp.float64) if has_pca else None
106
+ pca_std = jnp.asarray(d["pca_std"], dtype=jnp.float64) if has_pca else None
107
+
108
+ parameters = tuple(d["parameters"])
109
+ modes = np.asarray(d["modes"])
110
+
111
+ return cls(
112
+ weights=weights, alphas=alphas, betas=betas,
113
+ param_mean=param_mean, param_std=param_std,
114
+ feature_mean=feature_mean, feature_std=feature_std,
115
+ pca_matrix=pca_matrix, pca_mean=pca_mean, pca_std=pca_std,
116
+ parameters=parameters, modes=modes,
117
+ )
118
+
119
+ def predict(self, params_dict: dict) -> jnp.ndarray:
120
+ """Forward pass on a single or batched input.
121
+
122
+ ``params_dict``: maps each ``self.parameters`` name to a 1-D array-like
123
+ of length B (batch size); the output has shape ``(B, n_modes)`` (or
124
+ ``(n_modes,)`` if B=1).
125
+ """
126
+ # Stack inputs in the canonical order
127
+ p = jnp.stack([jnp.asarray(params_dict[name], dtype=jnp.float64)
128
+ for name in self.parameters], axis=-1) # (B, n_params)
129
+ # Standardise
130
+ h = (p - self.param_mean) / self.param_std
131
+
132
+ # Hidden layers with α-β sigmoid activation.
133
+ # CosmoPower .npz weights are stored as (in_dim, out_dim) so the
134
+ # matmul is `h @ W` (no transpose).
135
+ for (W, b), a, beta in zip(self.weights[:-1], self.alphas, self.betas):
136
+ z = h @ W + b
137
+ h = (beta + _sigmoid(a * z) * (1.0 - beta)) * z
138
+
139
+ # Final linear layer (no activation)
140
+ W, b = self.weights[-1]
141
+ preds = h @ W + b # (B, n_out)
142
+
143
+ # PCA decompression (if applicable)
144
+ if self.pca_matrix is not None:
145
+ # PCA-encoded: undo PCA standardisation, project back, then
146
+ # undo feature standardisation
147
+ preds = preds * self.pca_std + self.pca_mean # (B, n_pca)
148
+ preds = preds @ self.pca_matrix # (B, n_modes)
149
+
150
+ # Undo feature standardisation
151
+ preds = preds * self.feature_std + self.feature_mean
152
+ return preds.squeeze()
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Default data-directory resolution
157
+ # ---------------------------------------------------------------------------
158
+
159
+ def default_data_dir() -> str:
160
+ """Resolve the CosmoPower emulator data directory.
161
+
162
+ Resolution order:
163
+ 1. ``$CLASSY_SZLITE_DATA_DIR`` environment variable
164
+ 2. ``~/class_sz_data`` (the recommended new location)
165
+ 3. ``~/class_sz_data_directory`` (legacy classy_szfast path)
166
+ """
167
+ env = os.environ.get("CLASSY_SZLITE_DATA_DIR")
168
+ if env:
169
+ return os.path.expanduser(env)
170
+ for candidate in ("~/class_sz_data", "~/class_sz_data_directory"):
171
+ path = os.path.expanduser(candidate)
172
+ if os.path.isdir(path):
173
+ return path
174
+ raise RuntimeError(
175
+ "classy_szlite: cannot find CosmoPower emulator data. Set the "
176
+ "$CLASSY_SZLITE_DATA_DIR environment variable, or place the emulator "
177
+ "directory tree at ~/class_sz_data (or the legacy "
178
+ "~/class_sz_data_directory). Required subdirs per cosmo_model: "
179
+ "PK/, growth-and-distances/, TTTEEE/, derived-parameters/."
180
+ )
@@ -0,0 +1,94 @@
1
+ """Emulator file IDs, grid configs, and lazy loading.
2
+
3
+ Single emulator suite: **ede-v2**. The ede-v2 CosmoPower emulators are
4
+ extremely accurate and cover the LCDM-equivalent point at ``fEDE = 0.001``;
5
+ they replace any need for a separate LCDM emulator path.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from functools import lru_cache
11
+
12
+ from ._emulator import Emulator, default_data_dir
13
+
14
+
15
+ # File IDs (paths relative to <data_root>/ede/)
16
+ _EMULATOR_FILES = {
17
+ "tt": "TTTEEE/TT_v2.npz",
18
+ "te": "TTTEEE/TE_v2.npz",
19
+ "ee": "TTTEEE/EE_v2.npz",
20
+ "pp": "PP/PP_v2.npz",
21
+ "pkl": "PK/PKL_v2.npz",
22
+ "pknl": "PK/PKNL_v2.npz",
23
+ "hz": "growth-and-distances/HZ_v2.npz",
24
+ "daz": "growth-and-distances/DAZ_v2.npz",
25
+ "s8z": "growth-and-distances/S8Z_v2.npz",
26
+ "der": "derived-parameters/DER_v2.npz",
27
+ }
28
+
29
+ # Subdir under the data root
30
+ _DATA_SUBDIR = "ede"
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Pk emulator grid + extrapolation configuration
35
+ # ---------------------------------------------------------------------------
36
+ # Final n_k = nk // ndspl. Final k = geomspace(kmin, kmax, nk)[::ndspl].
37
+ # prefac:
38
+ # 'k3' — emulator output is log10[ k³ Pk ]; recover Pk via × k^-3
39
+ # extrap_kmin: extend the Pk grid down to this k via P(k) ∝ k^n_s
40
+ PK_GRID = dict(kmin=5e-4, kmax=10.0, nk=1000, ndspl=1,
41
+ prefac="k3", extrap_kmin=1e-4)
42
+
43
+ # Distance emulator z-grid: HZ trained on linspace(0, 20, 5000); DAZ omits
44
+ # z=0 (returns 4999 values) — we prepend chi(0)=0 in cosmology._predict_distances.
45
+ DIST_ZMAX = 20.0
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Per-emulator log10 convention
50
+ # ---------------------------------------------------------------------------
51
+ # Whether the raw NN output is log10(physical) vs physical directly.
52
+ # True → caller must apply 10^ to recover the physical quantity.
53
+ # False → raw output IS the physical quantity (or the prefactored-Pk form).
54
+ LOG10_OUTPUT = {
55
+ "tt": True, "ee": True, "pp": True, "te": False, # CMB
56
+ "pkl": True, "pknl": True, # Pk (handled in cosmology)
57
+ "hz": True, "daz": True, "s8z": True, # distances + sigma8(z)
58
+ "der": True, # derived (sigma_8 = der[1])
59
+ }
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Default cosmology parameters (matches ede-v2 emulator training default)
64
+ # ---------------------------------------------------------------------------
65
+ DEFAULT_COSMO = {
66
+ "ln10^{10}A_s": 3.047, "n_s": 0.9665,
67
+ "H0": 67.66, "omega_b": 0.02242, "omega_cdm": 0.11933,
68
+ "tau_reio": 0.054,
69
+ # EDE-specific defaults (LCDM-equivalent point)
70
+ "fEDE": 0.001, "log10z_c": 3.562, "thetai_scf": 2.83, "r": 0.0,
71
+ # ν convention: 3 degenerate ncdm of 0.02 eV → Σmν = 0.06 eV
72
+ "m_ncdm": 0.02, "N_ur": 0.00441,
73
+ }
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Lazy emulator loading + cache
78
+ # ---------------------------------------------------------------------------
79
+
80
+ @lru_cache(maxsize=16)
81
+ def get_emulator(name: str) -> Emulator:
82
+ """Load (and cache) the named emulator."""
83
+ if name not in _EMULATOR_FILES:
84
+ raise ValueError(f"Unknown emulator name {name!r}. "
85
+ f"Supported: {list(_EMULATOR_FILES)}")
86
+ root = default_data_dir()
87
+ path = os.path.join(root, _DATA_SUBDIR, _EMULATOR_FILES[name])
88
+ if not os.path.isfile(path):
89
+ raise FileNotFoundError(f"classy_szlite: emulator file not found: {path}")
90
+ return Emulator.from_npz(path)
91
+
92
+
93
+ def output_is_log10(name: str) -> bool:
94
+ return LOG10_OUTPUT[name]
classy_szlite/api.py ADDED
@@ -0,0 +1,194 @@
1
+ """High-level public API: derived params, CMB Cls, matter Pk, distances,
2
+ tSZ Cl^yy. All functions take a :class:`classy_szlite.params.CosmoParams`.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import numpy as np
7
+ import jax
8
+ import jax.numpy as jnp
9
+
10
+ from ._registry import get_emulator, DEFAULT_COSMO
11
+ from .params import CosmoParams, ProfileParamsA10
12
+ from .cosmology import build as build_cosmo_grids, get_pk, get_pknl, get_distances
13
+ from .hmf import build_halo_grids
14
+ from .power_spectrum import cl_yy_1h_2h
15
+
16
+ jax.config.update("jax_enable_x64", True)
17
+
18
+
19
+ def cosmo_to_dict(cosmo: CosmoParams) -> dict:
20
+ """Convert ``CosmoParams`` to the emulator-style dict (with curly-brace key).
21
+
22
+ Values pass through as-is (float or jax.Array) so the returned dict is
23
+ JAX-traceable — pass a ``CosmoParams`` of tracers to ``jax.grad``.
24
+ """
25
+ return {
26
+ "omega_b": cosmo.omega_b,
27
+ "omega_cdm": cosmo.omega_cdm,
28
+ "H0": cosmo.H0,
29
+ "tau_reio": cosmo.tau_reio,
30
+ "ln10^{10}A_s": cosmo.ln10_10_As,
31
+ "n_s": cosmo.n_s,
32
+ "m_ncdm": cosmo.m_ncdm,
33
+ "N_ur": cosmo.N_ur,
34
+ "fEDE": cosmo.fEDE,
35
+ "log10z_c": cosmo.log10z_c,
36
+ "thetai_scf": cosmo.thetai_scf,
37
+ "r": cosmo.r,
38
+ }
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Derived parameters (σ8, Ω_m, S8)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def derived(cosmo: CosmoParams) -> dict:
46
+ """Derived parameters: ``sigma_8``, ``Omega_m``, ``S8``.
47
+
48
+ Also returns the full 17-element DER emulator output as ``'der_full'``
49
+ (σ8 is at index 1; consult the CosmoPower DER training script for the
50
+ full list).
51
+ """
52
+ em = get_emulator('der')
53
+ full = dict(DEFAULT_COSMO); full.update(cosmo_to_dict(cosmo))
54
+ p_in = {k: [full[k]] for k in em.parameters}
55
+ der = 10.0 ** np.asarray(em.predict(p_in)).flatten()
56
+
57
+ sigma_8 = float(der[1])
58
+ h = float(cosmo.H0) / 100.0
59
+ # Σmν = 3 × m_ncdm for ede-v2 ν convention (3 degenerate ν)
60
+ sum_mnu = 3.0 * float(cosmo.m_ncdm)
61
+ Omega_m = (float(cosmo.omega_b) + float(cosmo.omega_cdm)
62
+ + sum_mnu / 93.14) / h ** 2
63
+ S8 = sigma_8 * (Omega_m / 0.3) ** 0.5
64
+ return {"sigma_8": sigma_8, "Omega_m": Omega_m, "S8": S8, "der_full": der}
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # CMB angular power spectra
69
+ # ---------------------------------------------------------------------------
70
+
71
+ # TT, EE, PP emulators output log10(prefactored Cl); TE outputs Cl directly
72
+ # (it can be negative). Recovery factor: ede-v2 uses 1/ell² to get raw Cl.
73
+ _CMB_LOG_CONVENTION = {"tt": True, "ee": True, "pp": True, "te": False}
74
+
75
+
76
+ def cl_TTTEEE(cosmo: CosmoParams,
77
+ spectra: tuple[str, ...] = ("tt", "te", "ee"),
78
+ ell_factor: bool = True) -> dict:
79
+ """CMB angular power spectra.
80
+
81
+ Returns a dict with keys ``'ell'`` and the requested spectra
82
+ (``'tt','te','ee'``). Values are **dimensionless** — multiply by
83
+ ``Tcmb_uK² = (2.7255e6)²`` to convert to μK².
84
+
85
+ ``ell_factor`` (default ``True``) — return ``D_ell = ell(ell+1) Cl / (2π)``;
86
+ ``False`` returns raw Cl.
87
+ """
88
+ full = dict(DEFAULT_COSMO); full.update(cosmo_to_dict(cosmo))
89
+
90
+ out = {}
91
+ ell = None
92
+ for spec in spectra:
93
+ if spec not in _CMB_LOG_CONVENTION:
94
+ raise ValueError(f"Unknown spectrum {spec!r}. Pick from {tuple(_CMB_LOG_CONVENTION)}.")
95
+ em = get_emulator(spec)
96
+ p_in = {k: [full[k]] for k in em.parameters}
97
+ pred = np.asarray(em.predict(p_in)).flatten()
98
+ if _CMB_LOG_CONVENTION[spec]:
99
+ pred = 10.0 ** pred
100
+ out[spec] = pred
101
+ if ell is None:
102
+ ell = np.asarray(em.modes)
103
+
104
+ # ede-v2 recovery factor: raw → Cl
105
+ factor_to_Cl = 1.0 / (ell ** 2)
106
+ for s in spectra:
107
+ out[s] = out[s] * factor_to_Cl
108
+
109
+ out["ell"] = ell
110
+ if ell_factor:
111
+ fac_dl = ell * (ell + 1) / (2.0 * np.pi)
112
+ for s in spectra:
113
+ out[s] = out[s] * fac_dl
114
+ return out
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Pk, Pnl, distances
119
+ # ---------------------------------------------------------------------------
120
+
121
+ def Pk(cosmo: CosmoParams, z_arr):
122
+ """Linear P(k, z) — returns ``(k, pk(z, k))``."""
123
+ return get_pk(cosmo_to_dict(cosmo), z_arr)
124
+
125
+
126
+ def Pnl(cosmo: CosmoParams, z_arr):
127
+ """Non-linear P(k, z) (HMcode) — returns ``(k, pk(z, k))``."""
128
+ return get_pknl(cosmo_to_dict(cosmo), z_arr)
129
+
130
+
131
+ def distances(cosmo: CosmoParams, z_arr):
132
+ """Returns ``(Hz, chi, Da)``. ``Hz`` is H(z)/c in 1/Mpc; distances in Mpc."""
133
+ return get_distances(cosmo_to_dict(cosmo), z_arr)
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # tSZ Cl^yy (halo-model, Arnaud 2010 profile)
138
+ # ---------------------------------------------------------------------------
139
+
140
+ def cl_yy(cosmo: CosmoParams, profile: ProfileParamsA10, ell,
141
+ z_grid: jax.Array | None = None,
142
+ n_z: int = 100, m_min: float = 1e10, m_max: float = 3.5e15,
143
+ n_m: int = 200, delta_crit: float = 500.0):
144
+ """Halo-model tSZ angular power spectrum (full pipeline per call).
145
+
146
+ Returns ``(cl_1h, cl_2h)`` — dimensionless C_ell. Multiply by
147
+ ``ell*(ell+1)/(2π)*1e12`` to get ``D_ell × 1e12``.
148
+
149
+ For MCMC sampling only profile parameters at fixed cosmology, use
150
+ :func:`cl_yy_factory` instead — ~3× faster.
151
+ """
152
+ if z_grid is None:
153
+ z_grid = jnp.geomspace(0.005, 3.0, n_z)
154
+ cosmo_dict = cosmo_to_dict(cosmo)
155
+ cg = build_cosmo_grids(cosmo_dict, z_grid=z_grid)
156
+ hg = build_halo_grids(cg, cosmo_dict, delta_crit=delta_crit,
157
+ m_min=m_min, m_max=m_max, n_m=n_m)
158
+ pp_dict = profile._asdict()
159
+ cl_1h, cl_2h = cl_yy_1h_2h(jnp.asarray(ell), cg, hg, cosmo_dict,
160
+ profile='arnaud10', profile_params=pp_dict)
161
+ return cl_1h, cl_2h
162
+
163
+
164
+ def cl_yy_factory(cosmo: CosmoParams, ell,
165
+ z_grid: jax.Array | None = None,
166
+ n_z: int = 100, m_min: float = 1e10, m_max: float = 3.5e15,
167
+ n_m: int = 200, delta_crit: float = 500.0):
168
+ """Fixed-cosmology fast-path: precompute the heavy bits, get a closure.
169
+
170
+ Builds ``CosmoGrids`` (emulators → P_lin, distances, σ(R)) and
171
+ ``HaloGrids`` (Tinker 08 HMF, bias) **once**, then returns:
172
+
173
+ ev(profile) -> (cl_1h, cl_2h)
174
+
175
+ A subsequent ``ev(profile)`` call only runs the ``cl_yy_1h_2h``
176
+ halo-model integration — typically ~5 ms per call. Intended for MCMC
177
+ over profile / nuisance parameters with fixed cosmology.
178
+ """
179
+ if z_grid is None:
180
+ z_grid = jnp.geomspace(0.005, 3.0, n_z)
181
+ cosmo_dict = cosmo_to_dict(cosmo)
182
+ cg = build_cosmo_grids(cosmo_dict, z_grid=z_grid)
183
+ hg = build_halo_grids(cg, cosmo_dict, delta_crit=delta_crit,
184
+ m_min=m_min, m_max=m_max, n_m=n_m)
185
+ ell_jax = jnp.asarray(ell)
186
+
187
+ def evaluate(profile: ProfileParamsA10):
188
+ cl_1h, cl_2h = cl_yy_1h_2h(
189
+ ell_jax, cg, hg, cosmo_dict,
190
+ profile='arnaud10', profile_params=profile._asdict(),
191
+ )
192
+ return cl_1h, cl_2h
193
+
194
+ return evaluate