diff --git a/.gitignore b/.gitignore index e6772628..e4cd74a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ # soliket specific chains/ +# local tooling (Claude Code, git worktrees) +.claude/ +.worktrees/ + +# ISO_sims generated datasets / cross-covariance (reproducible from the notebooks) +notebooks/ISO_sims/sims/ +notebooks/ISO_sims/XCov_mflike_lensing.fits + # macos .DS_Store .tmp @@ -116,3 +124,6 @@ venv.bak/ # git worktrees .worktrees/ + +CONTEXT.md +/notebooks/test_chains diff --git a/examples/ISO_sims.yaml b/examples/ISO_sims.yaml new file mode 100644 index 00000000..2c0bced4 --- /dev/null +++ b/examples/ISO_sims.yaml @@ -0,0 +1,232 @@ +# ISO_sims joint analysis -- YAML twin of analyse_datasets.ipynb. +# Replace with your SOLikeT checkout and with your cobaya +# packages path, regenerate the sims/ twins (create_datasets.ipynb + +# create_cross_covariance.ipynb), then run: cobaya-run examples/ISO_sims.yaml + +likelihood: + soliket.gaussian.MultiGaussianLikelihood: + components: + - mflike.TTTEEE + - soliket.LensingLikelihood + stop_at_error: true + options: + - input_file: /notebooks/ISO_sims/sims/mflike_smooth.fits + cov_Bbl_file: data_sacc_w_covar_and_Bbl.fits + - theory_lmax: 5000 + data_folder: /notebooks/ISO_sims/sims + data_filename: lensing_smooth.sacc.fits + correction_filename: /data/LensingLikelihood/corrections_lensing.sacc.fits + fiducial_filename: /data/LensingLikelihood/fiducial_lensing.sacc.fits + cross_cov_path: /notebooks/ISO_sims/sims/XCov_mflike_lensing.fits +theory: + camb: + extra_args: + lens_potential_accuracy: 1 + WantTransfer: true + Transfer.high_precision: true + Transfer.kmax: 1.2 + kmax: 0.9 + num_nu_massless: 1.044 + num_nu_massive: 2 + nu_mass_eigenstates: 2 + nu_mass_fractions: + - 0.14763410387308012 + - 0.8523658961269198 + nu_mass_numbers: + - 1 + - 1 + share_delta_neff: true + stop_at_error: false + mflike.BandpowerForeground: + stop_at_error: true +sampler: + mcmc: + max_samples: 50 + Rminus1_stop: 0.1 +params: + H0: + latex: H_0 + value: 67.7 + logA: + drop: true + latex: \log(10^{10} A_\mathrm{s}) + value: 3.05 + As: + value: 'lambda logA: 1e-10*np.exp(logA)' + latex: A_\mathrm{s} + ombh2: + latex: \Omega_\mathrm{b} h^2 + value: 0.0224 + renames: + - omegabh2 + omch2: + latex: \Omega_c h^2 + value: 0.1202 + renames: + - omegach2 + ns: + latex: n_s + value: 0.9649 + tau: + prior: + dist: norm + loc: 0.0544 + scale: 0.0073 + ref: + dist: norm + loc: 0.0544 + scale: 0.0073 + proposal: 0.0073 + latex: \tau_\mathrm{reio} + Alens: + value: 1.0 + latex: A_\mathrm{lens} + omega_de: + derived: true + latex: \Omega_\Lambda + renames: + - omegal + omegam: + derived: true + latex: \Omega_\mathrm{m} + omegamh2: + derived: 'lambda omegam, H0: omegam*(H0/100)**2' + latex: \Omega_\mathrm{m} h^2 + sigma8: + derived: true + latex: \sigma_8 + mnu1: + value: 0.0 + drop: true + mnu2: + value: 'lambda: np.sqrt(7.5e-5)' + drop: true + mnu3: + value: 'lambda: np.sqrt(2.5e-3)' + drop: true + mnu: + value: 'lambda mnu1, mnu2, mnu3: mnu1 + mnu2 + mnu3' + latex: \sum m_\nu + nnu: + value: 3.044 + latex: N_\mathrm{eff} + a_tSZ: + latex: a_\mathrm{tSZ} + value: 3.3 + a_kSZ: + latex: a_\mathrm{kSZ} + value: 1.6 + a_p: + latex: a_p + value: 6.9 + beta_p: + latex: \beta_p + value: 2.2 + a_c: + latex: a_c + value: 4.9 + beta_c: + latex: \beta_c + value: 2.2 + a_s: + latex: a_s + value: 3.1 + a_gtt: + latex: a_\mathrm{dust}^\mathrm{TT} + value: 2.8 + a_gte: + latex: a_\mathrm{dust}^\mathrm{TE} + value: 0.1 + a_gee: + latex: a_\mathrm{dust}^\mathrm{EE} + value: 0.1 + a_psee: + latex: a_\mathrm{ps}^\mathrm{EE} + value: 0 + a_pste: + latex: a_\mathrm{ps}^\mathrm{TE} + value: 0 + xi: + latex: \xi + value: 0.1 + alpha_tSZ: + latex: \alpha_\mathrm{tSZ} + value: 0 + beta_s: + latex: \beta_s + value: -2.5 + renames: + - nrunrun + T_d: + value: 9.7 + latex: T_d + T_effd: + value: 19.6 + latex: T_{\mathrm{dust},\mathrm{eff}} + beta_d: + value: 1.5 + latex: \beta_\mathrm{dust} + alpha_s: + value: 1.0 + latex: \alpha_s + renames: + - nrun + alpha_p: + value: 1.0 + latex: \alpha_p + alpha_dT: + value: -0.6 + latex: \alpha_{\mathrm{dust},T} + alpha_dE: + value: -0.4 + latex: \alpha_{\mathrm{dust},E} + bandint_shift_LAT_93: + latex: \Delta_{\rm band}^{93} + value: 0.0 + bandint_shift_LAT_145: + latex: \Delta_{\rm band}^{145} + value: 0.0 + bandint_shift_LAT_225: + latex: \Delta_{\rm band}^{225} + value: 0.0 + cal_LAT_93: + latex: \mathrm{Cal}^{93} + value: 1 + cal_LAT_145: + latex: \mathrm{Cal}^{145} + value: 1 + cal_LAT_225: + latex: \mathrm{Cal}^{225} + value: 1 + calT_LAT_93: + value: 1 + latex: \mathrm{Cal}_{\rm T}^{93} + calE_LAT_93: + latex: \mathrm{Cal}_{\rm E}^{93} + value: 1 + calT_LAT_145: + value: 1 + latex: \mathrm{Cal}_{\rm T}^{145} + calE_LAT_145: + latex: \mathrm{Cal}_{\rm E}^{145} + value: 1 + calT_LAT_225: + value: 1 + latex: \mathrm{Cal}_{\rm T}^{225} + calE_LAT_225: + latex: \mathrm{Cal}_{\rm E}^{225} + value: 1 + calG_all: + value: 1 + latex: \mathrm{Cal}_{\rm G}^{\rm All} + alpha_LAT_93: + value: 0 + latex: \alpha^{93} + alpha_LAT_145: + value: 0 + latex: \alpha^{145} + alpha_LAT_225: + value: 0 + latex: \alpha^{225} +packages_path: +output: chains/ISO_sims diff --git a/notebooks/ISO_sims/analyse_datasets.ipynb b/notebooks/ISO_sims/analyse_datasets.ipynb new file mode 100644 index 00000000..1ad384d7 --- /dev/null +++ b/notebooks/ISO_sims/analyse_datasets.ipynb @@ -0,0 +1,273 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Analysing a joint dataset\n", + "\n", + "Step 3 of the ISO pipeline: an end-to-end joint analysis of the primary CMB (MFLike TT/TE/EE) and the\n", + "CMB-lensing reconstruction, fitting **exactly the products built upstream**:\n", + "\n", + "1. build the joint likelihood on the smooth datasets from\n", + " [`create_datasets.ipynb`](create_datasets.ipynb) and evaluate it at the fiducial point;\n", + "2. fold in the **cross-covariance** from\n", + " [`create_cross_covariance.ipynb`](create_cross_covariance.ipynb);\n", + "3. launch an MCMC.\n", + "\n", + "We work at the `build_info` layer rather than `quickstart`, so the configuration stays an ordinary\n", + "Cobaya `info` dict with the dataset, accuracy and cosmology as explicit, editable knobs;\n", + "`resolve_aliases` recovers the role aliases (`.mflike`, `.lensing`). Because both smooth twins were\n", + "imprinted at the same ISO fiducial we fit at, the joint chi-square is **0 by construction**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **ISO-local overrides.** All three notebooks pass `defaults_dir=\"defaults\"` to `build_info`, so the\n", + "> fiducial comes from the YAMLs in `ISO_sims/defaults/`: each file per-file *replaces* its packaged\n", + "> `soliket/presets/defaults/` counterpart, the rest fall back to the package. Here `cosmo.yaml`\n", + "> (cosmology, incl. the `mnu*` neutrino mass sum) and `theory.yaml` (the camb neutrino `extra_args`\n", + "> for the SO **2-eigenstate normal hierarchy**) are overridden; foreground and systematics fall back.\n", + "> The NH override is **camb-only** and needs no Python — `build_info` applies it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. A joint likelihood on the smooth datasets\n", + "\n", + "`build_info(\"multigaussian\")` returns the `info` for a single `MultiGaussianLikelihood` wiring\n", + "MFLike + CMB lensing onto one shared CAMB theory. We point each component at its smooth twin in\n", + "`sims/`: the MFLike `input_file` at `mflike_smooth.fits`, and the lensing `data_folder` at `sims/`\n", + "(its multi-hundred-MB `correction`/`fiducial` aux files are referenced by **absolute** path from the\n", + "shipped install, so we never copy them). `iso_multigaussian` centralises that wiring so the three\n", + "build points below stay identical." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from pathlib import Path\n", + "\n", + "from cobaya.model import get_model\n", + "from cobaya.tools import resolve_packages_path\n", + "\n", + "from soliket.presets import build_info, resolve_aliases\n", + "\n", + "MGL = \"soliket.gaussian.MultiGaussianLikelihood\"\n", + "PACKAGES = resolve_packages_path()\n", + "\n", + "# Inline artifact paths, matching create_datasets.ipynb / create_cross_covariance.ipynb.\n", + "SIMS = Path(\"sims\")\n", + "MFLIKE_SMOOTH = SIMS / \"mflike_smooth.fits\"\n", + "LENSING_SMOOTH = SIMS / \"lensing_smooth.sacc.fits\"\n", + "XCOV = SIMS / \"XCov_mflike_lensing.fits\"\n", + "LENS_SHIPPED = os.path.join(PACKAGES, \"data\", \"LensingLikelihood\")\n", + "\n", + "\n", + "def loglike(model):\n", + " \"\"\"Total log-likelihood of a model at its fiducial point.\"\"\"\n", + " return float(sum(model.loglikes({})[0]))\n", + "\n", + "\n", + "def iso_multigaussian(sample=None, cross_cov_path=None):\n", + " \"\"\"Joint MFLike + lensing info at the ISO fiducial, wired to fit the sims/ twins.\"\"\"\n", + " info = build_info(\"multigaussian\", sample=sample, defaults_dir=\"defaults\")\n", + " info[\"packages_path\"] = PACKAGES\n", + " opts = info[\"likelihood\"][MGL][\"options\"]\n", + " # MFLike component -> smooth CMB+fg data (absolute input_file; reuse shipped cov/Bbl).\n", + " opts[0][\"input_file\"] = str(MFLIKE_SMOOTH.resolve())\n", + " # Lensing component -> smooth twin in sims/; corrections + fiducial stay in the\n", + " # shipped folder (absolute filenames bypass data_folder joining).\n", + " opts[1][\"data_folder\"] = str(SIMS.resolve())\n", + " opts[1][\"data_filename\"] = LENSING_SMOOTH.name\n", + " opts[1][\"correction_filename\"] = os.path.join(\n", + " LENS_SHIPPED, \"corrections_lensing.sacc.fits\"\n", + " )\n", + " opts[1][\"fiducial_filename\"] = os.path.join(\n", + " LENS_SHIPPED, \"fiducial_lensing.sacc.fits\"\n", + " )\n", + " if cross_cov_path is not None:\n", + " info[\"likelihood\"][MGL][\"cross_cov_path\"] = str(cross_cov_path)\n", + " # --- editable knobs ----------------------------------------------------\n", + " # info[\"theory\"][\"camb\"][\"extra_args\"][\"lens_potential_accuracy\"] = 4 # accuracy\n", + " # -----------------------------------------------------------------------\n", + " return info\n", + "\n", + "\n", + "info = iso_multigaussian()\n", + "model = get_model(info)\n", + "roles = resolve_aliases(model)\n", + "\n", + "print(\"members :\", type(roles.mflike).__name__, \"+\", type(roles.lensing).__name__)\n", + "print(\"joint loglike at fiducial:\", round(loglike(model), 4), \"(chi^2 = 0 twins)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Adding the cross-covariance\n", + "\n", + "Both probes look at the same sky, so their errors are correlated. The physical cross-covariance is the\n", + "`sims/XCov_mflike_lensing.fits` produced by [`create_cross_covariance.ipynb`](create_cross_covariance.ipynb)\n", + "via `CrossCov.from_cmb_lensing(roles.mflike, roles.lensing)`. It keys the block by the components' real\n", + "names and carries their auto-covariances, so it drops straight into the likelihood through\n", + "`cross_cov_path`. We rebuild from the same wiring with that one extra option and compare the joint\n", + "log-likelihood with and without the cross term." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if not XCOV.is_file():\n", + " print(\"Cross-covariance not found:\", XCOV)\n", + " print(\"Run create_cross_covariance.ipynb (RUN_FULL = True) first to produce it.\")\n", + "else:\n", + " model_xcov = get_model(iso_multigaussian(cross_cov_path=XCOV))\n", + " print(\"loglike without cross-cov:\", round(loglike(model), 4))\n", + " print(\"loglike with cross-cov:\", round(loglike(model_xcov), 4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Running an MCMC\n", + "\n", + "Sampling is Python-native. Passing `sample=[...]` to `build_info` turns the named dual parameters into\n", + "sampled ones (they get their priors back); add a `sampler` block and hand the `info` to `cobaya.run`.\n", + "The cell below sets up a short chain over `tau`, guarded by `RUN_MCMC` so the notebook runs\n", + "top-to-bottom without launching a multi-minute sampler." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "RUN_MCMC = False # set True to launch the sampler (minutes)\n", + "\n", + "info_mcmc = iso_multigaussian(\n", + " sample=[\"tau\"], cross_cov_path=XCOV if XCOV.is_file() else None\n", + ")\n", + "\n", + "sampled = [\n", + " p for p, v in info_mcmc[\"params\"].items() if isinstance(v, dict) and \"prior\" in v\n", + "]\n", + "print(\"sampled parameters:\", sampled)\n", + "\n", + "if RUN_MCMC:\n", + " from cobaya import run\n", + "\n", + " info_mcmc[\"sampler\"] = {\"mcmc\": {\"max_samples\": 50, \"Rminus1_stop\": 0.1}}\n", + " info_mcmc[\"output\"] = \"chains/multigaussian\"\n", + " updated_info, sampler = run(info_mcmc)\n", + " print(\"done:\", sampler.products()[\"sample\"].shape)\n", + "else:\n", + " print(\"RUN_MCMC is False - skipping the sampler.\")" + ] + }, + { + "cell_type": "markdown", + "id": "61c73cbd", + "metadata": {}, + "source": [ + "### A YAML twin for `cobaya-run`\n", + "\n", + "The Python run above can be serialized to YAML and launched from the command line. We write the joint MCMC `info` to `examples/ISO_sims.yaml`, templating the machine-specific roots as `` (your SOLikeT checkout) and `` (your cobaya packages path) so the file is committable. Fill them in, regenerate the `sims/` twins, and run:\n", + "\n", + "```bash\n", + "cobaya-run examples/ISO_sims.yaml\n", + "```" + ] + }, + { + "cell_type": "code", + "id": "63433477", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "EXAMPLES = Path(\"../../examples\")\n", + "EXAMPLES.mkdir(exist_ok=True)\n", + "REPO = Path(\"../..\").resolve()\n", + "\n", + "# YAML twin of the Python MCMC run above. Machine-specific roots are templated as\n", + "# (this checkout) and (the cobaya packages path) so the file is\n", + "# portable and committable.\n", + "info_yaml = iso_multigaussian(\n", + " sample=[\"tau\"], cross_cov_path=XCOV.resolve() if XCOV.is_file() else None\n", + ")\n", + "info_yaml[\"sampler\"] = {\"mcmc\": {\"max_samples\": 50, \"Rminus1_stop\": 0.1}}\n", + "info_yaml[\"output\"] = \"chains/ISO_sims\"\n", + "\n", + "header = (\n", + " \"# ISO_sims joint analysis -- YAML twin of analyse_datasets.ipynb.\\n\"\n", + " \"# Replace with your SOLikeT checkout and with your cobaya\\n\"\n", + " \"# packages path, regenerate the sims/ twins (create_datasets.ipynb +\\n\"\n", + " \"# create_cross_covariance.ipynb), then run: cobaya-run examples/ISO_sims.yaml\\n\\n\"\n", + ")\n", + "text = yaml.dump(info_yaml, sort_keys=False, default_flow_style=False)\n", + "text = text.replace(str(REPO), \"\").replace(str(PACKAGES), \"\")\n", + "\n", + "dest = EXAMPLES / \"ISO_sims.yaml\"\n", + "dest.write_text(header + text)\n", + "print(\"wrote\", dest.resolve())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recap\n", + "\n", + "- `iso_multigaussian()` gives the joint MFLike + CMB-lensing `info` at the ISO fiducial, wired to fit\n", + " the smooth twins in `sims/`; `resolve_aliases(model)` recovers `.mflike` / `.lensing`.\n", + "- The cross-covariance from [`create_cross_covariance.ipynb`](create_cross_covariance.ipynb) folds in\n", + " through `cross_cov_path`.\n", + "- Sampling runs by adding a `sampler` block and calling `cobaya.run(info)`;\n", + " the same config is written to `examples/ISO_sims.yaml` (templated paths) for `cobaya-run`.\n", + "\n", + "This closes the pipeline: [`create_datasets.ipynb`](create_datasets.ipynb) builds the data,\n", + "[`create_cross_covariance.ipynb`](create_cross_covariance.ipynb) the cross-covariance, and this\n", + "notebook the joint fit — all at one ISO fiducial, all through `sims/`. `quickstart(\"multigaussian\")`\n", + "does the lazy one-call version when you want convenience over the knobs." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "soliket", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/ISO_sims/create_cross_covariance.ipynb b/notebooks/ISO_sims/create_cross_covariance.ipynb new file mode 100644 index 00000000..af84fc11 --- /dev/null +++ b/notebooks/ISO_sims/create_cross_covariance.ipynb @@ -0,0 +1,421 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-000", + "metadata": {}, + "source": [ + "# Building a cross-covariance\n", + "\n", + "When two SOLikeT likelihoods analyse data drawn from the *same* sky, their measurement errors are\n", + "correlated. A joint analysis therefore needs the **cross-covariance** between their data vectors, not\n", + "just the two auto-covariances. This notebook builds the cross-covariance between the primary CMB\n", + "(MFLike TT/TE/EE) and the CMB-lensing reconstruction, which is dominated by the fact that lensing\n", + "smooths the primary acoustic peaks.\n", + "\n", + "`soliket.cross_covariance` provides two layers:\n", + "\n", + "- a **one-liner**, `CrossCov.from_cmb_lensing(session)`, that pulls everything it needs from a\n", + " `Session` and returns a ready-to-save `CrossCov`;\n", + "- the **low-level kernels and builders** (`cmb_lensing_crosscov`, `lensing_induced_cov`,\n", + " `shear_kappa_crosscov`, `camb_lensing_derivatives`, ...) for finer control.\n", + "\n", + "The result is consumed by [`analyse_datasets.ipynb`](analyse_datasets.ipynb), which feeds it into a\n", + "`MultiGaussianLikelihood`." + ] + }, + { + "cell_type": "markdown", + "id": "cell-001", + "metadata": {}, + "source": [ + "## 1. The one-liner (full accuracy)\n", + "\n", + "On real MFLike data the cross-covariance is driven by the CAMB *lensed-Cl derivative*\n", + "`d C_ell^XY / d C_L^phiphi`, evaluated at the MFLike `lmax` (~9000). That derivative is a dense\n", + "`(4, lmax+1, lmax+1)` array of roughly **3 GB** and takes several minutes. Since the three notebooks\n", + "are meant to run as one analysis, the cell below ships with `RUN_FULL = True` and writes the real\n", + "cross-covariance into `sims/` for the analysis to consume; set it to `False` to skip the heavy\n", + "derivative on a memory-constrained machine.\n", + "\n", + "`CrossCov.from_cmb_lensing` takes the two evaluated likelihoods directly (`roles.mflike`,\n", + "`roles.lensing` from `resolve_aliases` — the concrete handles, not a `Session`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-002", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "RUN_FULL = True # set False to skip the heavy (~3 GB, minutes) CAMB derivative\n", + "\n", + "SIMS = Path(\"sims\")\n", + "SIMS.mkdir(exist_ok=True)\n", + "XCOV = SIMS / \"XCov_mflike_lensing.fits\"\n", + "\n", + "if RUN_FULL:\n", + " from cobaya.model import get_model\n", + " from cobaya.tools import resolve_packages_path\n", + "\n", + " from soliket.gaussian.gaussian_data import CrossCov\n", + " from soliket.presets import build_info, resolve_aliases\n", + "\n", + " # ISO fiducial via the override folder; build the model and take the named\n", + " # roles (the concrete handles, not a Session).\n", + " info = build_info(\"multigaussian\", defaults_dir=\"defaults\")\n", + " info[\"packages_path\"] = resolve_packages_path()\n", + " model = get_model(info)\n", + " model.loglikes({}) # evaluate CMB + lensing at the fiducial point\n", + " roles = resolve_aliases(model)\n", + "\n", + " # full-accuracy CAMB derivative (~3 GB, minutes)\n", + " xcov = CrossCov.from_cmb_lensing(roles.mflike, roles.lensing)\n", + " xcov.save(str(XCOV))\n", + " cmb, lensing = xcov.component_names # (\"mflike\", \"CMB Lensing\")\n", + " print(\"saved cross-covariance\", XCOV.name, \"block:\", xcov[(cmb, lensing)].shape)\n", + "else:\n", + " print(\"RUN_FULL is False - skipping the full-accuracy CAMB derivative.\")" + ] + }, + { + "cell_type": "markdown", + "id": "3f2d0da3", + "metadata": {}, + "source": [ + "## 2. Validating the saved product\n", + "\n", + "A cheap sanity-check on the **actual** cross-covariance written above (no recomputation): load it back\n", + "and confirm it is self-consistent and usable in a joint fit — the cross block matches the two auto\n", + "blocks, the auto-covariances are symmetric, and the assembled joint covariance is finite and\n", + "invertible. We also report how strongly the two probes are correlated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a51a296", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "\n", + "from soliket.gaussian.gaussian_data import CrossCov\n", + "\n", + "XCOV = Path(\"sims\") / \"XCov_mflike_lensing.fits\"\n", + "\n", + "xcov = CrossCov.load(str(XCOV))\n", + "cmb, lensing = xcov.component_names\n", + "A = np.asarray(xcov[(cmb, cmb)]) # MFLike auto-covariance\n", + "B = np.asarray(xcov[(lensing, lensing)]) # CMB-lensing auto-covariance\n", + "X = np.asarray(xcov[(cmb, lensing)]) # the CMB x lensing cross block\n", + "\n", + "print(\"components :\", xcov.component_names)\n", + "print(\"auto blocks :\", A.shape, \"+\", B.shape)\n", + "print(\"cross block :\", X.shape, \"(expect\", (A.shape[0], B.shape[0]), \")\")\n", + "\n", + "# Structural checks: the saved product must be self-consistent and usable.\n", + "assert X.shape == (A.shape[0], B.shape[0]), \"cross block does not match the auto blocks\"\n", + "for name, M in [(cmb, A), (lensing, B)]:\n", + " assert np.allclose(M, M.T), f\"{name} auto-covariance is not symmetric\"\n", + "full = np.block([[A, X], [X.T, B]])\n", + "assert np.all(np.isfinite(full)), \"joint covariance has non-finite entries\"\n", + "np.linalg.inv(full) # raises LinAlgError if singular -> unusable in the fit\n", + "\n", + "print(\n", + " \"joint cov :\",\n", + " full.shape,\n", + " \"| min eig: {:.2e}\".format(np.linalg.eigvalsh(full).min()),\n", + " \"| invertible: yes\",\n", + ")\n", + "\n", + "# How strongly are the two probes correlated? (dimensionless correlation block)\n", + "corr = X / np.sqrt(np.outer(np.diag(A), np.diag(B)))\n", + "print(\"max |corr(CMB, lensing)| : {:.3f}\".format(np.abs(corr).max()))" + ] + }, + { + "cell_type": "markdown", + "id": "5a84fa43", + "metadata": {}, + "source": [ + "### What the per-bandpower labels buy you\n", + "\n", + "The saved product is more than a matrix: every block carries the **identity** of each bandpower it\n", + "spans. When `MultiGaussianLikelihood` assembles the joint covariance, `CrossCov.to_canonical` uses\n", + "these labels to realign each block to the data *by identity* — so the cross-covariance stays correct\n", + "even when a probe's data vector is in a different order than the block was built in (a reordered\n", + "`cov_Bbl`, TE/ET folding) or carries different scale cuts. The build order is no longer load-bearing;\n", + "the cross-cov can be produced in any order and is reshuffled to the canonical theory order on load." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75271228", + "metadata": {}, + "outputs": [], + "source": [ + "# The MFLike rows are labelled in mflike's own vocabulary (spectrum, channel pair,\n", + "# ell); the lensing columns in the SACC's (data_type, tracers, ell). These identities\n", + "# are what align each block to its likelihood's data vector -- by identity, not order.\n", + "cmb_ids = xcov.component_ids(cmb)\n", + "lens_ids = xcov.component_ids(lensing)\n", + "\n", + "print(f\"{cmb}: {len(cmb_ids)} labelled bandpowers, e.g.\")\n", + "for key in cmb_ids[:3]:\n", + " print(\" \", key)\n", + "print(f\"{lensing}: {len(lens_ids)} labelled bandpowers, e.g.\")\n", + "for key in lens_ids[:3]:\n", + " print(\" \", key)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1178111", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Normalise the joint covariance to a correlation matrix (diag -> 1).\n", + "d = 1.0 / np.sqrt(np.diag(full))\n", + "corr_full = full * np.outer(d, d)\n", + "n_cmb = A.shape[0]\n", + "cmax = np.abs(corr).max()\n", + "\n", + "fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(11, 4.5))\n", + "\n", + "# Left: the full joint correlation matrix, with the MFLike|lensing block divider.\n", + "im0 = ax0.imshow(corr_full, cmap=\"RdBu_r\", vmin=-1, vmax=1)\n", + "ax0.axhline(n_cmb - 0.5, color=\"k\", lw=0.8)\n", + "ax0.axvline(n_cmb - 0.5, color=\"k\", lw=0.8)\n", + "ax0.set_title(\"Joint correlation matrix\")\n", + "ax0.set_xlabel(f\"{cmb} | {lensing}\")\n", + "ax0.set_ylabel(f\"{cmb} | {lensing}\")\n", + "fig.colorbar(im0, ax=ax0, fraction=0.046, label=\"correlation\")\n", + "\n", + "# Right: the CMB x lensing cross-correlation block on its own (much smaller) scale.\n", + "im1 = ax1.imshow(corr, cmap=\"RdBu_r\", vmin=-cmax, vmax=cmax, aspect=\"auto\")\n", + "ax1.set_title(f\"{cmb} x {lensing} cross-correlation\")\n", + "ax1.set_xlabel(f\"{lensing} bandpowers\")\n", + "ax1.set_ylabel(f\"{cmb} bandpowers\")\n", + "fig.colorbar(im1, ax=ax1, fraction=0.046, label=\"correlation\")\n", + "\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-003", + "metadata": {}, + "source": [ + "## Appendix: Validating the machinery on small synthetic data\n", + "\n", + "To see the same code path run end-to-end *without* the multi-GB derivative, we feed the low-level\n", + "builder a tiny synthetic MFLike-like SACC (one TT spectrum, `lmax = 60`) and a stub lensing\n", + "likelihood. This is exactly how the unit tests exercise the glue: extract the windows and fiducial\n", + "cosmology from the SACC, run a small CAMB derivative, and contract it with the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-004", + "metadata": {}, + "outputs": [], + "source": [ + "from types import SimpleNamespace\n", + "\n", + "import numpy as np\n", + "import sacc\n", + "\n", + "from soliket.cross_covariance import (\n", + " camb_lensing_derivatives_from_sacc,\n", + " cmb_combs_from_spec_meta,\n", + " cmb_lensing_crosscov,\n", + " lensing_induced_cov,\n", + ")\n", + "\n", + "\n", + "def tiny_mflike_sacc():\n", + " \"\"\"A minimal MFLike-like SACC: one TT spectrum with bandpower windows + metadata.\"\"\"\n", + " s = sacc.Sacc()\n", + " s.add_tracer(\"Misc\", \"LAT_93_s0\", quantity=\"cmb_temperature\", spin=0)\n", + " support = np.arange(2, 30)\n", + " n_bins = 3\n", + " weight = np.zeros((len(support), n_bins))\n", + " centers = []\n", + " for b, idx in enumerate(np.array_split(np.arange(len(support)), n_bins)):\n", + " weight[idx, b] = 1.0 / len(idx)\n", + " centers.append(support[idx].mean())\n", + " s.add_ell_cl(\n", + " \"cl_00\",\n", + " \"LAT_93_s0\",\n", + " \"LAT_93_s0\",\n", + " np.array(centers),\n", + " np.zeros(n_bins),\n", + " window=sacc.BandpowerWindow(support, weight),\n", + " )\n", + " s.metadata[\"f_sky_LAT\"] = 0.4\n", + " s.metadata[\"cosmo_params\"] = repr(\n", + " dict(\n", + " cosmomc_theta=0.0104,\n", + " logA=3.05,\n", + " ombh2=0.0224,\n", + " omch2=0.1202,\n", + " ns=0.9649,\n", + " Alens=1.0,\n", + " tau=0.0544,\n", + " )\n", + " )\n", + " s.metadata[\"accuracy_params\"] = repr({})\n", + " s.metadata[\"lmax\"] = 60\n", + " return s\n", + "\n", + "\n", + "# A stub lensing likelihood: just the kappa binning matrix and a flat C_ell^phiphi.\n", + "lmax_kk = 25\n", + "binning = np.zeros((2, lmax_kk))\n", + "binning[0, 2:12] = 0.1\n", + "binning[1, 12:lmax_kk] = 0.1\n", + "lensing_stub = SimpleNamespace(\n", + " binning_matrix=binning,\n", + " provider=SimpleNamespace(\n", + " get_Cl=lambda ell_factor=True: {\"pp\": np.ones(lmax_kk) * 1e-8}\n", + " ),\n", + ")\n", + "\n", + "mflike_sacc = tiny_mflike_sacc()\n", + "\n", + "# The CMB rows: one (ind_camb, support, weight) triple per spectrum, built from\n", + "# mflike's own spec_meta so the block rows land in MFLike's data-vector order.\n", + "# Here we hand-roll the one-TT-spectrum spec_meta the stub SACC implies; against a\n", + "# real likelihood this is just ``cmb_combs_from_spec_meta(mflike.spec_meta)``.\n", + "ell, _, ind = mflike_sacc.get_ell_cl(\n", + " \"cl_00\", \"LAT_93_s0\", \"LAT_93_s0\", return_ind=True\n", + ")\n", + "spec_meta = [\n", + " {\n", + " \"pol\": \"tt\",\n", + " \"hasYX_xsp\": False,\n", + " \"t1\": \"LAT_93\",\n", + " \"t2\": \"LAT_93\",\n", + " \"bpw\": mflike_sacc.get_bandpower_windows(ind),\n", + " \"leff\": ell,\n", + " }\n", + "]\n", + "combs = cmb_combs_from_spec_meta(spec_meta)\n", + "\n", + "# Each block is built from the same CAMB lensed-Cl derivative; run CAMB once and\n", + "# share the bundle across builders via ``derivatives=`` instead of recomputing it\n", + "# per block (the derivative is the expensive part, ~3 GB / minutes at full accuracy).\n", + "derivs = camb_lensing_derivatives_from_sacc(mflike_sacc)\n", + "\n", + "block = cmb_lensing_crosscov(mflike_sacc, lensing_stub, combs, derivatives=derivs)\n", + "print(\"CMB x lensing block :\", block.shape, \" finite:\", np.all(np.isfinite(block)))\n", + "\n", + "induced = lensing_induced_cov(mflike_sacc, combs, derivatives=derivs)\n", + "print(\n", + " \"lensing-induced block:\",\n", + " induced.shape,\n", + " \" symmetric:\",\n", + " np.allclose(induced, induced.T),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-005", + "metadata": {}, + "source": [ + "### Wrapping a block in a `CrossCov`\n", + "\n", + "`CrossCov` is the container `MultiGaussianLikelihood` reads. You register each component's\n", + "auto-covariance with `add_component`, the off-diagonal block with `add_cross_covariance`, and `save`\n", + "it to a SACC file. (`from_cmb_lensing` does all of this for you on real data.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-006", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "from soliket.gaussian.gaussian_data import CrossCov\n", + "\n", + "n_cmb, n_kk = block.shape\n", + "xcov = CrossCov()\n", + "xcov.add_component(\"mflike\", np.eye(n_cmb)) # placeholder auto-covariances for the demo\n", + "xcov.add_component(\"lensing\", np.eye(n_kk))\n", + "xcov.add_cross_covariance(\"mflike\", \"lensing\", block)\n", + "\n", + "path = os.path.join(tempfile.mkdtemp(prefix=\"soliket_xcov_\"), \"XCov_demo.fits\")\n", + "xcov.save(path)\n", + "\n", + "reloaded = CrossCov.load(path)\n", + "print(\"saved to\", path)\n", + "print(\"round-trip block matches:\", np.allclose(reloaded[(\"mflike\", \"lensing\")], block))" + ] + }, + { + "cell_type": "markdown", + "id": "cell-007", + "metadata": {}, + "source": [ + "## 3. The full set of blocks\n", + "\n", + "A complete joint CMB + lensing + LSS analysis needs several blocks, all built from the same CAMB\n", + "lensed-Cl derivative (`camb_lensing_derivatives`):\n", + "\n", + "| Block | Builder | What it couples |\n", + "| --- | --- | --- |\n", + "| CMB x CMB-lensing | `cmb_lensing_crosscov` | MFLike TT/TE/EE x reconstruction kappa-kappa |\n", + "| lensing-induced | `lensing_induced_cov` | extra CMB-internal covariance from lensing |\n", + "| CMB x shear/galaxy-kappa | `shear_kappa_crosscov` | MFLike x an LSS cross-correlation likelihood |\n", + "| N1 reconstruction-noise | `n1_crosscov_block` | the N1 bias contribution to the kappa cross-cov |\n", + "\n", + "The N1 *matrix* itself is produced with the external [`lensitbiases`](https://github.com/carronj/lensitbiases)\n", + "package and is **not** part of `soliket.cross_covariance` (only the kernel that *applies* a precomputed\n", + "N1 matrix is). See [`../dev/cross_cov/create_cross_covariance.ipynb`](../dev/cross_cov/create_cross_covariance.ipynb)\n", + "for the full-accuracy reference build, including N1, and for the committed reference products used in\n", + "regression testing.\n", + "\n", + "Next: [`analyse_datasets.ipynb`](analyse_datasets.ipynb) loads a saved `CrossCov` into a\n", + "`MultiGaussianLikelihood` and runs a joint analysis." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "soliket (3.12.13.final.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/ISO_sims/create_datasets.ipynb b/notebooks/ISO_sims/create_datasets.ipynb new file mode 100644 index 00000000..90aecf43 --- /dev/null +++ b/notebooks/ISO_sims/create_datasets.ipynb @@ -0,0 +1,392 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating simulated datasets\n", + "\n", + "This is step 1 of the ISO end-to-end pipeline:\n", + "\n", + "1. **this notebook** synthesises the datasets,\n", + "2. [`create_cross_covariance.ipynb`](create_cross_covariance.ipynb) builds the cross-covariance\n", + " between them,\n", + "3. [`analyse_datasets.ipynb`](analyse_datasets.ipynb) runs the joint fit on exactly these products.\n", + "\n", + "All artifacts go to a stable, git-ignored `sims/` directory that the downstream notebooks read by\n", + "the same filenames. We build, from simplest to most involved:\n", + "\n", + "1. **From scratch** — a galaxy x CMB-lensing cross-correlation, computed with\n", + " [CCL](https://github.com/LSSTDESC/CCL), with the cosmology and the noise exposed as knobs.\n", + "2. **A smooth CMB-lensing twin** — the shipped reconstruction with its bandpowers replaced by the\n", + " theory at the ISO fiducial (chi-square = 0 when fit at the same cosmology), via\n", + " `soliket.sacc_tools.smooth_twin_sacc`.\n", + "3. **A smooth MFLike CMB+foregrounds dataset** — the per-frequency theory binned through MFLike's own\n", + " windows, via `soliket.sacc_tools.smooth_mflike_sacc`.\n", + "\n", + "Datasets 2 and 3 are the joint-analysis inputs; everything is built at the **ISO fiducial**\n", + "(`defaults_dir=\"defaults\"` — cosmology incl. the SO normal-hierarchy neutrinos)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "import sacc\n", + "\n", + "# All three ISO notebooks share this directory under the same name (git-ignored).\n", + "SIMS = Path(\"sims\")\n", + "SIMS.mkdir(exist_ok=True)\n", + "\n", + "# Canonical artifact filenames the downstream notebooks read.\n", + "GALAXY_KAPPA = SIMS / \"galaxy_kappa.sim.fits\"\n", + "LENSING_SMOOTH = SIMS / \"lensing_smooth.sacc.fits\"\n", + "MFLIKE_SMOOTH = SIMS / \"mflike_smooth.fits\"\n", + "print(\"writing simulated datasets to\", SIMS.resolve())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. A galaxy x kappa dataset from scratch\n", + "\n", + "We simulate the cross-correlation of an unWISE-like galaxy sample with the SO CMB-lensing\n", + "convergence. The three spectra (`gg`, `gk`, `kk`) come from CCL; the bandpower windows and the joint\n", + "covariance come from `soliket.sacc_tools`. The **cosmology is sourced from the same shared ISO\n", + "fiducial** as the lensing / MFLike twins (`defaults/cosmo.yaml`, translated to CCL convention by\n", + "`iso_ccl_cosmology`), so all three datasets agree on the cosmological parameters. We wrap the build in\n", + "a function so the **cosmology** and the **galaxy noise spectrum** stay explicit knobs, then build the\n", + "canonical dataset at the fiducial and two variants (a lower-amplitude cosmology, a custom noise)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pyccl as ccl\n", + "\n", + "from soliket.presets import load_fiducial_map\n", + "from soliket.sacc_tools import gaussian_covariance, top_hat_windows\n", + "\n", + "# Fiducial galaxy redshift distribution, borrowed from the shipped reference dataset.\n", + "ref = sacc.Sacc.load_fits(\"../../tests/data/unwise_g-so_kappa.sim.sacc.fits\")\n", + "Z, NZ = ref.tracers[\"gc_unwise\"].z, ref.tracers[\"gc_unwise\"].nz\n", + "\n", + "\n", + "def iso_ccl_cosmology(defaults_dir=\"defaults\", **overrides):\n", + " \"\"\"A ``ccl.Cosmology`` at the ISO fiducial, read from ``defaults/cosmo.yaml``.\n", + "\n", + " The galaxy x kappa dataset uses the SAME shared fiducial as the lensing / MFLike\n", + " twins, translated from CAMB convention (``H0``, ``ombh2``, ``omch2``, ``logA``)\n", + " to CCL's (``h``, ``Omega_b``, ``Omega_c``, ``A_s``) -- so all three datasets share\n", + " one set of cosmological parameters instead of an ad-hoc one. `overrides` replace\n", + " individual CCL kwargs (e.g. ``A_s=1.8e-9`` for the low-amplitude knob), keeping the\n", + " cosmology a knob while anchoring its baseline to the shared fiducial.\n", + "\n", + " Massive neutrinos are omitted here: the ISO sum (~0.06 eV) is a sub-percent effect\n", + " at l <= 600 for this standalone demo, and the CCL/CAMB neutrino conventions differ\n", + " (CCL's normal-hierarchy minimum exceeds the ISO sum), so exact matching is not\n", + " meaningful at this level.\n", + " \"\"\"\n", + " cosmo = load_fiducial_map(defaults_dir)[\"cosmo\"]\n", + "\n", + " def central(name): # central value of a cosmo.yaml param spec\n", + " spec = cosmo[name]\n", + " return spec[\"ref\"][\"loc\"] if \"ref\" in spec else spec[\"value\"]\n", + "\n", + " h = central(\"H0\") / 100.0\n", + " kwargs = dict(\n", + " h=h,\n", + " Omega_b=central(\"ombh2\") / h**2,\n", + " Omega_c=central(\"omch2\") / h**2,\n", + " n_s=central(\"ns\"),\n", + " A_s=1e-10 * np.exp(central(\"logA\")), # As(logA), per cosmo.yaml\n", + " matter_power_spectrum=\"linear\",\n", + " )\n", + " kwargs.update(overrides)\n", + " return ccl.Cosmology(**kwargs)\n", + "\n", + "\n", + "def build_galaxy_kappa(\n", + " out_path,\n", + " *,\n", + " cosmo,\n", + " ngal_per_arcmin2=1.0,\n", + " fsky=0.4,\n", + " ell_max=600,\n", + " n_bins=20,\n", + " noise_gg=None,\n", + "):\n", + " \"\"\"Simulate an unWISE-like galaxy x SO CMB-lensing cross-correlation SACC.\n", + "\n", + " `cosmo` is a ``ccl.Cosmology``; `noise_gg` optionally overrides the galaxy\n", + " auto-spectrum noise (default: shot noise ``1 / n_gal``). Returns the SACC.\n", + " \"\"\"\n", + " b1, mag_bias = 1.0, 0.4\n", + " gc = ccl.NumberCountsTracer(\n", + " cosmo,\n", + " has_rsd=False,\n", + " dndz=(Z, NZ),\n", + " bias=(Z, b1 * np.ones_like(Z)),\n", + " mag_bias=(Z, mag_bias * np.ones_like(Z)),\n", + " )\n", + " ck = ccl.CMBLensingTracer(cosmo, z_source=1086.0)\n", + "\n", + " ells, window = top_hat_windows(ell_max, n_bins)\n", + " # Bin theory through the window rather than sampling it at the bin centres, so\n", + " # the stored data is exactly what the likelihood computes for it (`w_bins @ cl`,\n", + " # a plain contraction). Sampling at centres instead leaves a curvature residual\n", + " # -- the bin mean of a curved spectrum is not its centre value -- worth ~13% in\n", + " # the first bin, where C_ell is steepest across the bin's 30 multipoles.\n", + " w_bins = window.weight.T\n", + " support = np.asarray(window.values, dtype=float)\n", + " # Per-bin widths read off the window, not assumed uniform: top_hat_windows\n", + " # splits ell_max + 1 multipoles into n_bins, so when that does not divide\n", + " # evenly the leading bins are one multipole wider, and Knox goes as 1/delta_ell.\n", + " delta_ell = (w_bins != 0).sum(axis=1)\n", + " cl_gg = w_bins @ ccl.angular_cl(cosmo, gc, gc, support)\n", + " cl_gk = w_bins @ ccl.angular_cl(cosmo, gc, ck, support)\n", + " cl_kk = w_bins @ ccl.angular_cl(cosmo, ck, ck, support)\n", + "\n", + " if noise_gg is None: # default: galaxy shot noise 1 / n_gal\n", + " ngal_sr = ngal_per_arcmin2 / np.deg2rad(1.0 / 60.0) ** 2\n", + " noise_gg = np.full_like(cl_gg, 1.0 / ngal_sr)\n", + " cls = np.array([[cl_gg + noise_gg, cl_gk], [cl_gk, cl_kk]])\n", + " cov = gaussian_covariance(cls, ells, delta_ell, fsky)\n", + "\n", + " s = sacc.Sacc()\n", + " s.metadata[\"info\"] = \"Simulated unWISE-like galaxy x SO CMB-lensing cross-correlation\"\n", + " s.add_tracer(\n", + " \"NZ\",\n", + " \"gc_unwise\",\n", + " quantity=\"galaxy_density\",\n", + " spin=0,\n", + " z=Z,\n", + " nz=NZ,\n", + " metadata={\"ngal\": ngal_per_arcmin2},\n", + " )\n", + " s.add_tracer(\n", + " \"Map\",\n", + " \"ck_so\",\n", + " quantity=\"cmb_convergence\",\n", + " spin=0,\n", + " ell=np.arange(3000),\n", + " beam=np.ones(3000),\n", + " )\n", + " s.add_ell_cl(\"cl_00\", \"gc_unwise\", \"gc_unwise\", ells, cl_gg, window=window)\n", + " s.add_ell_cl(\"cl_00\", \"gc_unwise\", \"ck_so\", ells, cl_gk, window=window)\n", + " s.add_ell_cl(\"cl_00\", \"ck_so\", \"ck_so\", ells, cl_kk, window=window)\n", + " s.add_covariance(cov)\n", + " s.save_fits(str(out_path), overwrite=True)\n", + " return s" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Canonical dataset at the shared ISO fiducial cosmology (from defaults/cosmo.yaml).\n", + "fiducial = iso_ccl_cosmology()\n", + "s = build_galaxy_kappa(GALAXY_KAPPA, cosmo=fiducial)\n", + "print(\"wrote\", GALAXY_KAPPA.name, \"->\", len(s.mean), \"data points\")\n", + "\n", + "# Knob 1 - a lower-amplitude deviation from the fiducial (lower A_s -> lower sigma8).\n", + "low_amp = iso_ccl_cosmology(A_s=1.8e-9)\n", + "build_galaxy_kappa(SIMS / \"galaxy_kappa.lowA.fits\", cosmo=low_amp)\n", + "\n", + "# Knob 2 - a custom (flat, noisier) galaxy noise spectrum instead of pure shot noise.\n", + "ells, _ = top_hat_windows(600, 20)\n", + "build_galaxy_kappa(\n", + " SIMS / \"galaxy_kappa.noisy.fits\", cosmo=fiducial, noise_gg=np.full(len(ells), 5e-6)\n", + ")\n", + "print(\"knob variants written (lower-amplitude cosmology, custom noise)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. A smooth CMB-lensing twin\n", + "\n", + "A *smooth* twin reuses the shipped dataset's tracers, bandpower windows and covariance but replaces\n", + "the noisy measured bandpowers with the theory at a chosen cosmology — so the likelihood gives\n", + "chi-square = 0 when fit at that cosmology. `soliket.sacc_tools.smooth_twin_sacc` does the SACC\n", + "surgery; we get the binned theory from the evaluated `lensing` component **of the joint\n", + "`multigaussian` model** (via `resolve_aliases`, the named role). Imprinting from the *same* model the\n", + "analysis fits with matters here: the joint CAMB is driven by both members' requirements, so a\n", + "standalone-`lensing` imprint would leave a small (~0.01) residual chi-square — pulling the lensing\n", + "component out of the joint model makes it exactly zero. The **imprint cosmology is a knob**: by\n", + "default the ISO fiducial, but any param override (e.g. a shifted `tau`) imprints a non-fiducial twin\n", + "for parameter-recovery tests.\n", + "\n", + "> **Overriding likelihood options per folder.** Beyond the param/`theory.yaml` overrides in\n", + "> `defaults/`, you can patch a preset's *likelihood/theory skeleton* by dropping a\n", + "> `defaults/templates/.yaml` — it is layered onto the packaged template (`recursive_update`,\n", + "> last wins), so you set only the keys you want (e.g. `theory_lmax`). Because the `multigaussian`\n", + "> preset *composes* its members, an override to `templates/lensing.yaml` reaches **both** this\n", + "> standalone lensing build and the joint fit — imprint and fit stay consistent by construction. We\n", + "> keep the ISO `defaults/` at the packaged values here, so no such file is shipped." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from cobaya.model import get_model\n", + "from cobaya.tools import resolve_packages_path\n", + "\n", + "from soliket.presets import build_info, resolve_aliases\n", + "from soliket.sacc_tools import smooth_twin_sacc\n", + "\n", + "\n", + "def smooth_lensing(out_path, **param_overrides):\n", + " \"\"\"Write a smooth (theory) CMB-lensing twin, imprinted from the JOINT model.\n", + "\n", + " The twin is fit by the ``multigaussian`` preset (analyse_datasets.ipynb), whose\n", + " shared CAMB is driven by *both* members' requirements. Imprinting from that same\n", + " joint model -- rather than the standalone ``lensing`` preset -- makes the binned\n", + " clkk bit-identical to what the fit recomputes, so the joint chi-square is 0\n", + " *exactly*. (A standalone-lensing imprint leaves a ~0.01 residual: the lensing-only\n", + " CAMB precision differs from the joint, and matching extra_args by hand does not\n", + " close it -- only the same model does.) `param_overrides` pins fiducial params\n", + " (e.g. ``tau=0.06``) to imprint a non-fiducial twin. Returns\n", + " ``(lensing_likelihood, binned_clkk)``. Note: builds the full joint model, so it\n", + " needs the MFLike data and runs CAMB at the joint accuracy (a few minutes).\n", + " \"\"\"\n", + " info = build_info(\"multigaussian\", defaults_dir=\"defaults\")\n", + " info[\"packages_path\"] = resolve_packages_path()\n", + " for name, value in param_overrides.items():\n", + " info[\"params\"][name] = {\"value\": value}\n", + "\n", + " model = get_model(info)\n", + " model.loglikes({}) # evaluate at the imprint cosmology\n", + " lensing = resolve_aliases(model).lensing\n", + " clkk = lensing._get_theory() # binned C_ell^kappakappa\n", + "\n", + " src = sacc.Sacc.load_fits(lensing.datapath) # reuse shipped tracers/windows/cov\n", + " smooth_twin_sacc(src, \"cl_00\", \"ck\", \"ck\", clkk, out_path=out_path)\n", + " return lensing, clkk\n", + "\n", + "\n", + "lensing, clkk = smooth_lensing(LENSING_SMOOTH)\n", + "print(\"wrote\", LENSING_SMOOTH.name, \"->\", len(clkk), \"bins\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Knob - imprint a second twin at a shifted tau (a parameter-recovery target).\n", + "smooth_lensing(SIMS / \"lensing_smooth.tau0p06.sacc.fits\", tau=0.06)\n", + "print(\"wrote a tau=0.06 twin alongside\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. A smooth MFLike CMB + foregrounds dataset\n", + "\n", + "The same smooth-twin idea for the primary CMB, where the data vector spans many frequency\n", + "cross-spectra. The per-frequency plumbing — combining CMB + foregrounds + systematics through\n", + "MFLike's `get_modified_theory`, binning with MFLike's own bandpower windows, and writing one `NuMap`\n", + "tracer per `(frequency, spin)` channel — lives in `soliket.sacc_tools.smooth_mflike_sacc`, which\n", + "takes the concrete handles (the evaluated likelihood + theory outputs, not a `Session`).\n", + "\n", + "This build runs CAMB at MFLike accuracy and takes a few minutes; the covariance and bandpower-window\n", + "(Bbl) matrices are reused from the shipped `cov_Bbl_file`, so only the data vector is regenerated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from soliket.sacc_tools import smooth_mflike_sacc\n", + "\n", + "RUN_MFLIKE = True # set False to skip the few-minute CAMB build\n", + "\n", + "if RUN_MFLIKE:\n", + " info = build_info(\"mflike\", defaults_dir=\"defaults\")\n", + " info[\"packages_path\"] = resolve_packages_path()\n", + " model = get_model(info)\n", + " roles = resolve_aliases(model)\n", + "\n", + " # Numeric fiducial values (skip lambda-valued / derived params).\n", + " params = {\n", + " k: v[\"value\"]\n", + " for k, v in info[\"params\"].items()\n", + " if isinstance(v, dict) and \"value\" in v and not isinstance(v[\"value\"], str)\n", + " }\n", + " model.loglikes(params) # evaluate at the ISO fiducial\n", + "\n", + " dls = model.provider.get_Cl(ell_factor=True)\n", + " fg_totals = roles.foreground.get_fg_totals()\n", + " smooth_mflike_sacc(roles.mflike, dls, fg_totals, params, out_path=MFLIKE_SMOOTH)\n", + " print(\"wrote\", MFLIKE_SMOOTH.name)\n", + "else:\n", + " print(\"RUN_MFLIKE is False - skipping the smooth MFLike build.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recap\n", + "\n", + "`sims/` now holds the pipeline inputs:\n", + "\n", + "| Artifact | Built from | Consumed by |\n", + "| --- | --- | --- |\n", + "| `mflike_smooth.fits` | `smooth_mflike_sacc` (CMB+fg theory, ISO fiducial) | the analysis (MFLike component) |\n", + "| `lensing_smooth.sacc.fits` | `smooth_twin_sacc` (lensing theory, ISO fiducial) | the analysis (lensing component) |\n", + "| `galaxy_kappa.sim.fits` (+ knob variants) | CCL from scratch | standalone galaxy x kappa example |\n", + "\n", + "The smooth lensing twin carries only the data + covariance; the analysis notebook points the\n", + "likelihood's `correction_filename` / `fiducial_filename` at the shipped (multi-hundred-MB) auxiliary\n", + "files in place, so we never copy those into `sims/`. Both smooth twins are built at the **same ISO\n", + "fiducial** the analysis fits at, so the joint chi-square is 0 by construction.\n", + "\n", + "Next: [`create_cross_covariance.ipynb`](create_cross_covariance.ipynb) builds the cross-covariance\n", + "between the MFLike and lensing data vectors; then\n", + "[`analyse_datasets.ipynb`](analyse_datasets.ipynb) runs the joint fit on these `sims/` products." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "soliket", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/ISO_sims/defaults/cosmo.yaml b/notebooks/ISO_sims/defaults/cosmo.yaml new file mode 100644 index 00000000..8b217cd6 --- /dev/null +++ b/notebooks/ISO_sims/defaults/cosmo.yaml @@ -0,0 +1,51 @@ +# ISO sims — local cosmology override for soliket.presets. +# Unlike the bundled soliket/presets/defaults/cosmo.yaml (which uses cobaya's +# single-massive neutrino default), this file deliberately keeps the SO +# 2-eigenstate NORMAL-HIERARCHY neutrino params (mnu1/mnu2/mnu3 + the mnu sum). +# The matching camb extra_args (num_nu_massive, nu_mass_eigenstates, ...) live in +# this folder's theory.yaml, applied by build_info -- no inline Python needed. +# Passed to build_info(..., defaults_dir="defaults"). +# Only the groups present here override; foreground/systematics fall back to the package. +H0: + prior: {min: 40, max: 100} + ref: {dist: norm, loc: 67.7, scale: 0.1} + latex: 'H_0' +logA: + drop: true + prior: {min: 1.6, max: 4.0} + ref: {dist: norm, loc: 3.05, scale: 0.001} + latex: '\log(10^{10} A_\mathrm{s})' +As: + value: 'lambda logA: 1e-10*np.exp(logA)' + latex: 'A_\mathrm{s}' +ombh2: + prior: {min: 0.022, max: 0.023} + ref: {dist: norm, loc: 0.0224, scale: 0.0001} + latex: '\Omega_\mathrm{b} h^2' +omch2: + prior: {min: 0.09, max: 0.15} + ref: {dist: norm, loc: 0.1202, scale: 0.001} + latex: '\Omega_c h^2' +ns: + prior: {min: 0.9, max: 1.1} + ref: {dist: norm, loc: 0.9649, scale: 0.001} + latex: 'n_s' +tau: + prior: {dist: norm, loc: 0.0544, scale: 0.0073} + ref: {dist: norm, loc: 0.0544, scale: 0.0073} + proposal: 0.0073 + latex: '\tau_\mathrm{reio}' +Alens: + value: 1.0 + latex: 'A_\mathrm{lens}' +omega_de: {derived: true, latex: '\Omega_\Lambda'} +omegam: {derived: true, latex: '\Omega_\mathrm{m}'} +omegamh2: + derived: 'lambda omegam, H0: omegam*(H0/100)**2' + latex: '\Omega_\mathrm{m} h^2' +sigma8: {derived: true, latex: '\sigma_8'} +mnu1: {value: 0.0, drop: true} +mnu2: {value: 'lambda: np.sqrt(7.5e-5)', drop: true} +mnu3: {value: 'lambda: np.sqrt(2.5e-3)', drop: true} +mnu: {value: 'lambda mnu1, mnu2, mnu3: mnu1 + mnu2 + mnu3', latex: '\sum m_\nu'} +nnu: {value: 3.044, latex: 'N_\mathrm{eff}'} diff --git a/notebooks/ISO_sims/defaults/foreground.yaml b/notebooks/ISO_sims/defaults/foreground.yaml new file mode 100644 index 00000000..bf1f5aff --- /dev/null +++ b/notebooks/ISO_sims/defaults/foreground.yaml @@ -0,0 +1,29 @@ +# SOLikeT Fiducial map — foreground group. +# ISO sims: a verbatim copy of the packaged soliket/presets/defaults/foreground.yaml — +# the ISO analysis takes the package baseline here, unmodified. It is present so this +# folder is a COMPLETE worked example rather than relying on per-file fallback. +# Unguarded by design: the packaged copy is tripwired against mflike's shipped defaults +# (tests/test_presets.py::test_presets_foreground_matches_mflike_defaults); this one is +# not, so an mflike bump can silently drift it. Re-copy when that tripwire fires. +a_tSZ: {prior: {min: 1.5, max: 5}, ref: 3.30, proposal: 0.05, latex: 'a_\mathrm{tSZ}'} +a_kSZ: {prior: {min: 0.0, max: 10}, ref: 1.60, proposal: 0.1, latex: 'a_\mathrm{kSZ}'} +a_p: {prior: {min: 2, max: 12}, ref: 6.90, proposal: 0.075, latex: 'a_p'} +beta_p: {prior: {min: 1, max: 4.0}, ref: 2.20, proposal: 0.03, latex: '\beta_p'} +a_c: {prior: {min: 0, max: 10}, ref: 4.90, proposal: 0.12, latex: 'a_c'} +beta_c: {prior: {min: 1.0, max: 4.0}, ref: 2.20, proposal: 0.03, latex: '\beta_c'} +a_s: {prior: {min: 1.0, max: 7}, ref: 3.10, proposal: 0.01, latex: 'a_s'} +a_gtt: {prior: {min: 0.0, max: 10}, ref: 2.80, proposal: 0.14, latex: 'a_\mathrm{dust}^\mathrm{TT}'} +a_gte: {prior: {min: 0, max: 11}, ref: 0.1, proposal: 0.007, latex: 'a_\mathrm{dust}^\mathrm{TE}'} +a_gee: {prior: {min: 0.0, max: 5.0}, ref: 0.1, proposal: 0.0006, latex: 'a_\mathrm{dust}^\mathrm{EE}'} +a_psee: {prior: {min: 0, max: 1}, ref: 0, proposal: 0.05, latex: 'a_\mathrm{ps}^\mathrm{EE}'} +a_pste: {prior: {min: -1, max: 1}, ref: 0, proposal: 0.05, latex: 'a_\mathrm{ps}^\mathrm{TE}'} +xi: {prior: {min: 0, max: 0.2}, ref: 0.1, proposal: 0.05, latex: '\xi'} +alpha_tSZ: {prior: {min: -5.0, max: 5.0}, ref: 0, proposal: 0.1, latex: '\alpha_\mathrm{tSZ}'} +beta_s: {prior: {min: -3.6, max: -1.5}, ref: -2.5, proposal: 0.2, latex: '\beta_s'} +T_d: {value: 9.7, latex: 'T_d'} +T_effd: {value: 19.6, latex: 'T_{\mathrm{dust},\mathrm{eff}}'} +beta_d: {value: 1.5, latex: '\beta_\mathrm{dust}'} +alpha_s: {value: 1.0, latex: '\alpha_s'} +alpha_p: {value: 1.0, latex: '\alpha_p'} +alpha_dT: {value: -0.6, latex: '\alpha_{\mathrm{dust},T}'} +alpha_dE: {value: -0.4, latex: '\alpha_{\mathrm{dust},E}'} diff --git a/notebooks/ISO_sims/defaults/systematics.yaml b/notebooks/ISO_sims/defaults/systematics.yaml new file mode 100644 index 00000000..bf57c0b2 --- /dev/null +++ b/notebooks/ISO_sims/defaults/systematics.yaml @@ -0,0 +1,20 @@ +# SOLikeT Fiducial map — systematics group. +# ISO sims: a verbatim copy of the packaged soliket/presets/defaults/systematics.yaml — +# the ISO analysis takes the package baseline here, unmodified. It is present so this +# folder is a COMPLETE worked example rather than relying on per-file fallback. +bandint_shift_LAT_93: {prior: {dist: norm, loc: 0.0, scale: 1.0}, ref: 0.0, proposal: 0.1, latex: '\Delta_{\rm band}^{93}'} +bandint_shift_LAT_145: {prior: {dist: norm, loc: 0.0, scale: 1.0}, ref: 0.0, proposal: 0.1, latex: '\Delta_{\rm band}^{145}'} +bandint_shift_LAT_225: {prior: {dist: norm, loc: 0.0, scale: 1.0}, ref: 0.0, proposal: 0.1, latex: '\Delta_{\rm band}^{225}'} +cal_LAT_93: {prior: {dist: norm, loc: 1.0, scale: 0.01}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}^{93}'} +cal_LAT_145: {prior: {dist: norm, loc: 1.0, scale: 0.01}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}^{145}'} +cal_LAT_225: {prior: {dist: norm, loc: 1.0, scale: 0.01}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}^{225}'} +calT_LAT_93: {value: 1, latex: '\mathrm{Cal}_{\rm T}^{93}'} +calE_LAT_93: {prior: {min: 0.9, max: 1.1}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}_{\rm E}^{93}'} +calT_LAT_145: {value: 1, latex: '\mathrm{Cal}_{\rm T}^{145}'} +calE_LAT_145: {prior: {min: 0.9, max: 1.1}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}_{\rm E}^{145}'} +calT_LAT_225: {value: 1, latex: '\mathrm{Cal}_{\rm T}^{225}'} +calE_LAT_225: {prior: {min: 0.9, max: 1.1}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}_{\rm E}^{225}'} +calG_all: {value: 1, latex: '\mathrm{Cal}_{\rm G}^{\rm All}'} +alpha_LAT_93: {value: 0, latex: '\alpha^{93}'} +alpha_LAT_145: {value: 0, latex: '\alpha^{145}'} +alpha_LAT_225: {value: 0, latex: '\alpha^{225}'} diff --git a/notebooks/ISO_sims/defaults/theory.yaml b/notebooks/ISO_sims/defaults/theory.yaml new file mode 100644 index 00000000..4a19d278 --- /dev/null +++ b/notebooks/ISO_sims/defaults/theory.yaml @@ -0,0 +1,14 @@ +# ISO sims — local theory override for soliket.presets (SO normal hierarchy, camb-only). +# REPLACES the packaged soliket/presets/defaults/theory.yaml wholesale (per-file +# fallback): the packaged single-massive keys (num_massive_neutrinos, nnu) never +# enter, so there is no conflict to resolve. The neutrino mass sum (mnu) and Neff +# (nnu) live as params in this folder's cosmo.yaml; the eigenstate structure lives +# here. Passed to build_info(..., defaults_dir="defaults") by the ISO notebooks. +camb: + extra_args: + num_nu_massless: 1.044 + num_nu_massive: 2 + nu_mass_eigenstates: 2 + nu_mass_fractions: [0.14763410387308012, 0.8523658961269198] + nu_mass_numbers: [1, 1] + share_delta_neff: true diff --git a/notebooks/_nb.py b/notebooks/_nb.py new file mode 100644 index 00000000..77601d8d --- /dev/null +++ b/notebooks/_nb.py @@ -0,0 +1,56 @@ +"""Notebook-only conveniences for SOLikeT tutorials. + +Repo-only sugar (not part of the shipped ``soliket`` package): resolving the +packages path, triggering data installation, pulling spectra off a +:class:`soliket.presets.Session`, and quick plotting. The reusable API lives in +``soliket.presets``; this module is just glue to keep the notebooks short. +""" + +import numpy as np + +# Spectra Cobaya can return from get_Cl, in a sensible plotting order. +_DEFAULT_SPECTRA = ("tt", "te", "ee") + + +def packages_path(): + """Cobaya's resolved packages path (where installed likelihood data lives).""" + from cobaya.tools import resolve_packages_path + + return resolve_packages_path() + + +def install_data(preset, path=None, **kwargs): + """Download the data a preset's likelihoods need, via ``cobaya.install``.""" + from cobaya.install import install + + from soliket.presets import build_info + + return install(build_info(preset), path=path or packages_path(), **kwargs) + + +def theory_dls(session, ell_factor=True): + """CMB :math:`D_\\ell` spectra for an evaluated session (a ``get_Cl`` dict). + + Call ``session.loglike()`` first so the provider has computed the point. + """ + return session.model.provider.get_Cl(ell_factor=ell_factor) + + +def foreground_totals(session): + """Total foreground bandpowers for the session's BandpowerForeground theory.""" + return session.foreground.get_fg_totals() + + +def plot_dls(dls, spectra=_DEFAULT_SPECTRA, ax=None): + """Log-log plot of ``|D_\\ell|`` for the requested spectra; returns the Axes.""" + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots() + ell = dls["ell"] + for spec in spectra: + ax.loglog(ell, np.abs(dls[spec]), label=spec.upper()) + ax.set_xlabel(r"$\ell$") + ax.set_ylabel(r"$|D_\ell|\ [\mu K^2]$") + ax.legend() + return ax diff --git a/notebooks/dev/smooth/01_multi_gaussian_setup.ipynb b/notebooks/dev/smooth/01_multi_gaussian_setup.ipynb index 2473490f..98cfe372 100644 --- a/notebooks/dev/smooth/01_multi_gaussian_setup.ipynb +++ b/notebooks/dev/smooth/01_multi_gaussian_setup.ipynb @@ -11,10 +11,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "07f61ba9-eb51-4824-88be-00eeb8220933", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Numpy : 1.24.3\n", + "Matplotlib : 3.10.9\n", + " CAMB : 1.6.6\n", + " Cobaya : 3.6.2\n" + ] + } + ], "source": [ "%matplotlib inline\n", "# import tempfile\n", @@ -35,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "518ade6c-55d8-44bf-8e72-1fb4afa996a1", "metadata": {}, "outputs": [], @@ -110,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "2357871e-4b5a-42f8-a5a5-b8a947404423", "metadata": {}, "outputs": [], @@ -126,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "22fda6d8-7e34-4db1-aa06-42c16f42af3e", "metadata": {}, "outputs": [], @@ -191,12 +202,55 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "2d73c662-374c-41dc-a17e-1c268befac44", "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-06-04 11:32:48.810786: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n", + "2026-06-04 11:32:48.896874: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.\n", + "2026-06-04 11:32:49.568402: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used.\n", + "2026-06-04 11:32:49.588064: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n", + "To enable the following instructions: AVX2 AVX_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2026-06-04 11:32:52.978958: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[camb] `camb` module loaded successfully from /home/ggalloni/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/camb\n", + "[mflike.ttteee] *ERROR* The 'data_folder' directory does not exist. Check the given path [/tmp/LAT_packages/data/soliket_mflike/smooth_data/split_mflike/].\n" + ] + }, + { + "ename": "LoggedError", + "evalue": "The 'data_folder' directory does not exist. Check the given path [/tmp/LAT_packages/data/soliket_mflike/smooth_data/split_mflike/].", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mLoggedError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[6], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mcobaya\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmodel\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m get_model\n\u001b[0;32m----> 3\u001b[0m model_multi \u001b[38;5;241m=\u001b[39m \u001b[43mget_model\u001b[49m\u001b[43m(\u001b[49m\u001b[43minfo_multi\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/model.py:1660\u001b[0m, in \u001b[0;36mget_model\u001b[0;34m(info_or_yaml_or_file, debug, stop_at_error, packages_path, override)\u001b[0m\n\u001b[1;32m 1655\u001b[0m get_logger(\u001b[38;5;18m__name__\u001b[39m)\u001b[38;5;241m.\u001b[39mdebug(\n\u001b[1;32m 1656\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInput info updated with defaults (dumped to YAML):\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 1657\u001b[0m yaml_dump(sort_cosmetic(updated_info)),\n\u001b[1;32m 1658\u001b[0m )\n\u001b[1;32m 1659\u001b[0m \u001b[38;5;66;03m# Initialize the parameters and posterior\u001b[39;00m\n\u001b[0;32m-> 1660\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mModel\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1661\u001b[0m \u001b[43m \u001b[49m\u001b[43mupdated_info\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mparams\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1662\u001b[0m \u001b[43m \u001b[49m\u001b[43mupdated_info\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mlikelihood\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1663\u001b[0m \u001b[43m \u001b[49m\u001b[43mupdated_info\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mprior\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1664\u001b[0m \u001b[43m \u001b[49m\u001b[43mupdated_info\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtheory\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1665\u001b[0m \u001b[43m \u001b[49m\u001b[43mpackages_path\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minfo\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mpackages_path\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1666\u001b[0m \u001b[43m \u001b[49m\u001b[43mtiming\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mupdated_info\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtiming\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1667\u001b[0m \u001b[43m \u001b[49m\u001b[43mstop_at_error\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minfo\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mstop_at_error\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1668\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/model.py:291\u001b[0m, in \u001b[0;36mModel.__init__\u001b[0;34m(self, info_params, info_likelihood, info_prior, info_theory, packages_path, timing, allow_renames, stop_at_error, post, skip_unused_theories, dropped_theory_params)\u001b[0m\n\u001b[1;32m 287\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtheory \u001b[38;5;241m=\u001b[39m TheoryCollection(\n\u001b[1;32m 288\u001b[0m info_theory \u001b[38;5;129;01mor\u001b[39;00m {}, packages_path\u001b[38;5;241m=\u001b[39mpackages_path, timing\u001b[38;5;241m=\u001b[39mtiming\n\u001b[1;32m 289\u001b[0m )\n\u001b[1;32m 290\u001b[0m info_likelihood \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_updated_info[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlikelihood\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[0;32m--> 291\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlikelihood \u001b[38;5;241m=\u001b[39m \u001b[43mLikelihoodCollection\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 292\u001b[0m \u001b[43m \u001b[49m\u001b[43minfo_likelihood\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 293\u001b[0m \u001b[43m \u001b[49m\u001b[43mtheory\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtheory\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 294\u001b[0m \u001b[43m \u001b[49m\u001b[43mpackages_path\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpackages_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 295\u001b[0m \u001b[43m \u001b[49m\u001b[43mtiming\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtiming\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 296\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 297\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m stop_at_error:\n\u001b[1;32m 298\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m component \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcomponents:\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/likelihood.py:318\u001b[0m, in \u001b[0;36mLikelihoodCollection.__init__\u001b[0;34m(self, info_likelihood, packages_path, timing, theory)\u001b[0m\n\u001b[1;32m 308\u001b[0m like_class \u001b[38;5;241m=\u001b[39m get_component_class(\n\u001b[1;32m 309\u001b[0m name,\n\u001b[1;32m 310\u001b[0m kind\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlikelihood\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 313\u001b[0m logger\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlog,\n\u001b[1;32m 314\u001b[0m )\n\u001b[1;32m 315\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m like_class \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 316\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39madd_instance(\n\u001b[1;32m 317\u001b[0m name,\n\u001b[0;32m--> 318\u001b[0m \u001b[43mlike_class\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 319\u001b[0m \u001b[43m \u001b[49m\u001b[43minfo\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 320\u001b[0m \u001b[43m \u001b[49m\u001b[43mpackages_path\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpackages_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 321\u001b[0m \u001b[43m \u001b[49m\u001b[43mtiming\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtiming\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 322\u001b[0m \u001b[43m \u001b[49m\u001b[43mstandalone\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 323\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 324\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m 325\u001b[0m )\n\u001b[1;32m 327\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_LikelihoodInterface(\u001b[38;5;28mself\u001b[39m[name]):\n\u001b[1;32m 328\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m LoggedError(\n\u001b[1;32m 329\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlog,\n\u001b[1;32m 330\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mLikelihood\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m \u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m is not actually a \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 331\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlikelihood (no current_logp attribute)\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 332\u001b[0m name,\n\u001b[1;32m 333\u001b[0m )\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/soliket/gaussian/gaussian.py:260\u001b[0m, in \u001b[0;36mMultiGaussianLikelihood.__init__\u001b[0;34m(self, info, **kwargs)\u001b[0m\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, info\u001b[38;5;241m=\u001b[39mempty_dict, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 259\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcomponents\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m info:\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlikelihoods: \u001b[38;5;28mlist\u001b[39m[Likelihood] \u001b[38;5;241m=\u001b[39m \u001b[43m[\u001b[49m\n\u001b[1;32m 261\u001b[0m \u001b[43m \u001b[49m\u001b[43mget_likelihood\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkv\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mkv\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mzip\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43minfo\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcomponents\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minfo\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43moptions\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 262\u001b[0m \u001b[43m \u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 264\u001b[0m default_info \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_defaults(input_options\u001b[38;5;241m=\u001b[39minfo)\n\u001b[1;32m 265\u001b[0m default_info\u001b[38;5;241m.\u001b[39mupdate(info)\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/soliket/gaussian/gaussian.py:261\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 258\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, info\u001b[38;5;241m=\u001b[39mempty_dict, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 259\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcomponents\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m info:\n\u001b[1;32m 260\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlikelihoods: \u001b[38;5;28mlist\u001b[39m[Likelihood] \u001b[38;5;241m=\u001b[39m [\n\u001b[0;32m--> 261\u001b[0m \u001b[43mget_likelihood\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkv\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m kv \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(info[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcomponents\u001b[39m\u001b[38;5;124m\"\u001b[39m], info[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moptions\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[1;32m 262\u001b[0m ]\n\u001b[1;32m 264\u001b[0m default_info \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_defaults(input_options\u001b[38;5;241m=\u001b[39minfo)\n\u001b[1;32m 265\u001b[0m default_info\u001b[38;5;241m.\u001b[39mupdate(info)\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/soliket/utils.py:55\u001b[0m, in \u001b[0;36mget_likelihood\u001b[0;34m(name, options)\u001b[0m\n\u001b[1;32m 53\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m options \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 54\u001b[0m options \u001b[38;5;241m=\u001b[39m {}\n\u001b[0;32m---> 55\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mt\u001b[49m\u001b[43m(\u001b[49m\u001b[43moptions\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/likelihoods/base_classes/InstallableLikelihood.py:58\u001b[0m, in \u001b[0;36mInstallableLikelihood.__init__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 48\u001b[0m not_or_old \u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 49\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mis not up to date\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m old \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhas not been correctly installed\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 50\u001b[0m )\n\u001b[1;32m 51\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m ComponentNotInstalledError(\n\u001b[1;32m 52\u001b[0m logger,\n\u001b[1;32m 53\u001b[0m (\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 56\u001b[0m ),\n\u001b[1;32m 57\u001b[0m )\n\u001b[0;32m---> 58\u001b[0m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__init__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/likelihood.py:96\u001b[0m, in \u001b[0;36mLikelihood.__init__\u001b[0;34m(self, info, name, timing, packages_path, initialize, standalone)\u001b[0m\n\u001b[1;32m 86\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21m__init__\u001b[39m(\n\u001b[1;32m 87\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 88\u001b[0m info: LikeDictIn \u001b[38;5;241m=\u001b[39m empty_dict,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 93\u001b[0m standalone\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[1;32m 94\u001b[0m ):\n\u001b[1;32m 95\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdelay \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m\n\u001b[0;32m---> 96\u001b[0m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__init__\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[1;32m 97\u001b[0m \u001b[43m \u001b[49m\u001b[43minfo\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 98\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 99\u001b[0m \u001b[43m \u001b[49m\u001b[43mtiming\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtiming\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 100\u001b[0m \u001b[43m \u001b[49m\u001b[43mpackages_path\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpackages_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 101\u001b[0m \u001b[43m \u001b[49m\u001b[43minitialize\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minitialize\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 102\u001b[0m \u001b[43m \u001b[49m\u001b[43mstandalone\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstandalone\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 103\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/theory.py:76\u001b[0m, in \u001b[0;36mTheory.__init__\u001b[0;34m(self, info, name, timing, packages_path, initialize, standalone)\u001b[0m\n\u001b[1;32m 66\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21m__init__\u001b[39m(\n\u001b[1;32m 67\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 68\u001b[0m info: TheoryDictIn \u001b[38;5;241m=\u001b[39m empty_dict,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 73\u001b[0m standalone\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[1;32m 74\u001b[0m ):\n\u001b[1;32m 75\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_measured_speed \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m---> 76\u001b[0m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__init__\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[1;32m 77\u001b[0m \u001b[43m \u001b[49m\u001b[43minfo\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 78\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 79\u001b[0m \u001b[43m \u001b[49m\u001b[43mtiming\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtiming\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 80\u001b[0m \u001b[43m \u001b[49m\u001b[43mpackages_path\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpackages_path\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 81\u001b[0m \u001b[43m \u001b[49m\u001b[43minitialize\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minitialize\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 82\u001b[0m \u001b[43m \u001b[49m\u001b[43mstandalone\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstandalone\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 83\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 85\u001b[0m \u001b[38;5;66;03m# set to Provider instance before calculations\u001b[39;00m\n\u001b[1;32m 86\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprovider: Provider \u001b[38;5;241m|\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/cobaya/component.py:401\u001b[0m, in \u001b[0;36mCobayaComponent.__init__\u001b[0;34m(self, info, name, timing, packages_path, initialize, standalone)\u001b[0m\n\u001b[1;32m 399\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 400\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m initialize:\n\u001b[0;32m--> 401\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minitialize\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 402\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m 403\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_params\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mstr\u001b[39m(e):\n", + "File \u001b[0;32m~/Projects/GitHub/SOLikeT/.venv/lib/python3.11/site-packages/mflike/mflike.py:89\u001b[0m, in \u001b[0;36m_MFLike.initialize\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 87\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdata_folder \u001b[38;5;241m=\u001b[39m os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mjoin(data_file_path, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdata_folder)\n\u001b[1;32m 88\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mexists(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdata_folder):\n\u001b[0;32m---> 89\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m LoggedError(\n\u001b[1;32m 90\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlog,\n\u001b[1;32m 91\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata_folder\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m directory does not exist. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 92\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCheck the given path [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdata_folder\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m].\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 93\u001b[0m )\n\u001b[1;32m 95\u001b[0m \u001b[38;5;66;03m# Read data\u001b[39;00m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_prepare_data()\n", + "\u001b[0;31mLoggedError\u001b[0m: The 'data_folder' directory does not exist. Check the given path [/tmp/LAT_packages/data/soliket_mflike/smooth_data/split_mflike/]." + ] + } + ], "source": [ "from cobaya.model import get_model\n", "\n", @@ -582,7 +636,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.12" + "version": "3.11.15" } }, "nbformat": 4, diff --git a/notebooks/first_step_tutorial.ipynb b/notebooks/first_step_tutorial.ipynb index 6c1692d5..953f560d 100644 --- a/notebooks/first_step_tutorial.ipynb +++ b/notebooks/first_step_tutorial.ipynb @@ -1,5 +1,21 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "ce2c3738", + "metadata": {}, + "source": [ + "# First steps with SOLikeT\n", + "\n", + "> **New here?** Start with [`quickstart.ipynb`](quickstart.ipynb) — it gets you a working\n", + "> likelihood in a few lines using the `soliket.presets` helpers (`quickstart`, `Session`).\n", + ">\n", + "> **This notebook** is the detailed deep-dive: it builds the same configuration *by hand* —\n", + "> parameter blocks, likelihood/theory wiring, the Cobaya `info` dict, YAML, and running an MCMC —\n", + "> so you understand every piece the presets assemble for you. For end-to-end workflows\n", + "> (simulating datasets, cross-covariances, joint analyses) see the [`ISO_sims/`](ISO_sims) notebooks." + ] + }, { "cell_type": "markdown", "id": "8f868abc", @@ -17,7 +33,7 @@ "- [Fiducial Parameter Values and Log-Posterior Evaluation](#fiducial-parameter-values-and-log-posterior-evaluation)\n", "- [From a Python Dictionary to a YAML Configuration](#from-a-python-dictionary-to-a-yaml-configuration)\n", "- [Sampler Block](#sampler-block)\n", - "- [Running Cobaya from the Notebook](#running-cobaya-from-the-notebook)\n", + "- [Running the Sampler](#running-the-sampler)\n", "\n", "## [Explore the Data](#explore-the-data)\n", "- [Inspecting the MFLike Data](#inspecting-the-mflike-data)\n", @@ -112,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "c18dd3f9", "metadata": {}, "outputs": [], @@ -183,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "d465f360", "metadata": {}, "outputs": [], @@ -229,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "e31ebaeb", "metadata": {}, "outputs": [], @@ -302,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "0ee59fb5", "metadata": {}, "outputs": [], @@ -347,44 +363,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "cf0adf41", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2026-02-02 16:35:51,505 [install] Installing external packages at '/Users/matteoforconi/Documents/GitHub/LAT_MFLike'\n", - "\n", - "================================================================================\n", - "likelihood:mflike.TTTEEE\n", - "================================================================================\n", - "\n", - " 2026-02-02 16:35:51,654 [install] 'mflike.TTTEEE' could not be found as internal, trying external.\n", - " 2026-02-02 16:35:52,191 [root] Numba not available, install it for better performance.\n", - " 2026-02-02 16:35:52,201 [install] Checking if dependencies have already been installed...\n", - " 2026-02-02 16:35:52,258 [install] External dependencies for this component already installed.\n", - " 2026-02-02 16:35:52,259 [install] Doing nothing.\n", - "\n", - "================================================================================\n", - "* Summary * \n", - "================================================================================\n", - "\n", - " 2026-02-02 16:35:52,259 [install] All requested components' dependencies correctly installed at /Users/matteoforconi/Documents/GitHub/LAT_MFLike\n" - ] - }, - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from cobaya.install import install\n", "from cobaya.tools import resolve_packages_path\n", @@ -429,7 +411,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "d74cc232", "metadata": {}, "outputs": [], @@ -485,7 +467,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "33505a66", "metadata": {}, "outputs": [], @@ -503,20 +485,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "b4dbdb69", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2026-02-02 16:36:01,707 [camb] `camb` module loaded successfully from /Users/matteoforconi/Documents/GitHub/CAMB-1.6.0/camb\n", - " 2026-02-02 16:36:08,433 [mflike.ttteee] Number of bins used: 3087\n", - " 2026-02-02 16:36:08,479 [mflike.ttteee] Initialized!\n" - ] - } - ], + "outputs": [], "source": [ "from cobaya.model import get_model\n", "\n", @@ -528,54 +500,70 @@ "id": "a51ce890", "metadata": {}, "source": [ - "### Fiducial Parameter Values and Log-Posterior Evaluation\n", + "### Fiducial parameter values and log-posterior evaluation\n", "\n", - "To test the model, we define fiducial values for both cosmological and foreground parameters, then compute the log-posterior at this specific point in parameter space." + "To test the model we evaluate the log-posterior at a fiducial point. Rather than hardcoding the values, we pull them from the **preset** with `load_fiducial_map()` — the same single source of truth the `quickstart` uses — so the tutorial and the presets never drift apart. The preset is only a *starting point*: any value can be overridden, as shown below.\n", + "\n", + "(One caveat: the preset is `H0`-based, while this hand-built block samples `theta_MC_100`. We therefore take the shared parameters from the preset and set `theta_MC_100` explicitly.)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "15f1550d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "logpost -> -48270\n" - ] - } - ], + "outputs": [], "source": [ + "from soliket.presets import load_fiducial_map\n", + "\n", + "# One source of truth for the fiducial point: load_fiducial_map() returns each\n", + "# parameter's spec; its reference (or fixed value) is the fiducial.\n", + "fiducial = load_fiducial_map()\n", + "\n", + "\n", + "def central(spec):\n", + " \"\"\"The fiducial value of a parameter spec (a fixed value or its reference).\"\"\"\n", + " if \"value\" in spec:\n", + " return spec[\"value\"]\n", + " ref = spec[\"ref\"]\n", + " return ref[\"loc\"] if isinstance(ref, dict) else ref\n", + "\n", + "\n", + "# Cosmology: take the parameters shared with this (theta_MC_100-based) block from\n", + "# the preset, and set theta_MC_100 explicitly (the preset is H0-based).\n", "cosmo_params_camb = {\n", - " \"theta_MC_100\": 1.04090,\n", - " \"logA\": 3.045,\n", - " \"ombh2\": 0.02236,\n", - " \"omch2\": 0.1202,\n", - " \"ns\": 0.9649,\n", - " \"tau\": 0.0544,\n", + " p: central(fiducial[\"cosmo\"][p]) for p in (\"logA\", \"ombh2\", \"omch2\", \"ns\", \"tau\")\n", "}\n", + "cosmo_params_camb[\"theta_MC_100\"] = 1.04090\n", + "\n", + "# Foregrounds: every parameter this model needs is carried by the preset.\n", + "fg_keys = (\n", + " \"a_tSZ\",\n", + " \"a_kSZ\",\n", + " \"alpha_tSZ\",\n", + " \"a_p\",\n", + " \"beta_p\",\n", + " \"beta_s\",\n", + " \"a_c\",\n", + " \"beta_c\",\n", + " \"a_s\",\n", + " \"T_d\",\n", + " \"a_gtt\",\n", + " \"a_gte\",\n", + " \"a_gee\",\n", + " \"a_psee\",\n", + " \"a_pste\",\n", + " \"xi\",\n", + ")\n", + "fg_params = {p: central(fiducial[\"foreground\"][p]) for p in fg_keys}\n", "\n", - "fg_params = {\n", - " \"a_tSZ\": 3.30,\n", - " \"a_kSZ\": 1.60,\n", - " \"a_p\": 6.90,\n", - " \"beta_p\": 2.08,\n", - " \"a_c\": 4.90,\n", - " \"beta_c\": 2.20,\n", - " \"a_s\": 3.10,\n", - " \"T_d\": 9.60,\n", - " \"a_gtt\": 2.81,\n", - " \"a_gte\": 0.10,\n", - " \"a_gee\": 0.10,\n", - " \"a_psee\": 0.000,\n", - " \"a_pste\": 0.000,\n", - " \"xi\": 0.20,\n", - "}\n", + "print(\n", + " \"logpost (preset fiducial) ->\", f\"{model.logpost(cosmo_params_camb | fg_params):.6g}\"\n", + ")\n", "\n", - "print(\"\\nlogpost ->\", f\"{model.logpost(cosmo_params_camb | fg_params):.6g}\")" + "# The preset is only a starting point — override any value and re-evaluate.\n", + "shifted = {**cosmo_params_camb, \"tau\": 0.08} | fg_params\n", + "print(\"logpost (tau = 0.08) ->\", f\"{model.logpost(shifted):.6g}\")" ] }, { @@ -585,39 +573,17 @@ "source": [ "### From a Python dictionary to a YAML configuration\n", "\n", - "Up to this point the input has been assembled as a Python dictionary. This is convenient to better understand all the blocks, but in practice Cobaya analyses are usually run from a YAML configuration file\n", - "\n", - "In the next cell we therefore:\n", - "\n", - "**Write the dictionary to disk as `tutorial.yaml`** \n", - "This creates a configuration file that can be reused, modified and shared.\n", + "Up to here the input has been a Python dictionary — convenient for understanding the blocks. In practice a configuration is often **serialized to YAML** so it can be reused, shared, or launched on a cluster. We write `tutorial.yaml`, rebuild the model straight from it to confirm the round-trip, and evaluate at the fiducial point.\n", "\n", - "**Load the model directly from the YAML file** \n", - "Using `get_model(\"tutorial.yaml\")`, Cobaya rebuilds the full pipeline (theory + likelihood + parameters).\n", - "\n", - "**Evaluate the posterior for a chosen parameter point** \n", - "Finally, we compute `logpost` at our fiducial cosmology and foreground parameters.\n", - "\n", - "A major advantage of this workflow is that if you want to change any option (e.g. likelihood settings, `extra_args`, priors, or sampled parameters), you can usually do it by editing the **YAML file**, without touching the Python code." + "We will still *run* the sampler from Python below; `cobaya-run tutorial.yaml` is the equivalent command-line entry point." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "a6ceefc1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "2904" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from pathlib import Path\n", "\n", @@ -633,23 +599,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "f3b686c4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2026-02-02 16:36:17,637 [model] *WARNING* Ignored blocks/options: ['output']\n", - " 2026-02-02 16:36:17,654 [camb] `camb` module loaded successfully from /Users/matteoforconi/Documents/GitHub/CAMB-1.6.0/camb\n", - " 2026-02-02 16:36:22,661 [mflike.ttteee] Number of bins used: 3087\n", - " 2026-02-02 16:36:22,701 [mflike.ttteee] Initialized!\n", - "\n", - "logpost -> -48270\n" - ] - } - ], + "outputs": [], "source": [ "model = get_model(\"tutorial.yaml\")\n", "print(\"\\nlogpost ->\", f\"{model.logpost(cosmo_params_camb | fg_params):.6g}\")" @@ -662,43 +615,32 @@ "source": [ "### Sampler block\n", "\n", - "So far we have only *evaluated* the posterior at a single point. To actually **sample** the posterior and produce chains, Cobaya needs a `sampler` block in the YAML configuration. Therefore an MCMC sampler is added to the dictionary and we replace the previous YAML. \n", + "So far we have only *evaluated* the posterior at a single point. To actually **sample** it and produce chains, Cobaya needs a `sampler` block. We add it to the `info` dictionary (the same block would go into the YAML).\n", "\n", "Important options are:\n", "\n", "**Maximum tries** \n", - "Sort of a safety net if the sampler struggles to find valid points.\n", + "A safety net if the sampler struggles to find valid points.\n", "\n", "**Parameter covariance** \n", - "If present thanks to a previous similar run, the path can be passed to speed up convergence. Otherwise Cobaya constuct a default one from the proposals. \n", + "If a covariance from a previous similar run is available, its path can be passed to speed up convergence. Otherwise Cobaya builds a default one from the proposals.\n", "\n", "**Proposal scale** \n", - "Global factor in front of all the parameters proposals.\n", + "Global factor in front of all the parameter proposals.\n", "\n", "**R-1** \n", "Convergence threshold using the Gelman–Rubin statistic: stop when `R - 1` drops below this value.\n", "\n", "**Max samples** \n", - "Hard cap on the number of samples. This is set tiny here only as a quick test that the pipeline runs; for real chains it is omitted" + "Hard cap on the number of samples. Set tiny here only as a quick test that the pipeline runs; for real chains it is omitted." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "7d39c11a", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "3126" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "info[\"sampler\"] = {\n", " \"mcmc\": {\n", @@ -709,14 +651,10 @@ " \"proposal_scale\": 1.0,\n", " \"Rminus1_stop\": 0.02,\n", " \"Rminus1_cl_stop\": 0.2,\n", - " \"max_samples\": 7,\n", + " \"max_samples\": 7, # tiny: just checks the sampler runs; omit for real chains\n", " }\n", "}\n", - "info[\"output\"] = \"test_chains/chain_mflike\"\n", - "\n", - "out.write_text(\n", - " yaml.dump(info, sort_keys=False, default_flow_style=False), encoding=\"utf-8\"\n", - ")" + "info[\"output\"] = \"test_chains/chain_mflike\"" ] }, { @@ -724,84 +662,33 @@ "id": "8854c570", "metadata": {}, "source": [ - "### Running Cobaya from the notebook\n", + "### Running the sampler\n", + "\n", + "With a `sampler` block in `info`, we launch Cobaya directly from Python with `cobaya.run(info)` — the same interface the [`quickstart`](quickstart.ipynb) and the `ISO_sims` notebooks use. It returns the updated info and the sampler object, so the products stay in the notebook. The run is guarded by `RUN_MCMC` and capped at `max_samples = 7` (a smoke test).\n", + "\n", + "On a cluster you would instead launch the serialized configuration from the command line:\n", "\n", - "Once `tutorial.yaml` contains both the model and the `sampler` block, we can run Cobaya directly from the notebook using its command-line interface. `--test` runs a very short execution to check that everything is correct whereas `-f` force to overwrite already existing chaiins with the same name. If the yaml file is modified but not the `output` field, without `-f` the command raise an error. To resume an existing chain replace it with `-r`\n" + "```bash\n", + "cobaya-run tutorial.yaml # -r to resume an existing chain, -f to overwrite\n", + "```" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "906da1f4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[output] Output to be read-from/written-into folder 'test_chains', with prefix 'chain_mflike'\n", - "[output] Found existing info files with the requested output prefix: 'test_chains/chain_mflike'\n", - "[output] Will delete previous products ('force' was requested).\n", - "[camb] `camb` module loaded successfully from /Users/matteoforconi/Documents/GitHub/CAMB-1.6.0/camb\n", - "[mflike.ttteee] Number of bins used: 3087\n", - "[mflike.ttteee] Initialized!\n", - "[mcmc] Getting initial point... (this may take a few seconds)\n", - "[prior] Reference values or pdfs for some parameters were not provided. Sampling from the prior instead for those parameters.\n", - "[mcmc] Initial point: theta_MC_100:1.041133, logA:3.039515, ombh2:0.02235377, omch2:0.1201465, ns:0.9651119, tau:0.05445908, a_tSZ:3.065179, a_kSZ:1.437613, a_p:6.891417, beta_p:1.809228, a_c:4.534292, beta_c:2.246655, a_s:3.081244, a_gtt:2.987446, xi:0.1977115, T_d:8.621974, a_gte:0.3083717, a_pste:-0.2862274, a_gee:0.1032605, a_psee:0.9868258\n", - "[model] Measuring speeds... (this may take a few seconds)\n", - "[model] Setting measured speeds (per sec): {mflike.TTTEEE: 96.6, camb.transfers: 0.789, camb: 1.07, mflike.BandpowerForeground: 127.0}\n", - "[mcmc] Dragging with number of interpolating steps:\n", - "[mcmc] * 1 : [['theta_MC_100', 'ombh2', 'omch2', 'tau'], ['logA', 'ns']]\n", - "[mcmc] * 14 : [['a_tSZ', 'a_kSZ', 'a_p', 'beta_p', 'a_c', 'beta_c', 'a_s', 'a_gtt', 'xi', 'T_d', 'a_gte', 'a_pste', 'a_gee', 'a_psee']]\n", - "[mcmc] Covariance matrix not present. We will start learning the covariance of the proposal earlier: R-1 = 30 (would be 2 if all params loaded).\n", - "[run] Test initialization successful! You can probably run now without `--test`.\n" - ] - } - ], - "source": [ - "!cobaya-run tutorial.yaml --test -f" - ] - }, - { - "cell_type": "markdown", - "id": "2a8f1d5f", - "metadata": {}, - "source": [ - "After the successful initialization, we can proceed to run the mcmc" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "b3c77a39", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[output] Output to be read-from/written-into folder 'test_chains', with prefix 'chain_mflike'\n", - "[output] Found existing info files with the requested output prefix: 'test_chains/chain_mflike'\n", - "[camb] `camb` module loaded successfully from /Users/matteoforconi/Documents/GitHub/CAMB-1.6.0/camb\n", - "[mflike.ttteee] Number of bins used: 3087\n", - "[mflike.ttteee] Initialized!\n", - "[mcmc] Getting initial point... (this may take a few seconds)\n", - "[prior] Reference values or pdfs for some parameters were not provided. Sampling from the prior instead for those parameters.\n", - "[mcmc] Initial point: theta_MC_100:1.040544, logA:3.049188, ombh2:0.02236315, omch2:0.1201584, ns:0.9644555, tau:0.05434677, a_tSZ:3.375564, a_kSZ:1.489677, a_p:6.57533, beta_p:1.817359, a_c:4.850605, beta_c:2.059293, a_s:3.111875, a_gtt:3.378982, xi:0.1592469, T_d:9.850543, a_gte:0.3552475, a_pste:-0.1678168, a_gee:0.1319006, a_psee:0.6922626\n", - "[mcmc] *WARNING* Parameter blocking manually/previously fixed: speeds will not be measured.\n", - "[mcmc] Dragging with number of interpolating steps:\n", - "[mcmc] * 1 : (['theta_MC_100', 'ombh2', 'omch2', 'tau'], ['logA', 'ns'])\n", - "[mcmc] * 14 : (['a_tSZ', 'a_kSZ', 'a_p', 'beta_p', 'a_c', 'beta_c', 'a_s', 'a_gtt', 'xi', 'T_d', 'a_gte', 'a_pste', 'a_gee', 'a_psee'],)\n", - "[mcmc] Covariance matrix not present. We will start learning the covariance of the proposal earlier: R-1 = 30 (would be 2 if all params loaded).\n", - "[mcmc] Sampling!\n", - "[mcmc] Progress @ 2026-02-02 16:37:09 : 1 steps taken, and 0 accepted.\n", - "[mcmc] Reached maximum number of accepted steps allowed (7). Stopping.\n", - "[mcmc] Sampling complete after 7 accepted steps.\n" - ] - } - ], + "outputs": [], "source": [ - "!cobaya-run tutorial.yaml" + "RUN_MCMC = False # set True to launch the sampler (a tiny test run; minutes)\n", + "\n", + "if RUN_MCMC:\n", + " from cobaya import run\n", + "\n", + " updated_info, sampler = run(info)\n", + " print(\"done:\", sampler.products()[\"sample\"].shape)\n", + "else:\n", + " print(\"RUN_MCMC is False - skipping the sampler.\")" ] }, { @@ -809,12 +696,12 @@ "id": "2074f613", "metadata": {}, "source": [ - "Eventually, we can remove the yaml file used for the exemplary run above" + "Finally, we can remove the `tutorial.yaml` written above." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "d8114157", "metadata": {}, "outputs": [], @@ -840,12 +727,12 @@ "source": [ "### Inspecting the MFLike data \n", "\n", - "As we have seen, MFLike uses data stored in the **SACC** format (a FITS file containing bandpowers, covariance, window functions, and metadata). To inspect what is inside, the data can be loaded and the infomrations can be retrieved by printing the metadata." + "As we have seen, MFLike uses data stored in the **SACC** format (a FITS file containing bandpowers, covariance, window functions, and metadata). To inspect what is inside, the data can be loaded and the information can be retrieved by printing the metadata." ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "02c82dfc", "metadata": {}, "outputs": [], @@ -870,30 +757,25 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "3c89b5af", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "logpost -> -41762.1\n" - ] - } - ], + "outputs": [], "source": [ "fiducial_cosmo_param = ast.literal_eval(s.metadata[\"cosmo_params\"])\n", "fiducial_cosmo_param[\"theta_MC_100\"] = fiducial_cosmo_param[\"cosmomc_theta\"] * 100\n", "fiducial_cosmo_param.pop(\"cosmomc_theta\")\n", "fiducial_cosmo_param.pop(\"Alens\")\n", "\n", - "fiducial_foreground_param = ast.literal_eval(s.metadata[\"fg_params\"])\n", - "fiducial_foreground_param.pop(\"a_gtb\")\n", - "fiducial_foreground_param.pop(\"a_gbb\")\n", - "fiducial_foreground_param.pop(\"a_pstb\")\n", - "fiducial_foreground_param.pop(\"a_psbb\")\n", + "# The data's foreground fiducial. The v0.8 SACC predates a couple of params the current\n", + "# mflike samples (beta_s, alpha_tSZ) and stores an older T_d, so we start from the\n", + "# preset fiducial (complete and model-accepted) and override with the values the data\n", + "# was generated with where they still apply.\n", + "sacc_fg = ast.literal_eval(s.metadata[\"fg_params\"])\n", + "fiducial_foreground_param = dict(fg_params)\n", + "fiducial_foreground_param.update(\n", + " {p: v for p, v in sacc_fg.items() if p in fg_params and p != \"T_d\"}\n", + ")\n", "\n", "print(\n", " \"\\nlogpost ->\",\n", @@ -914,38 +796,42 @@ "id": "8f079341", "metadata": {}, "source": [ - "Once the Cobaya `model` is built, we can access its internal components directly. This is useful, for example, if we want to compare **data** vs **model predictions** for specific spectra.\n", + "Once the Cobaya `model` is built, we can reach its components by **role** rather than by fragile positional indexing. `resolve_aliases(model)` (from `soliket.presets` — the same helper the quickstart uses) matches each component by class, so it survives any reordering of the `info` dict:\n", "\n", - "**Components**\n", - "- `mflike_likelihood = model.components[0]` selects the MFLike likelihood object.\n", - "- `foreground_theory = model.components[3]` selects the foreground theory object.\n", + "- `roles.mflike` — the MFLike likelihood object\n", + "- `roles.foreground` — the foreground theory object\n", + "- `roles.cosmo` — the Boltzmann (CAMB/CLASS) theory\n", + "- `roles.lensing` — the lensing likelihood (added later, once we include it)\n", "\n", - "Depending on how your YAML is ordered, these indices can change; the goal is simply to grab the MFLike likelihood and the foreground theory instances. From that is possible to obtain the spectra rpovided to the likelihood. \n", + "From these we can compare **data** vs **model predictions** for specific spectra.\n", "\n", "**Cl** \n", - "With ell_factor=True, the output is returned in the common $D_\\ell$ convention\n", + "With `ell_factor=True`, the output is returned in the common $D_\\ell$ convention.\n", "\n", "**fg** \n", - "The foreground object is composed as fg_total[`pol`][`frequency`,`frequency`][$\\ell$]\n", + "The foreground object is composed as `fg_total[pol][frequency, frequency][ell]`.\n", "\n", "**Data** \n", - "To get the data from the sacc file, we need to indicate the name of the spectra (`cl_00`,`cl_0e` or `cl_ee`), name of the experiment (`LAT_93_s0`,`LAT_93_s2`,`LAT_145_s0`, ...) and wether we want the relative covariance matrix and indeces returned. " + "To get the data from the SACC file we indicate the spectrum (`cl_00`, `cl_0e` or `cl_ee`), the experiment (`LAT_93_s0`, `LAT_93_s2`, `LAT_145_s0`, ...), and whether we want the covariance matrix and indices returned." ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "b0e65092", "metadata": {}, "outputs": [], "source": [ - "mflike_likelihood = model.components[0]\n", - "foreground_theory = model.components[3]" + "from soliket.presets import resolve_aliases\n", + "\n", + "roles = resolve_aliases(model) # components by role, robust to reordering\n", + "mflike_likelihood = roles.mflike\n", + "foreground_theory = roles.foreground" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "92ada7c1", "metadata": {}, "outputs": [], @@ -956,7 +842,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "b9e8332e", "metadata": {}, "outputs": [], @@ -978,21 +864,10 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "1b34dc82", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAf1pJREFUeJzt3Xd4VHXaxvHvzKT3hIT0RidUCR3pKoK9t7WiKyuuL6KuYNdV0HVx3VVwLauoqyv2tbAq0nvvvYUkEAhJIL3OnPePIQORUAJJZjK5P9eVi8w5Z848Gcdw86smwzAMRERERKTJMzu7ABERERGpHwp2IiIiIm5CwU5ERETETSjYiYiIiLgJBTsRERERN6FgJyIiIuImFOxERERE3ISCnYiIiIibULATERERcRMKdiIiIiJuQsFORAD417/+RUBAAAEBAXh5eeHp6el4bDKZTnlu2LBhzi5dRESOMWmvWBH5rd///vcEBATw2muv1emciIg4l1rsROQkGzZsoGvXrnU+JyIizqVgJ9JMrFixgrvvvptWrVrh6+tLQEAAvXv35uWXX6awsNBxnWEYbNq0qdbwdrpzp3vdESNGEBgYSEBAAEOHDmXx4sUnXbdu3Touu+wyEhIS8PX1JSwsjH79+vHvf/+7Tj/nhg0bGD16NK1bt8bX1xdfX1/atm3L/fffz6pVq2pcO336dEwm00nHq11++eUkJSXV6fUby9m8r3pPRZofBTuRZmDixIn07duXXbt28cQTTzBz5kw+/fRTevXqxfPPP8/f//53x7W7d++mvLycTp06nXSf052rzcqVKxk0aBClpaV8/PHHfPzxx5SVlTF8+HCWLl1a49qjR48SHx/PpEmTmDlzJh999BFJSUncfvvtvPjii2f1em+//TapqaksX76c//u//+OHH37gxx9/ZNy4cWzevJlevXqxe/fus7qXKzvb91XvqUgzZIiIW3v22WcNwHjuuedqPb9lyxZj5cqVjsdffvmlkZKSUuu1pztXmxEjRhiRkZFGcXGx41hBQYERHh5u9O/f/6zu0adPHyM+Pv6M1y1atMgwm83GFVdcYZSXl9d6zeeff27s37/f8fiDDz4wgBo//4kuu+wyIzEx8azqPJP8/Hxj5syZpzz//fffG0VFRWd1r/N9X93lPRWRk6nFTsSNrV69mhdffJHrr7+eZ599ttZrOnbsSM+ePR2P63N83eLFixkyZAh+fn6OY4GBgQwaNIglS5aQlZV1xnuEh4fj4eFxxusmTZqExWLh7bffxsvLq9ZrbrjhBmJiYs66/t8ymUyn/EpLSzvtc9977z0uv/xyPvzww5POvf3221x55ZV89NFHZ1XH+b6vrvSewvm9ryJS05n/zxaRJuvFF1/EZrOddbcb2MNb796963yuNhUVFXh7e590vPrYxo0biY6OrnHOZrNhs9k4cuQIX3zxBT///DNvvvnmaV/HarUyd+5cevbsedL9zobVaqWqquqk48ZvFg34bfdxaWkpt99+O1arlbCwsNO+xvjx49m/fz933303ZWVl3H///QD8/e9/Z9y4cTzxxBP84Q9/OKt66/q+uvJ7Cuf3vopITQp2Im6quLiYmTNnMnDgQNq3b3/Wz9uwYQP33ntvnc/VJiUlhWXLlmGz2TCb7R0EVVVVLF++HIDc3NyTnvPAAw/w9ttvA+Dl5cU//vEPRwg6lZycHEpLS0lMTDzpnNVqrREmLBYLJpOpxjV9+/Y95b1PvOeJ11mtVq677jry8/OZP38+QUFBp60RYMqUKfj4+DBmzBjKysooKytjwoQJvPDCCzz99NNnfH61ur6vrvye/vbac3lfReQ4BTsRN7V582YqKiro3r17nZ53uoHwdR0k/8c//pHRo0fz4IMP8uSTT2Kz2Xj++efZt28fgCOUnOiJJ57g3nvvJTs7m++//54HH3yQ4uJiHn300Tq9drXU1FTWr1/vePzqq6+edK+PPvqIjh07nvTchx9+mIyMjFrv++CDD/Ljjz/y/fff06NHj7Ou56WXXsLHx4dx48adsp4zqev72lTeUzj391VE7BTsRNzU0aNHAerUjfbxxx/zyy+/8OGHH9Yauurqnnvu4fDhw7z44ou89dZbAPTr149HH32UV155hdjY2JOek5CQQEJCAgCjRo0C7LN677zzTiIiImp9nfDwcHx9fR3B5kSffvopJSUlZGVlceWVV9b6/N+OM6wWHBxcawh58cUX+ec//8m//vUvLr300lP89KeWn5+PyWTCMAzHf6e6qOv72hTeUzj/91VEtNyJiNuKiooCqPUv5tpkZmbyt7/9jf3799faRXquHn/8cXJycti4cSNpaWksWbKEI0eO4O/vT2pq6hmf37t3b6qqqtizZ88pr7FYLAwbNoxVq1adNHEgJSWFnj170qVLl/P+WcC+TtvTTz/Nc889xz333FOn5xqGwYMPPsjf/vY3/vWvf/HWW28xadKkc2o5O5/31dXeUzi/91VEjlOwE3FTXbp0oVWrVnz++edkZ2fXes3s2bMd33/55Zf87ne/4+jRo4SHh9drLd7e3nTu3JnExETS09OZMWMG9913H76+vmd87ty5czGbzbRq1eq0102cOBGr1cqYMWOorKysr9Jr+Omnn7jvvvu45557TjnL+FRsNhv33Xcfb7/9Np988gl33303Y8aM4YMPPuD1119n7NixtU4sOJ1zfV9d6T2F83tfRaQmdcWKuCmTycS7777LqFGj6NmzJ4899hgpKSlYrVY2btzIjBkz8PX1Zfjw4QDMmjWLm2++ma5du540EP5cbdq0ia+++oqePXvi7e3N+vXrefnll2nbti1//vOfa1z7+9//nqCgIHr37k1kZCQ5OTl88cUXzJgxg8cee+yUXYbVBgwYwNSpU/njH/9Ijx49+P3vf0+nTp0wm81kZWXx1VdfAZzzYPy9e/dyww030KpVK+6++26WLVtW4/wFF1xQ60zVaq+99hoff/wxX3zxBVdffbXj+J133omPjw+/+93v6NixIw8++OAZaznb99XV31M4//dVRH7DiWvoiUgj2Lhxo3HbbbcZMTExhqenpxEREWGkpqYajz76qLFmzRrHdbGxsca4ceOMJUuW1Ntrb9++3Rg0aJARFhZmeHl5GW3atDGeeuqpWhfiff/9942BAwca4eHhhoeHhxESEmIMHjzY+Pjjj+v0muvWrTPuvvtuIzk52fD29jZ8fHyMNm3aGHfccYcxe/bsGtfWZTHduXPnGsApv/bu3XvaukpKSk773i5atOiUiwD/1tm+r67+nhrG+b+vIlKTyTDq2PYvIm6nuLiYDh060KdPH7788ktnlyMiIudIwU5EOHDgAImJiaxZs6ZeB8SLiEjj0uQJEWHjxo0kJibSpUsX5syZ4+xyRETkHCnYiTRzVquV//znP9xyyy0MGjSI/fv3O7skERE5R+qKFREREXETarETERERcRMKdiIiIiJuQsFORERExE0o2ImIiIi4CbfbUsxms3HgwAECAwPrbVskEREREWcxDIPCwkJiYmIwm0/fJud2we7AgQPEx8c7uwwRERGRepWRkUFcXNxpr3G7YBcYGAjYf/jz2ZhaRERExBUUFBQQHx/vyDin43bBrrr7NSgoSMFORERE3MbZDDFzm8kTU6dOJSUlhV69ejm7FBERERGncLudJwoKCggODiY/P18tdiIiItLk1SXbuE2LnYiIiEhz53Zj7ERERFyZ1WqlsrLS2WWIC7FYLHh4eNTLMm1uE+ymTp3K1KlTsVqtzi5FRESkVkVFRWRmZuJmo6CkHvj5+REdHY2Xl9d53Udj7ERERBqB1Wpl586d+Pn5ERERoUX0BbAvPlxRUcHhw4exWq20bdv2pEWI65Jt3KbFTkRExJVVVlZiGAYRERH4+vo6uxxxIb6+vnh6erJv3z4qKirw8fE553tp8oSIiEgjUkud1OZMW4Wd9X3q5S4uQOvYiYiISHPnNsFu7NixbNmyhZUrVzq7FBERkWYjLS0Nk8nEunXrnF2K4EbBTkREROqXyWQ67dddd93l7BLlNzR5QkRERGqVlZXl+H7GjBk888wzbN++3XHM19eXI0eOOKM0rFYrJpOp3samuQu9GyIirmDbTPhpov1PERcRFRXl+AoODsZkMp10rNqePXsYOnQofn5+dOvWjaVLl9a415IlSxg0aBC+vr7Ex8fz0EMPUVxc7Dh/5MgR7rjjDkJDQ/Hz82PkyJHs3LnTcX769OmEhITwww8/kJKSgre3NwsXLsTT05ODBw/WeK1HHnmEQYMGNdC74trcJthp8oSINFnbZsJnt8Dyt+1/Ktw1C4ZhUFludcpXQyxh++STT/Loo4+ybt062rVrxy233EJVVRUAGzduZMSIEVx77bVs2LCBGTNmsGjRIh588EHH8++66y5WrVrFd999x9KlSzEMg1GjRtXYpaOkpITJkyfz3nvvsXnzZnr27EmrVq34+OOPHddUVVXx73//m7vvvrvef8amwG26YseOHcvYsWMdi/iJiLgywzAoya+gMK8MnxU/E2yyYDKsGCYLxet+xWg5jIBQ75pLY2ybCWkLIWkgdBjlvOKlXlRV2Hjn/+Y75bV///fBeHpb6vWejz76KJdddhkAzz//PJ06dWLXrl106NCBV199lVtvvZVx48YB0LZtW/7xj38wePBg3nrrLTIyMvjuu+9YvHgx/fv3B+CTTz4hPj6eb7/9lhtuuAGwrwU4bdo0unXr5njd0aNH88EHH/DYY48B8OOPP1JSUsKNN95Yrz9fU+E2wU5ExOWcEMSM9iPJyyomfVMemdvzOJxeSGmhvSUiyTuay0Kt2AwzZqzMX9qStHlL8A30JLpNCK26R9DafxUeX94GJgssmwY3/0fhTlxK165dHd9HR0cDkJ2dTYcOHVi9ejW7du3ik08+cVxjGAY2m429e/eyc+dOPDw86NOnj+N8ixYtaN++PVu3bnUc8/LyqvE6YG/pe+qpp1i2bBl9+/bl/fff58Ybb8Tf37+hflSXpmAnItIQjnWvGiYLpmXTmFv1LFtzute4xGSCgDAfyoIvYoUlkBaVazls6sYRa0/MeWWUFlayZ+1h9qw9THnIDLr42Fv1MFkgbZGCXRPn4WXm938f7LTXrm+enp6O76tbmm02m+PP+++/n4ceeuik5yUkJLBjx45a72kYRo1Wa19f35MWeG7ZsiVXXHEFH3zwAa1atWLmzJnMmzfvfH+cJkvBTkSkntlsBgUrfiYIM2bD3hIXVr4Gi0cPYtuFkNCpBZGtgmgRG4CnV3V3WE8AWgN9gapKK4f3FZK+NY8dKw6RUdCJrj7fOVr1rAn9qd+ONGlsJpOp3rtDXVWPHj3YvHkzbdq0qfV8SkoKVVVVLF++3NEVm5uby44dO+jYseMZ73/vvfdy8803ExcXR+vWrRkwYEC91t+UuE2wmzp1KlOnTsVqtTq7FBFppiorrGxbksW6X9MJK4zmslCbPYiZbLQcPIrRlww8Icidnoenheg2IUS3CaH35clkbGnHkv/64XdkBfsrOlPwZQSXjC6iRWxAA/9UIufv8ccfp2/fvowdO5b77rsPf39/tm7dyqxZs3jjjTdo27YtV111Fffddx9vv/02gYGBTJgwgdjYWK666qoz3n/EiBEEBwfz4osv8sILLzTCT+S63GZWrHaeEBFnsVbZ2DQ/k38/vZQFn+2gIKeMLI/+bG/1N6w97oOb/0PM5beedaj7LZPJREKnFvSb+Ef8bvwrh7wGkHegmC9fWcXOlYfq+acRqX9du3Zl/vz57Ny5k4EDB3LBBRfw9NNPO8biAXzwwQekpqZy+eWX069fPwzDYObMmTW6eE/FbDZz1113YbVaueOOOxryR3F5JqMh5jw7UfWs2Pz8fIKCgpxdjoi4MZvNYOfKQ6z4fg8FOWUABIb5cMElCXToF91g3WylhRX88q/NZG6zLww74Po2dL8oQbNmXVxZWRl79+4lOTkZHx8fZ5fjdu677z4OHTrEd9995+xSzsnpPh91yTZu0xUrItKYsnbns3DGDg6nFwLgG+RFz5FJdLowBotnw3aG+AZ6ccVD3Vny1S7Wz85g8Ze78Mv6lXa7H9asWWl28vPzWblyJZ988gn//e9/nV2O0ynYiYicjWOtYWUt+7JwfRt2LLd3gXr5etBjRAJdh8Y36kB4s9nEgOvb4BvoybJv91CyYQ6Gv2bNSvNz1VVXsWLFCu6//34uvvhiZ5fjdAp2IiJnUr10CRZ8mEbFkYlg6k1K/2j6XNUavyAvp5RlMplIvTQJwwb7f+5Md//v7curGFZIutApNYk0tua8tEltFOxERM6geP2v+GJfZsRmmGkbvoOetz9AZJJrjONNHZnIosJr+XEJxPlsJumKqwlWa51Is+Q2s2K1V6yI1LfKciuLvtjJ/KUtMXN86ZK211/nMqEO7C13F97QFqPtSBbl3813P0VTVlx55ieKiNvRrFgRkVqkb85l3qfbKcy1z3Yd0GUHnRP24tF2sMuOXSsrruSLySspyCkjqUsLRj3Q9aRV+sV5NCtWTkezYkVEGkBZUSWLvtjJ9uUHAfvyJYNva09ip2FOruzMfPw9ufT+Lnz5yirSNuaydUkWKQNinF2WiDQiBTsREex7Uu5cdYhFn++ktLASTNBtaDy9r0zGy6fp/KqMiA+kz5WtWPr1bhZ9vpO49qEEhfs6uywRaSRN57eViEgDKcwrY/6n29m3KReAsBh/ht7egajkYCdXdm66X5RA2oYcsnblM+fjbVw1rjum7f/T4sUizYDbTJ4QEakrm81gw9xM/vP8cvZtysXsYaLPlcnc+ESvJhvqwL7G3fA7O2LxNLN/+xEO/PApfHYLLH/b/ue2mc4uUcTtTJ8+nZCQEGeXoWAnIs1T7oEivvnrahbO2EFluZXoNsHc/FRveo5KxuLR9H81Bkf40XNkEgBHl/2MYbLAiYsXi5ylu+66C5PJdNLXrl27nF2a1EJdsSLSrFgrbaz+KY3VP+3DZjXw9LHQ7+rWdB4Ui8nsXjNIL7g4ge3LD5J2NIVOof+1hzotXizn4NJLL+WDDz6ocSwiIqLO96moqMDLq2EW9G7IezclTf+fpSIiZ+ngnnxmTFrJyh/TsFkNkrqGc+uzfegyJM7tQh2AxdPMoJvbkVbem5lHJ1La+R7tISvnxNvbm6ioqBpfFouF+fPn07t3b7y9vYmOjmbChAlUVVU5njdkyBAefPBBxo8fT3h4uGPLry1btjBq1CgCAgKIjIzk9ttvJycnx/G8wsJCbrvtNvz9/YmOjuZvf/sbQ4YMYdy4cY5rkpKSePHFF7nrrrsIDg7mvvvuA+Crr76iU6dOeHt7k5SUxJQpU2r8LCaTiW+//bbGsZCQEKZPnw5AWloaJpOJr7/+mqFDh+Ln50e3bt1YunRpjedMnz6dhIQE/Pz8uOaaa8jNza1xfv369QwdOpTAwECCgoJITU1l1apV5/T+14XbBDstUCwitdo2E+sPj7Pp3Xf56tXVHMkqxjfQk0vu7cSoP3QhINS91xOL7xhGqwsi2FvWm9kHb1eocyGGYVBSWeKUr/pYwnb//v2MGjWKXr16sX79et566y3+9a9/8eKLL9a47sMPP8TDw4PFixfz9ttvk5WVxeDBg+nevTurVq3ip59+4tChQ9x4442O54wfP57Fixfz3XffMWvWLBYuXMiaNWtOquHVV1+lc+fOrF69mqeffprVq1dz4403cvPNN7Nx40aee+45nn76aUdoq4snn3ySRx99lHXr1tGuXTtuueUWR2hdvnw599xzDw888ADr1q1j6NChJ/3ct912G3FxcaxcuZLVq1czYcIEPD0961xHXblNV+zYsWMZO3asYxE/EZHqPV5NhpnOJhv7vCbik3oVA65rg49/w/+CdRX9rm5N2voc9m3MJXP7EeLahzq7JAFKq0rp82kfp7z28luX4+fpd9bX//DDDwQEBDgejxw5knbt2hEfH8+bb76JyWSiQ4cOHDhwgMcff5xnnnkGs9nedtSmTRv+8pe/OJ77zDPP0KNHDyZNmuQ49v777xMfH8+OHTuIjo7mww8/5NNPP2X48OEAfPDBB8TEnLwm47Bhw3j00Ucdj2+77TaGDx/O008/DUC7du3YsmULr776KnfddddZ/7wAjz76KJdddhkAzz//PJ06dWLXrl106NCBv//974wYMYIJEyY4XmfJkiX89NNPjuenp6fz2GOP0aFDBwDatm1bp9c/V27TYicicqLi/HL2/fCNYxswm2FmUJ9sht/RsVmFOoCQSD86DYoFYMlXuzBsbrXhkDSCoUOHsm7dOsfXP/7xD7Zu3Uq/fv1q7G4yYMAAioqKyMzMdBzr2bNnjXutXr2auXPnEhAQ4PiqDj+7d+9mz549VFZW0rt3b8dzgoODad++/Ul1/fbeW7duZcCAATWODRgwgJ07d2K1Wuv0M3ft2tXxfXR0NADZ2dmO1+nXr1+N63/7ePz48dx7771cdNFFvPzyy+zevbtOr3+u3KbFTkQEwGa1sXHeflZ8v4cYozWJoTYMLJhNVgJ7XOTs8pym12VJbFuWxeH0QnasPET7PlGN88LbZjbY+nm2YwHV3ETHR/p6+LL81uVOe+268Pf3p02bNjWOGYZx0pZ11V28Jx739/evcY3NZuOKK67glVdeOel1oqOj2blz50n3OPHev63rbGuqZjKZTjpWWXny3sondptW39Nms52ylt967rnnuPXWW/nxxx/53//+x7PPPstnn33GNddcc8bnng8FOxFxG1m785n/n+3kZhYBUJJ0EUcHtCGkeI19JmgzHl/mG+hFjxGJLP/vHpb9dzete0Tg4Wmp/xc6IcgZGJg+u9U+G3fZtHqbuJG7v4glX+/Gsvsn4rw3YkscSEr/aLwOLm1SCzCbTKY6dYe6mpSUFL766qsaYWrJkiUEBgYSGxt7yuf16NGDr776iqSkJDw8To4hrVu3xtPTkxUrVhAfHw/Y90rduXMngwcPPmNNixbVXM5nyZIltGvXDovF/nmPiIggKyvLcX7nzp2UlJSc3Q99wussW7asxrHfPgZ7F227du14+OGHueWWW/jggw8U7EREzqS0sIKl3+xm6xL7L2tvPw/6XdOalAExmMw9geudW6CL6DY8nk3z91OUV87Gufu54JKE+n2BY2MabZgxL5tGWlkvEr3NmLFimCyYqtfPO48WvLSNOfz8zibizMsYFTrZ3tWe/T18i/016jFAyuk98MADvP766/zxj3/kwQcfZPv27Tz77LOMHz/eMb6uNmPHjuXdd9/llltu4bHHHiM8PJxdu3bx2Wef8e677xIYGMidd97JY489RlhYGC1btuTZZ5/FbDaf1Br3W4888gi9evXiz3/+MzfddBNLly7lzTffZNq0aY5rhg0bxptvvknfvn2x2Ww8/vjjdZ7U8NBDD9G/f3/+8pe/cPXVV/PLL7/UGF9XWlrKY489xvXXX09ycjKZmZmsXLmS6667rk6vcy40xk5Emix7t2smnzy7zBHqOg6I5rYX+tJpoPutS3e+PL0s9LkyGYDVP6VRVlxpD2M/TTzv3ShsVhtZc36wBy3sYxoNcIxvNBlW0neVnNcOGFm78/npnU1UVdroELULw2TBbLJhGGAYJkyGPUBqAebGERsby8yZM1mxYgXdunVjzJgxjB49mqeeeuq0z4uJiWHx4sVYrVZGjBhB586d+b//+z+Cg4MdgfC1116jX79+XH755Vx00UUMGDCAjh074uNz+lnsPXr04PPPP+ezzz6jc+fOPPPMM7zwwgs1Jk5MmTKF+Ph4Bg0axK233sqjjz6Kn1/dWk779u3Le++9xxtvvEH37t355ZdfavzcFouF3Nxc7rjjDtq1a8eNN97IyJEjef755+v0OufCZNTHnGcXUj0rNj8/n6CgIGeXIyINZN+mXBZ/uZMjB+1dKC3iAhh8S3uiW2tW/OnYbAYzXlxB3oFihvXZS8d9448vXHyOLV1VlVZ+emcTph3/47LQyfYWO2yUXfERxQUVFK6exeaMVsR6baKr/4+Ysdlfs88YuHTSmV8AKC+p5LM/r6DoSDlJXcO5dEg6ls9vO147OCbK0G4k9LjD5VrtysrK2Lt3L8nJyWcMKFJTcXExsbGxTJkyhdGjRzu7nAZxus9HXbKNumJFpEnJPVDEki93kb4lDwAff096X5FMp4ExmC3qhDgTs9lEv2ta8+PUDVRum4vhZ8F04lZjdQxDVZVWZr61kYwteXh49mV/6jRiPTdD0oX4dBiFD9Bi6A3Y1h1mx78/oDvf24NfHXfAmP+fHRQdKadTxDoGJhzCYh5sD6JpiyDpQravOIjnlk9p5bMSY+cvmHb8T12yTdjatWvZtm0bvXv3Jj8/nxdeeAGAq666ysmVuT4FOxFpEkoLK1j+/V62LNyPYYDZYqLr0Dh6jkrC2695LV9yvhI7tyC2XQgZ+zrT1ff7c95qzDAM5n+y3R7qvC1c/kBXYtsPqfXaVt0j8Au+n5+nQqRpA9bYAVzQbiTms5g1u2PlQXauPESyz0qGWCbBagus+qc9uB1r8UtOqmL783OPdQWfe1AV1/HXv/6V7du34+XlRWpqKgsXLiQ8PNzZZbk8BTsRcWkVZVVsmJPJ2l/2UVFm73JrdUEE/a5pTUjLpjuj0JlMJhP9rm3Dly8f5cejExk6IAe/LsPqHILWz86gbO13XBi0iZiLryCi/elnLEYlB1N13318/8Y6bNsMvP/1Hp33P3raWbOFeWXM/3QHAD3a7YPDx0Lob4Kbl48Hnu2HYk777pxaBMW1XHDBBaxevdrZZTRJCnYi4pKqKq1sXnCA1T+lUVpoX2MqIiGQAde3Ibaddk44X5FJQbTt2ZKdq3rza1YYV97QvU7PT9+Sy4EfPuWy0MkYWDAt/h7iA88YDuPah3LRXSn88t5mqnbMx/A/dVewYTOY89FWKkqr6Ba/iciQXMi2nrKFMe6qW/nx+WxiPTfR9obr8FdrnTRDCnYi4lKsVhvblx5k5Y97KTpSDkBwhC+9r0ymbWqkZrrWoz5XtWb32sNkbMlj3+ZcEju1OKvnHT1Uwi/vbaan56ZjEyXq1vXZtmckxUfL2f99Z7r7f29fpqSWoLZ+TgaZ247Q2m8lF1ZOgp3H1t1rNwIuOHnf24BQH6zJI1i8tTcV2cn0bsDFkUVcldsEu6lTpzJ16tQ6bxkiIk527C9fa8IAdhxNZfVPaeRnlwIQEOpNr8uSad8vCosmRtS74AhfugyJY/3sDOZ/up2bn+6Nl8/p/1ooL61i5lsbKC+pojihN+aKcxuj1/2iBIqOXMePiyDOezMxIy4nAuxLryQNJMPWlyVf27dg6pmSAftP6IINTT5lUGvfN5qMrUcoXv4NrHq+3hdHFnF1bhPsxo4dy9ixYx1TgkWkCTi2oK2BBcuyaew5MpH88t74BHjSc2QSnQbFNMzuCOLQ+4pk9qw9TGFuGcv+u4dBN7U75bU2m8Ev723myMES/IO96D72fshq45iZWtfgNOC6Nvxy9HIWre5Nm/+tYkTAS45FhrNLrqe/fzkkD6TFhSPhs4/PKkC26h6Bh6eZ0LI1GAHnN+NXpClym2AnIk1LRWkVR+b/SIRhxmyyYjPMJAZsJXrULXQeHHvGliOpH14+Hgz5XXu+/8d6Ns7NJK5dKK0uiDhpj1fDMNj6wXvEp8/H068LPR74A/7B3hA86pwDk8ls4qK7UigrriRy/wbHjFabYSLV70t7N2/298B/aixtcrrX8/S2EJ8Sxv5tnelunPuMX5GmSr85RaRRFeeXs3FeJhvn7SfGFs9loTbHwrIdb7kBS+dEZ5fY7CSktKDrsDg2zMlk1vQt3FiQTejcex3dmMZNn7Jt2UE6ZTyKzc9Md9P3UNoFOP8WMIunmSv+2I29n1+Eefv3js+CcWyRY0dr26WTzjpAJncLZ8763iy0PM/AXtnNfp9gaV4U7ESkUWTvK2D9nAx2rcrGZrVveJMfNYTMHvHEeG6C5IFY9Jev0wy4rg15B4rJ3HaEjJn/JcTX4tiia+9/v6U8txSb37GdHeq5a9NsMdP6lrswtkVg7F6I4e2HadGUc25tS+oSjskEG/Z3pdsD/Qhq4VsvdcqppaWlkZyczNq1a+nevbtTa5k3bx5Dhw7lyJEjhISEnPXzTCYT33zzDVdffXWD1dYYFOxEpMHYrDb2rMthw5wMsnbnO45Htw6m20XxtOoWgcnc14kVSjWzxcyoP3Tlp3c2kbHLvnBxddfo1qzWmC0me0tdA3ZtmjpchqXDZfYHcT3Peeyeb6AXUa2DydqVT9qGHLoOja/3WpuTu+66iw8//NDxOCwsjF69evGXv/yFrl27AhAfH09WVlazWkD4ueee49tvv2XdunXOLqUGBTsRqXfF+eVsW5rFpgX7KcqzL1litpho07Ml3YbF0zJR+zi7Ik9vC5eN7crmBS2YN9ebkJLVHLR2wZIyil5XJENBl3MOW3XW4dzH7gEkd41QsKtHl156KR988AEABw8e5KmnnuLyyy8nPT0dsG96HxUV5cwS5RitHyAi9cKwGaRvyeWntzfy0cQlLPt2D0V55fYZrqOSuGNSfy6+u5NCnYszm010GRLHkOcfpvOkD7nk1ce49P4utIgJsAetOox1c6akrvY1+fbvOErlhu/ty6hsm+nkqpoub29voqKiiIqKonv37jz++ONkZGRw+PBhwN4VazKZHK1X8+bNw2QyMXv2bHr27Imfnx/9+/dn+/btjns+99xzdO/enY8//pikpCSCg4O5+eabKSwsdFxjGAZ/+ctfaNWqFb6+vnTr1o0vv/yyRm0zZ86kXbt2+Pr6MnToUNLS0s748+zcuZNBgwbh4+NDSkoKs2bNOumaxx9/nHbt2uHn50erVq14+umnqay0L5Y+ffp0nn/+edavX4/JZMJkMjF9+nQAXnvtNbp06YK/vz/x8fE88MADFBUV1eXtPi9qsROR81LdOrdl0QEKcsocx6NaBZFyYSxte7XUkiVNVFP+7xYa5U9wS19C8+fj+fVk91vPzomLLxcVFfHJJ5/Qpk0bWrQ4/aLWTz75JFOmTCEiIoIxY8Zwzz33sHjxYsf53bt38+233/LDDz9w5MgRbrzxRl5++WVeeuklAJ566im+/vpr3nrrLdq2bcuCBQv43e9+R0REBIMHDyYjI4Nrr72WMWPG8Ic//IFVq1bxyCOPnLYmm83GtddeS3h4OMuWLaOgoIBx48addF1gYCDTp08nJiaGjRs3ct999xEYGMif/vQnbrrpJjZt2sRPP/3Er7/+CuBYas1sNvOPf/yDpKQk9u7dywMPPMCf/vQnpk2bVpe3+Zwp2IlInVVVWNm7Podtyw6SsSUXwz4XAi9fD9r3iaLTwBhaxAY4t0hp9pK6hhOwfNPxvWPdZT27Y+s/NmZY/eGHHwgIsP8/XVxcTHR0ND/88ANm8+k7/l566SUGD7bvITxhwgQuu+wyysrK8PHxAewha/r06QQGBgJw++23M3v2bF566SWKi4t57bXXmDNnDv369QOgVatWLFq0iLfffpvBgwfz1ltv0apVK/72t79hMplo3749Gzdu5JVXXjllTb/++itbt24lLS2NuLg4ACZNmsTIkSNrXPfUU085vk9KSuKRRx5hxowZ/OlPf8LX15eAgAA8PDxO6oI+MSQmJyfz5z//mT/84Q8KdiLiWgybwYFdR9m+/CC7V2dTUXZ8l5fq1rk2PVvi6dV0W3nEvSR3DWfdwtNvW9YkpS08PomlkcLq0KFDeeuttwDIy8tj2rRpjBw5khUrVpCYeOoliqonVwBER0cDkJ2dTUJCAmAPTNWhrvqa7OxsALZs2UJZWRkXX3xxjXtWVFRwwQUXALB161b69u2LyXR8q8HqEHgqW7duJSEhwRHqTvWcL7/8ktdff51du3ZRVFREVVUVQUFnHkoyd+5cJk2axJYtWygoKKCqqoqysjKKi4vx9/c/4/PPl4KdiJySYRjkZBaxe3U2O1YcojDveFdrYJgP7ftG0b5PFCGRfk6sUqR20a2D+Z+lPz8emcjgftkEdL+o6bfWgb37ddm0Rl182d/fnzZt2jgep6amEhwczLvvvsuLL754yud5eno6vq8OXzabrdbz1ddUn6/+88cffyQ2NrbGdd7e3oD9d1Rd1facE4MhwLJly7j55pt5/vnnGTFiBMHBwXz22WdMmTLltPfet28fo0aNYsyYMfz5z38mLCyMRYsWMXr0aMf4vIamYCciNZwY5natyXbs2wrg6WOhTWpLOvSNIrp1CCaz6TR3EnEus8VMQqcW7FzZmzDvBPp1aHPmJzUFHUad9U4cDcVkMmE2myktLT3zxecoJSUFb29v0tPTHd25tV3z7bff1ji2bNmyM943PT2dAwcOEBMTA8DSpUtrXLN48WISExN58sknHcf27dtX4xovL6+T9qdftWoVVVVVTJkyxdFN/fnnn5+2nvqmYCcipw1zFg8ziZ1b0Ca1JUndwtXVKk1Kctdwdq48xN4NufS7pvZgV5hXxrpf0ynKKyepaws69Is+qQXH5ZzncjB1VV5ezsGDBwE4cuQIb775JkVFRVxxxRUN9pqBgYE8+uijPPzww9hsNi688EIKCgpYsmQJAQEB3HnnnYwZM4YpU6Ywfvx47r//flavXu2YnXoqF110Ee3bt+eOO+5gypQpFBQU1AhwAG3atCE9PZ3PPvuMXr168eOPP/LNN9/UuKZ6csS6deuIi4sjMDCQ1q1bU1VVxRtvvMEVV1zB4sWL+ec//1nfb81pKdiJNFNWq42snUfZuyGHtA05NWa0WjzNJHZqQevUCJK6hGvfVmmyEjqFYTabOJJVTPGKb/HPW15jJunh9EK++8c6yors3WS2rT/Scski+/IuPe5wj67bevDTTz85xsgFBgbSoUMHvvjiC4YMGdKgr/vnP/+Zli1bMnnyZPbs2UNISAg9evTgiSeeACAhIYGvvvqKhx9+mGnTptG7d28mTZrEPffcc8p7ms1mvvnmG0aPHk3v3r1JSkriH//4B5deeqnjmquuuoqHH36YBx98kPLyci677DKefvppnnvuOcc11113HV9//TVDhw7l6NGjfPDBB9x111289tprvPLKK0ycOJFBgwYxefJk7rjjjgZ7j37LZJxLB7ULKygoIDg4mPz8/LMa5CjSnJQVV5K+OZe0DTns25xHRWmV41x1mGuT2pLELi0U5sRtfPu3NXim/cJloZOPj0u7+T+UxV3MjEkrKMorp2vsBlL8ZtOicEHNJ9fjjNOysjL27t1LcnKyY1aoSLXTfT7qkm1c7jd3YWEhw4YNo7KyEqvVykMPPcR9993n7LJEmiSbzeBweiGZ2/JI35xH1u58DNvxf8v5BnqS2CWc5K7hxHUIVZgTt9SudxQVBzY5tkjDZMHYu5BZs2IpyiunU8Q6Blqfh8Ka3a8GJkzusDyKNCsu91vcz8+P+fPn4+fnR0lJCZ07d+baa6894yKIImKXf7iUzG15ZGzNI3P7EcqLq2qcD4vxJ6mrPcy1TArCrAkQ4uba9oxkzrfdMJuOL3uyK7cd6ZtzsXia6dNtP2w+1pJ3AhMG5O2xrxuncCdNhMsFO4vFgp+ffemEsrIyrFbrOU1nFmkuivPLObDzKPu3HyFja16NsXIAXj4WYtuHEt8xjIROLQiO8HVSpSLO4eltIfjCa/lxjpWkwK14tBvMrlWHGBA4m9jOsfhW7D++JpxhpdCvE8VHy4ny2gU7f4Ed/3OfHSvE7dV7sFuwYAGvvvoqq1evJisri2+++Yarr766xjXTpk3j1VdfJSsri06dOvH6668zcOBAx/mjR48yePBgdu7cyauvvkp4eHh9lynSJBmGQf7hUrJ2HeXArnwO7DxKweGayw2YzSYiWwUR3zGM+I5htEwMxGzRttDSvKVemsSMVYNJO9ybpMIVXBY6GQMzpr02MB37/6PdCLjgdmg5jEMv30tLzz2Orlu32LFCmoV6D3bFxcV069aNu+++m+uuu+6k8zNmzGDcuHFMmzaNAQMG8PbbbzNy5Ei2bNniWIk6JCSE9evXc+jQIa699lquv/56IiMj67vUc3Zg11FWzUzDx98THz8PvP098fH3xNvfw37M3xNvPw/Hn/pLVc5VVYWVwxlFZKcVkLU7n6xdRykpqKh5kQnC4wKIaRNCfMcwYtqFaKycyG94elu4evwFLP5qF62ydmBYLZg41vVq2OzhLTQZOowiEKiI7Ie58HsM3GjHCmkW6v23/8iRI0/ab+1Er732GqNHj+bee+8F4PXXX+fnn3/mrbfeYvLkyTWujYyMpGvXrixYsIAbbrih1vuVl5dTXl7ueFxQUFAPP8XpFeSUkrEl76yv9/Kx4BPgibefJz7+x4Kg32+CYC0h0aJA2KzYrDaOHCzhUFoBh9IKyE4rIHd/cY3JDgBmDxORiUFEtwkhpm0IUa2D8fZVkBM5k4BQH0bc2xm2XQ+ffQWYgWMtdr8Jb0EXXsePnxbSKngbHQa3wpS20H6iHlrtNLxIalNfn4tG/dugoqKC1atXM2HChBrHL7nkEpYsWQLAoUOH8PX1JSgoiIKCAhYsWMAf/vCHU95z8uTJPP/88w1a929Ftw5h+F0dKSuqpLykirLiSsqLKykrqTp2rJKy4irHUhIVZdZj+2qWnf7Gv+Ht74F/sDcBId74hXjjH+yFf7A3/iHejj/9gjzVItgElRVXkptZRE5mETn7i8jNLCLvQDHWKttJ1/oGeRGZFERkUhAxbYNpmRSEh6cWCRY5Zyfu3ODpA5VlJ+3gkNwtnHn/7gv50HHhsWVSlk07r7F2Fov9/9uKigp8fTXWVWoqKSkBTt5mra4aNdjl5ORgtVpP6laNjIx0rGidmZnJ6NGjMQwDwzB48MEHa2wi/FsTJ05k/PjxjscFBQXEx8c3zA9wTHCE71kNQLdZbZSXVp0cAItrhkH7sePfl5fYA2F5cRXlxVXkHSg+9YuYwC/Q61jY88IvxB4E/YO98Qv2coRA3wBPbf/kBGVFlRw5WMyRQyUcOVjCkYPF5O4voiivvNbrPX0stEwMJDIpiJZJQURbl+B7eBmm5EEa3yNSn86wc4OXjwfJ3cJpuX3T8e7Y8xxr5+HhgZ+fH4cPH8bT09Ox5ZQ0b4ZhUFJSQnZ2NiEhIY5/AJwrp/Tf/HarFsMwHMdSU1NZt27dWd/L29vbsRmwqzFbzPgGeOEb4FWn59lsBuUllZTkV1CcX07x0QqKj5Yf+76c4vwKSvLtfxo2g5KCCkoKKjh82lpM9qBXo8XvePCrfuzl6+H6W+m4mMoKKwWHSynIKeVodilHTwhy1avZ1yawhQ/hcQG0iAsgIi6QFnEBBLXwOR7At82Ez+60/2Wy/C3NyhNpZO16R7F5Y2e6+x9fJuV8xtqZTCaio6PZu3fvSfuOioSEhBAVFXXe92nUYBceHo7FYnG0zlXLzs4+78kRU6dOZerUqSdtyNsUmc0mRyBsERtwyutsNoOyokp72PtN8HN8f7Sc0sJKbFaDorzyU7YUVfPwNOMX7IVfkBe+gV74BnnhF3j8sV+Q57E/m0cItNkMSgsr7CH72HtcdLScgpxSCg6XUZBTevJkht8IDPMhNMqPkCg/QqP8CYv2p0VcwJnHxaUtPL5KvmbliTS6+E5hzPEawI9HJnJhr0ME97y4xv+DBbmlbJq3H4AuQ+MIDDvzbhJeXl60bduWiorT/96Q5sXT0/O8W+qqNWqw8/LyIjU1lVmzZnHNNdc4js+aNYurrrrqvO49duxYxo4d69h2ozkwm034BdlDVkRC4Cmvs1bZKCmwh72SoxUUHQsoJdVh8FhoKS+poqrSRkFO2UlrodX6+h4mfI9N/PD28zg+OeSEP739PfDysX95elvsXz4Wx/cNHQwNw8BWZVBZYaWy3ErVsT/LS6uoKKmi3PFVSXnp8cdlRfb3pKSw8qTJC7Xx8vUgOMKXoHBfQqP9CI3yIzTSn5BIPzy9z/F/1qSB9jE91eFOs/JEGpXFYqZtz0g2zO2NZ1lLLiEdfpoISQPJCxrMV6+udoyl3rb8ILfclIvv4WU19qKtjdls1pZi0mDqPdgVFRWxa9cux+O9e/eybt06wsLCSEhIYPz48dx+++307NmTfv368c4775Cens6YMWPquxQ5xuJhJjDM54z/mqyssNpb+AoqKCmssP9ZUEFpYWWNxyWFFVSWWbFVGcdaB8/xX54m8PSy4OFlxuJhxmwx2f/0sD+2eJhqBL8aM4aM6mPHDxiGPcRWllupKrcHuMoK21kFs9OWabJPYHBMWAn2IqiFD0Hhvo4w5+N/foNda3XiAO/fDOwWkcbRvm8UG+ZmYt3yI2ROckyiWG99lorS7rSICyCqcjEJ5T/i+/3KeplkIXI+6j3YrVq1iqFDhzoeV09suPPOO5k+fTo33XQTubm5vPDCC2RlZdG5c2dmzpxJYmJifZcideTpZSGkpR8hLf3OeG1VhZWSggp761ZJpX2iR4l9Ekj5CZNAykoqqSg9FrLKqqg4FrgwAAP78fLG6T43W0x4elvw8LLg5euBj58HXn4e9tZG3+pWRw/HGoSOiSeBTpx5fIYB3iLSsCISAolMDiI6Z6NjEoUNM6FlawgK78t1qTPxXP43jOqh3oYVMMGaj/X/rjiFyXCTBXVOHGO3Y8cO8vPzCQoKcnZZUgvDMKiqsLesVZRVYa20Ya2yYa0ysFVVf29/XP3xPLHlzmQCjj00YXJ8D/Yxgh7eFjy9LI4Q5+ltP6Z1AUXkXOzblMumd9/lstDJ2DBhxmBN6Q20vWoUgb/cfeonqtVO6kn1MLOzyTZuE+yq1eWHFxERORPDMJjz8TaCNvyVXgFfYjNMmE0GhCTB0X2AgYH935iGUf2PTwv0GQOXTnJq7eIe6pJt1IQhIiJyGiaTiaG/60CrFD8MLPZQB3A0DY5FOhOwq7w/JhMYmvAkTqR9iERERM7AbDbRov9ISP+IY21z1WcgqgsMmcD22bFs3/E/eqZkEjlY42PFOdymxW7q1KmkpKTQq1cvZ5ciIiLuqHqmertj+6GbLIANhkyADqNI6NSCtPLeLCm6x37+p4n2hcZFGpHG2ImIiNTVtpknLUWUf7iEfz+9jFa+KxkZPOn4GpSaRCHnqS7ZRl2xIiIidVXLUkRB4b74B3sRbdt4fAsy7RojjcxtumJFREScyWQyEd02hP0VnY+HOk2ikEamFjsREZF6Et06hIWrerMiYBK9O+/XrjHS6NymxU6TJ0RExNmi29j3Kl+X2Rlb4gBIW6gJFNKoNHlCRESknthsBv8av4AYYymXhU7WBAqpF1qgWERExAnMZhNRrYOJ9dqEwbFQVz2BQqQRKNiJiIjUo9h2ofYJFGgChTQ+BTsREZF6FNsulLTy3vxS8hRG7/vVDSuNSrNiRURE6lFEQgCePhZ2FqRyQadeRCQEOrskaUbcpsVOs2JFRMQVmC1mYtqEAFC49GttLSaNSrNiRURE6tmaX/aR9eN/NDNW6oVmxYqIiDhRctdwYr02YTPMmhkrjUrBTkREpJ6FRvlTGNQTs8nmWPakJLw3e9YdpvhoubPLEzemyRMiIiINIHTwDfz4VRkJflvwCw+m6PMvySzfxi/Wvlxz+UEiWQ9JA9U9K/VKwU5ERKQBdBwQzeaFw0jPhssKJ2PzNdHN73uOVEYTuiwLAzOmZdNg4CMw/BlnlytuQl2xIiIiDcBiMXPVuAvo0W4fNsyYTfa5iqGeWQCYsNkvXDhFs2al3rhNsNNyJyIi4mp8/D2JHn4F5uoQd4Iaa1Ks+bjxihK3puVOREREGtrsF+wtc6ej5VDkFLTciYiIiCsZ/ow9uLUbBe1HsSH6VfaU9cLAZD+v5VCknmjyhIiISGPoMMrRIhe6JY8NW/Jo5bMSw2TBZFgh6UInFyjuQC12IiIijSymfQgHzP358chEymKHQbuRzi5J3ISCnYiISCOzWMwkdm4BgG/mLNj5C3x2i2bHynlTsBMREXGC5G7adkzqn4KdiIiIEyR2akFWVRf7tmMm+7ZjGmcn50uTJ0RERJzAy9eDqlYj+HGXwYDkJYS09HN2SeIG3KbFTgsUi4hIU5PcLQKAkKPzNc5O6oXbBLuxY8eyZcsWVq5c6exSREREzkpyV42zk/rlNsFORESkqfEP8aYouJd9nB3Hwp2nj7PLkiZMwU5ERMSJfHtexcqi6zFhwzCZYeEUjG0/OrssaaI0eUJERMSJOg+KZefCSmyGGTM2bIaJok/H4xPxCl7eFgiIhB53aB9ZOStqsRMREXEibz9PYi+5ArPJHurMJoMg80G8ctfDgTWw43+aVCFnTcFORETEyUIH34Bx86cQ2RnjVBet+bgxS5ImSsFORETEBZg6XIZ52BOYTnXBjplqtZMzUrATERFxFR1Gwc3/wdZuJNlV7Si2Bh9vwdNSKHIWFOxERERcSYdRmG/9jA2tP2RewQPHWvC0FIqcHQU7ERERF5TYpQVp5b3ZxC2ADY4thaLuWDkdBTsREREXFNsuFICq4iIMkwUMm7pj5YwU7ERERFyQX5AXIZF+7K/ojKl6uzHDCkkXOrs0cWFuE+ymTp1KSkoKvXr1cnYpIiIi9SKmTTBp5b3ZmvgatB0B7UY6uyRxcSbDME65ZE5TVFBQQHBwMPn5+QQFBTm7HBERkXO2bWkWsz/cSveETQyoePp4q93N/9FOFHWxbSakLQRPX6gshaSBTer9q0u20ZZiIiIiLiq6TQgAAUdXYPhbjnfJpi1qUsHEqWa/YJ90ggkw7H8umwYDH4Hhzzi5uPrnNl2xIiIi7iYo3Ae/YC8yyzXO7pxsm3ks1AGOFQGP/blwCvOmD6ekssQZlTUYBTsREREXZTKZiG4dQlp5b3a2+7vG2dVV2kJOF3WGpK3iy+/vabx6GoGCnYiIiAuLbhMMwJGsYvu2Yjt/gc9u0Xp2Z8PTF7BB9UZtHr41ThvAtZ4tG7uqBqVgJyIi4sJijo2z881Zdmw9O6vWszsbjm5YM9Xdr0ZVeY1LTEBA20sbvbSGpGAnIiLiwlrE+uPpbSG9pJN9nJ22Fzs7aQvtAfhYi5192oQNK3DA05uy1sPccnaxgp2IiIgLM1vMRLUKIq28N4cSfo+2FztLSQOPt25iOObEWoCg1Lvxuf0btwt1oGAnIiLi8mKObS9WnH3k2MzYY+PG1nzs3MJcWYdRcPN/ONr9ZhYHBGPl2IInJjMBWJxdXYNRsBMREXFxbXrYB/hvP9jW3goFgGGfTDH7BecV5sq2zeTItu945fASPvPzxAIYJgsmw+bWy8VogWIREREXFxLpR0RCIHvSe5EXNIiwggXHT1av0+aGi+2es2OLEgcBk4FXWl9A0cCXCdi/1h7q3LALtppa7ERERJqA1JGJACzPrKW1SePtjjsW6qrH09mAcWGpBHS5AS6d5NahDhTsREREmoRW3SNodUEEe0p6sab0hpMv0Hg7xxInxzYOw8AedLxbD3duXY1IwU5ERKQJMJlMXHR3CrHtQ1iafyuHqtrWvKD4kHMKcyFHl73hWI64Otwx8BG3b6U7kcsFu4yMDIYMGUJKSgpdu3bliy++cHZJIiIiLsHTy8JlD3QjqlUQqwqvr3ly/+rmN5Fi20z4aSLMfoEjH4wgJG2JI9g4Ql0zG3toMgzDOPNljScrK4tDhw7RvXt3srOz6dGjB9u3b8ff3/+snl9QUEBwcDD5+fkEBQU1cLUiIiKNL3d/EZ/9eQWjQiaR5LOyesMsOzdcdLdW22bat1bDDNiwcby1qkZrXWWpfU27Jvye1CXbuFyLXXR0NN27dwegZcuWhIWFkZeX59yiREREXEiL2ACiWwezpfSimqHOZG4+W42lLbT/vNgcY+nghFCHyT6pZPnbzWpv3XoPdgsWLOCKK64gJiYGk8nEt99+e9I106ZNIzk5GR8fH1JTU1m4cGGt91q1ahU2m434+Pj6LlNERKRJa9cnirTy3mz1uPXYEZN94eLmstVYwf5jCzVTI9w6Qh2GPfg1s7116z3YFRcX061bN958881az8+YMYNx48bx5JNPsnbtWgYOHMjIkSNJT0+vcV1ubi533HEH77zzTn2XKCIi0uQldWkBwJz9N1DZ52EcQcbdlz7ZNhPeGQpb/suJY8mOf2+GqK72bljDdmynDqtbL0p8onpfoHjkyJGMHDnylOdfe+01Ro8ezb333gvA66+/zs8//8xbb73F5MmTASgvL+eaa65h4sSJ9O/fv75LFBERafICQn1oERdAbmYRhYfyCKsOMNWtU014TNkpOcbV2Z3UUmcy28PckAn2nz+2p/29cPNFiU/UqDtPVFRUsHr1aiZMmFDj+CWXXMKSJUsAMAyDu+66i2HDhnH77bef8Z7l5eWUl5c7HhcUFNRv0SIiIi4qsXMLcjOLSC9KIcywAse6Ht20O9bYa99xozrQOcbTxaZCqyFQWVYzxHUY1WwCXbVGnTyRk5OD1WolMjKyxvHIyEgOHjwIwOLFi5kxYwbffvst3bt3p3v37mzcuPGU95w8eTLBwcGOL43HExGR5iKxk707dnVaJ4wLH4HqVdwWTnG7pU9s235g/9ZvTm6lG/gI3DfHvqxJM9hZ4kycslesyVRjDg+GYTiOXXjhhdhstrO+18SJExk/frzjcUFBgcKdiIg0C5GtgvDysVBWVElJ3lH8jy39AbjPHrLbZmJd8BcsB9YSd+Lx2FQY+GizD3K/1ajBLjw8HIvF4midq5adnX1SK97Z8vb2xtvbuz7KExERaVIsFjNxHcPYs/YwByq60JbfNIwsnGIfZ9ZUw8+xfV8tJ50wQXzfpvtzNaBG7Yr18vIiNTWVWbNm1Tg+a9as854kMXXqVFJSUujVq9d53UdERKQpSUgJA2DDgWMzQX9rwV8buaJ6csK+ryczms0s17qq92BXVFTEunXrWLduHQB79+5l3bp1juVMxo8fz3vvvcf777/P1q1befjhh0lPT2fMmDHn9bpjx45ly5YtrFy58nx/BBERkSYj4dg4u0N78ynrOxFietS84EDT3GqsbOW7jn1fa4hNbT67a5yDeu+KXbVqFUOHDnU8rh7/dueddzJ9+nRuuukmcnNzeeGFF8jKyqJz587MnDmTxMTE+i5FRETE7QWG+RAa7c+RrGIytuTRdtBjNZYEAZpWl+y2mRSteIuAPQtqHq/vMXXbZtp3r2ji2439lsvtFXu+tFesiIg0N8u+3c3qn/YR1yGUq8Zd4BibVkO7UXDrf5xT4Nk6VnfNfV9NmNqNrN/aq9fDq177z8VbAJv0XrHnSmPsRESkuUoZGIPJBJnbjnBob4F9Juxvx9vtmOnaXbLHQt2J+74CmDCgx5nXta2TtIXHQ52bbTfmNsFOY+xERKS5CmrhS7s+UQAs+Gw7NpthD3ftfrMTlKtuN3ZCqDu2y+tx7RpgkeGkgcdDnZttN+Y2wU5ERKQ563dNa7x8LGTvK2TDnAz7wR53/OYqk8u1Thm1hDr7hIlj0ybqu7UO7EHx5v9AnzEu3w1bV05ZoFhERETql3+wN/2va8O8T7az/L97SO4WQXCHUZByFWz577GrDJfabsz663NYFv3t5FCXchUExTXsHq9uut2Y27TYaYydiIg0dykXxhDbPoSqShtrZ9mXGSMoFkwn/HV/aKtzivuNylnP1B7qBj4CN36k7cHOkdsEO42xExGR5s5kMtFrVDIA25dlUVFWdWw82Qk7UuyY6dxxdttmUv7va/Fc/PfaQ11T3wLNydwm2ImIiAjEtAshKMKXqgobGVvy7K1e7UZyfKlfE6z52DnFzX4BPrsFr12zqytRqKtnCnYiIiJuxGQy0apbOAB71h+2H+xxB8fnmhrOWfrkN5MkjlWiUFfPFOxERETcTFIXe7DL3HoEwzBOaLU7wcIpjRfuTrGcian9sdmpCnX1xm2CnSZPiIiI2EW2CsLsYaKkoIL87FL7wZOWPqFRwp1t9vO1L2cy8BG4xb2WGnEFbhPsNHlCRETEzsPTQmSSfeupAzuP2g92GHXybhTQcIsWb5tJ1Sc3YF74miZJNCK3CXYiIiJyXGy7UAD27zxy/GBtW41B/U+mODZJwrLzF0ChrjFpgWIRERE3FN0mGICsXfk1T1SHqoVTjh+rXgLlfLpFt82078FasB+2/FeTJJxEwU5ERMQNRSUHYzJBYW4ZRUfKCAg9YceJ4c/AoS2w4yfsscsMP02EBa9CQKR9PF5dQt6xyRHVbXO/7Xo1tR8FF9yu8XSNwG26YjV5QkRE5DgvXw/C4wMByNqdf/IFjiVQzIANjqbBgTWw43/w2S1nN6li20x4Z6hjckRtoU6TJBqXyTAM48yXNR0FBQUEBweTn59PUFCQs8sRERFxmoUzdrBhbiZdhsQx6OZ2J1+wbSbMexkOrq/9BjE9YNBj9lBW3dXq6Wtv7cvZDnl7alxeHeZsHGs5UtdrvahLtlFXrIiIiJuKbhPChrmZZO0+WvsF1a1on91S+/kDa+znAiKh6NBpX6s61GXG9yIutjckXahWOidQsBMREXFT0a3tEyhyMosoL63C27eWv/Y7HFskeM3Hx1rhdp98zRlCHdhDXXbPO4i7/I3zrFrOh9uMsRMREZGa/EO8CQr3AQMO7qllnF21DqPg1v/AQ2tqXw7lDHb7h1Bw7Tu0VKhzOgU7ERERNxbdJgSArF1Hz+4Jw5+xt+DFpNZ6unpgfpqHhTm+PnyVej0Jj+wiqOtN512rnD91xYqIiLixmDYhbF92kIytR+h71Vk+qcOo4xMm1nwMxYcgIJJDAS1YlDaLeZYqlgaF8my/Z7mu9RUNWr/UjdsEu6lTpzJ16lSsVquzSxEREXEZiV1agAmy0wooyCklKNz37J98LOAZhsGn2z7lr6v+SlWQF/GBrfl48BQ6tujYcIXLOXGbrljtFSsiInIy/2BvYtuGALBrdXadn19YUcgj8x/h5RUvU2Wr4uLEi5lx+QyFOhflNsFOREREate2VyQAmxcdwGY7++Vrt+Vt4+YfbmbWvll4mD2Y0HsCUwZPIdArsKFKlfOkYCciIuLm2vWOwtvPg4LDpezblHvG6w3D4KsdX3Hbj7eRXphOtH80H176Ibd1vA2TyXTG54vzKNiJiIi4OU9vCykDYgDYMCfjtNeWVJbw5KIneW7pc1TYKhgUN4jPL/+crhFdG6NUOU8KdiIiIs1A5yGxYILMbUcoyCmt9ZoNhzdww/c38P2e77GYLIzrMY43hr1BiE9I4xYr50zBTkREpBkIauFLXPtQALYtzapxrspWxVvr3+KO/91BemE6Uf5RvHfJe4zuMhqzSVGhKdF/LRERkWaiY/9oAHasPL5FWEZBBnf+dCfT1k3DalgZmTySr678ip5RPZ1VppwHt1nHTkRERE4vqUs4ZouJ/OxSjhwsZl7hL0xeMZnSqlICPQN5su+TXNbqMmeXKedBwU5ERKSZ8PL1IKZtCDt3ZTB+/nhWlS0BoGdkT1668CViAmKcXKGcL7cJdtp5QkRE5MwK2u7jC/9XKSkrwMPswR8v+CN3ptyJxWxxdmlSD0yGYZz9SoVNQEFBAcHBweTn5xMUFOTsckRERFxCWVUZr695nU+2fgJAaGkkb171Ol2jOzu5MjmTumQbt2mxExERkdpty9vGhAUT2J2/G4AeR4fQY/tlBPWPgmgnFyf1SrNiRURE3JTVZuVfG//FLT/ewu783YT7hjNt+DTujngQD5sX6ZvPvAuFNC1qsRMREXFDe/L38PSip9mQswGAYfHDeLb/s4T5hLEn5TCbF+wnfUuek6uU+qZgJyIi4kasNisfb/mYN9a+QYWtggDPAP7U609c3eZqxz6vcR1CMZtN5B8uJf9wCcERfk6uWuqLgp2IiIibSMtP46nFT7H+8HoABsQO4Ll+zxHlH1XjOi8fD6JaB3Ng51HSN+fRZYiCnbtQsBMREWniqmxVfLL1E95Y+wbl1nICPAN4rNdjXNPmGkcr3W8ldAqzB7steXQZEtfIFUtDUbATERFpwjbnbOb5pc+zNW8rAP1j+vN8/+dPaqX7rYSUFiz7dg+Z249grbJh8dB8SnegYCciItIEFVcW8+baN/l026fYDBtBXkE80vOR07bSnSg8LgDfIC9KCyo4sOso8R3CGqFqaWgKdiIiIk3MnPQ5TFo+iUMlhwC4rNVlPNbzMVr4tjjre5jMJpK6tGDr4iz2rj2sYOcmFOxERESaiIPFB5m8fDJzMuYAEBcQx9N9n6Z/bP9zul/rC1qydXEWu9ceZuBN7TCZz9zSJ67NbYKd9ooVERF3ZbVZ+Wz7Z7yx9g2KK4vxMHlwZ6c7ub/b/fh6+J7zfeM6hOLl60FJQQVZe/KJaRNSf0WLU2ivWBERERe2LW8bLyx9gY05GwHoFtGNZ/o9Q7vQdvVy/1+nb2H7soN0HRbHwBvr555Sv7RXrIiISBNXUlnCW+vf4uMtH2M1rAR4BjCuxzhuaH8DZlP9zWBt3aMl25cdZM/aw1x4fVt1xzZxCnYiIiIuxDAMfk3/lb+s/AsHiw8CcEniJTze+3Fa+rWs99eL7xiKp4+FoiPlHEorIKpVcL2/hjQeBTsREREXsefoHiavmMyyrGUARPtH82SfJxkcP7jBXtPD00JSl3B2rjzE7jXZCnZNnIKdiIiIkxVXFvP2+rf5eMvHVBlVeJm9uLvz3YzuMvq8JkecrTY9Wh4Ldofpf12bs1oHT1yTgp2IiIiTGIbB//b+jymrppBdmg3AkLgh/KnXn4gPim+0OhI6heHhaaYwr4wjWSWExfg32mtL/VKwExERcYKtuVt5ecXLrMleA0B8YDwTek9gUNygRq/Fw8tCZKsg9m8/Stbuowp2TZiCnYiISCPKK8vjjbVv8NWOrzAw8PXwZXTn0dzV+S68Ld5Oqyu6dcixYJdPp4GxTqtDzo+CnYiISCOotFXy+fbPmbpuKoUVhQCMTB7J+NTxRPlHObk6iG5tnzSRteuocwuR86JgJyIi0sCWHljKKyteYXf+bgA6hHVgQu8JpEamOrmy46JaBWMyQUFOGcX55fgHO6/1UM6dgp2IiEgDySzM5K+r/srs9NkAhHqH8scef+TaNtdiMVucXF1NXr4ehMUGkJtZRNaufNqk1v+aedLwFOxERETqWUllCf/a9C+mb5pOha0Ci8nCzR1u5g/d/kCwt+uuExfTJoTczCIO7DqqYNdEKdiJiIjUE8Mw+CntJ6asmsKhkkMA9Inuw4ReE2gT2sbJ1Z1ZTNsQNs7L5MDOo84uRc6Rgp2IiEg92Ja3jZdXvMzqQ6sBiA2I5dGejzI8YXiTWfA3uo29NTF3fxFlxZX4+Hs6uSKpKwU7ERGR85BXlsc/1vyDr3d+jYGBj8WH0V1Gc1enu/Dx8HF2eXXiH+xNSKQfRw+VcHB3Pkldw51dktSR2dkF1Oaaa64hNDSU66+/3tml1Opo2VFeWfEKqw6uwmqzOrscERFxgkprJR9t/ojLv76cr3ba16QbmTSS767+jjHdxjS5UFct5lirnbpjmyaXbLF76KGHuOeee/jwww+dXUqt5mXO499b/82/t/6bMJ8whsQPYXjCcPpE93Hq4pIiItI4Fu1fxCsrXiGtIA2AjmEdmdB7Aj0iezi3sHoQ0zaELYuz2L/jiLNLkXPgksFu6NChzJs3z9llnFJycDJXtr6SeRnzyCvL4+udX/P1zq/x8/BjYNxAhicMZ2DsQAK8ApxdqoiI1KO0/DReXfUqCzIXABDmE8b/9fg/rmp9lcstX3KuYtuHAZCdXkhJQQV+QV5Orkjqot67YhcsWMAVV1xBTEwMJpOJb7/99qRrpk2bRnJyMj4+PqSmprJw4cL6LqNBdYvoxksXvsS8m+bx7iXvcnP7m2np15KSqhJ+TvuZPy34E4NmDOIPv/6Br3Z8RW5prrNLFhGR81BYUchfV/6Va767hgWZC/AweXBnyp38cM0PXNvW9dakOx8Bod5EJASCAfs25Ti7HKmjem+xKy4uplu3btx9991cd911J52fMWMG48aNY9q0aQwYMIC3336bkSNHsmXLFhISEuq7nAblafakb3Rf+kb3ZWKfiWzO2czs9NnMTp9NWkEai/YvYtH+RZiXmeke0Z3hCcMZnjic2ADtwSci0hTYDBvf7vqWv6/5O3lleQAMjB3IY70eIzk42cnVNZykLi04nF5I2oZcOvaPcXY5UgcmwzCMBru5ycQ333zD1Vdf7TjWp08fevTowVtvveU41rFjR66++momT57sODZv3jzefPNNvvzyy9O+Rnl5OeXl5Y7HBQUFxMfHk5+fT1BQUP39MHW05+geR8jbnLu5xrkOYR0YljCM4QnDaRvStslMgxcRaU7WZq/l5RUvsyV3CwBJQUk81usxBsUNcnJlDe9weiGfT1qJh7eF0X+9EA9P92mRbIoKCgoIDg4+q2zTqGPsKioqWL16NRMmTKhx/JJLLmHJkiXndM/Jkyfz/PPP10d59apVSCtahbTivq73kVWUxZyMOcxOn83qQ6vZlreNbXnbmLZuGvGB8faWvIThdI3oitnkkhOVRUSajYPFB/nb6r8xc+9MAAI8AxjTbQy3drgVT0vzWNctPD4A/xBvio+Ws3/7URI7t3B2SXKWGjXY5eTkYLVaiYyMrHE8MjKSgwcPOh6PGDGCNWvWUFxcTFxcHN988w29evWq9Z4TJ05k/PjxjsfVLXauJDogmts63sZtHW/jSNkR5mXMY076HJYcWEJGYQbTN09n+ubphPuGMzR+KMMThtM7qnez+QUiIuIKSipL+GDzB0zfNJ0yaxkmTFzb9loevOBBwn2b13puJpOJxC4t2LLwAPs25ijYNSFOmRX7265HwzBqHPv555/P+l7e3t54ezedJUZCfUK5pu01XNP2GkoqS1i0fxGz02ezIHMBOaU5fLHjC77Y8QWBnoEMih/E8IThDIgZgJ+nn7NLFxFxS4Zh8MOeH3h99etkl2YD0KNlDx7v/TgpLVKcXJ3zJHUJZ8vCA6RtzGXgzYaGDTURjRrswsPDsVgsNVrnALKzs09qxaurqVOnMnXqVKzWprNgsJ+nH5ckXcIlSZdQaa1k+cHlzE6fzdz0ueSW5fLjnh/5cc+PeFu86RfTj+EJwxkSN4QQnxBnly4i4ha25W1j0vJJrM1eC9i3ARufOp6LEy9u9kEmrkMoFk8zhXll5B0opkWslvBqCpwyeSI1NZVp06Y5jqWkpHDVVVfVmDxxruoywNBVWW1WNuRsYPY+++SLzKJMxzmLyUJqZKpj8kWUf5QTKxURaZryy/N5c+2bfL7jc2yGDV8PX37f9ffckXIHXhat21bth6nr2bcxl75XtyL10iRnl9NsOXXyRFFREbt27XI83rt3L+vWrSMsLIyEhATGjx/P7bffTs+ePenXrx/vvPMO6enpjBkzpr5LabIsZgsXtLyAC1pewCM9H2HHkR3MSbdPvth+ZDsrDq5gxcEVvLziZTq16OSYfNEqpJWzSxcRcWk2w8Y3O7/h72v+zpFy+84KlyZdyiM9H9E/lGuR1LkF+zbmsm9TroJdE1HvLXbz5s1j6NChJx2/8847mT59OmBfoPgvf/kLWVlZdO7cmb/97W8MGlQ/08fdocXudDIKM5iTPoc56XNYm70Wg+P/+ZKDkx0hr1OLTs2+G0FE5EQbD29k0vJJbMrdBEDr4NY80ecJekf3dnJlrqsgp5SPn1qKyWxi9F8vxNtPk/qcoS7ZpkG7YhvTiWPsduzY4bbB7kQ5pTnMzZjL7PTZLM9aTpWtynEu0i/S0V2bGpmKh9kld48TEWlwOaU5vLH2Db7Z+Q0GBgGeATzQ/QFu7nAznmYFlTP59LllHDlYwoj7OtMmtaWzy2mWmmWwq+buLXanUlhRyMLMhcxOn83C/QsprSp1nAv2DmZw3GCGJwynf0x/fDx8nFipiEjjqLRW8um2T/nn+n9SVFkEwJWtr+Th1Ieb3fIl52PRFztZPzuDjv2jGXZHR2eX0ywp2DXDYHeicms5yw4sY3b6bOZlzHOMIwHw9fBlQMwAhiUMY3D8YIK8mud7JCLubUHmAv6y8i/sK9gHQEqLFCb2nkj3lt2dW1gTlLElj+/+sQ7/YC/ufHmAhvk4gcvuPCGNw9vizeD4wQyOH0yVrYq12Wsdky+yirP4Nf1Xfk3/FQ+TB72jezM8YThD44cS4Rfh7NJFRM7Lnvw9vLryVRbtXwRAC58W/F+P/+OqNldpZ59zFN02GA8vM8X5FeTuLyY8TsueuDK3abFrjmPs6sowDLbmbbXvYbtvNrvzdzvOmTDRNaKrY/JFQlCCEysVEambgooC/rn+n/xn63+oMqrwMHtwe8rt/L7L7wnwUhA5X9XLnvS7pjU9RiQ6u5xmR12xzbwr9myl5acxO302c9LnsCFnQ41zbULaOEJeh7AOanoXEZdktVn5Ztc3vLH2DfLK8gAYEjeER3s9SmKQAkh92TgvkwWf7SC2XQhXj+/h7HKaHQU7Bbs6O1R8yDHDdtXBVVQZx2fYxgbEOvawvaDlBVjMFidWKiJit+bQGl5e8TJb87YC9iWfHu/1OANiBzi5MveTf7iEfz+9DLPZxOgpA/Hy1UiuxqRgp2B3XvLL81mQuYDZ6bNZvH8xZdYyx7kwnzCGxA9heMJw+kb31QrtItLoDhYf5LVVr/G/tP8BEOgZyAPdH+CmDjdp+ZIG9O9nlpKfXcrIMV1o1V1jshuTgp2CXb0prSplyYElzN43m3mZ8yisKHSc8/f0Z2DsQIYnDOfC2As1jkVEGlRZVRnTN0/n/U3vU1pVigkT17W7jj9e8EfCfMKcXZ7bWzBjBxvnZpIyMIaht3VwdjnNSrMMdpo80fAqbZWsOriK2emzmZs+l+zSbMc5T7MnfaP7MjxhOEPih9DCt4UTKxURd2IYBrP2zWLKqikcKD4AQI+WPZjQewIdW2hdtcayb1MuP7y5noBQb+6Y1F9jrxtRswx21dRi1zhsho1NOZscky/SCtIc58wmM90jutsnXyQOJzYg1nmFikiTtuPIDl5e8TIrD64EIMo/ikdSH2FE0ggFi0ZWVWHlvUcWYq20cfMzvWkRo16axqJgp2DXqAzDYE/+HvsyKumz2ZK7pcb5DmEdHNubtQ1pq1/GInJGhRWFTFs3jU+3fYrNsOFt8ebuzndzT+d78PXwdXZ5zdb3/1hH+pY8Blzfhu4XaVmsxqJgp2DnVAeKDjgWRF6TvQabYXOciw+M56KEixiWMIyuEV21YKiI1GAYBj/s+YEpq6aQW5YLwMWJF/NIz0fU+u8C1v2azuIvdxGfEsaVD3V3djnNhoKdgp3LyCvLY37GfGanz2bpgaVU2Coc5yJ8IxgaP5RhCcPoHdUbT4tms4k0ZzuP7OSl5S+x+tBqAJKCkpjYeyL9Y/s7uTKplpdVzH+eX47Fw8zo1wbi6aXlrxqDgp2CnUsqrixm0f5FzE6fzcLMhY5NuQECPAO4MPZChsYPZWDcQAK9Ap1YqYg0puLKYqatm8YnWz/Baljxsfhwf7f7uSPlDi2p5GIMw+CjJ5ZQdKScy//YjcROmijXGJplsNOs2KalwlrBioMrHDNsq7tcADzMHvSK7MXQhKEMjR9KlH+UEysVkYZiGAY/p/3MqytfdcyyH54wnD/1+hMxATFOrk5OZe6/t7Fl0QG6DYvnwhvbOrucZqFZBrtqarFremyGjY05G5mbPpc5GXPYm7+3xvmUFimOLltNvhBxD2n5aUxaPomlWUsB+/jbib0nMjBuoJMrkzPZtTqbn9/dRFiMP7c808fZ5TQLCnYKdk1aWn4aczPmMjdjLuuy12Fw/CNavb3ZsIRhXNDyAjzM2tZGpCkpqyrjvY3v8f6m96m0VeJl9uLervdyT+d78LZ4O7s8OQtlRZX867GFYMBdLw/AP0T/3Rqagp2CndvIKc1hQeYC5qbPZWnWUsqt5Y5zwd7BDI4bzND4ofSP6Y+fp58TKxWRM1mYuZBJyyeRWZQJwICYATzR5wkSgrRsRlPzxeSVZO8rZPhdHenQN9rZ5bg9BTsFO7dUUlnC0gNLmZMxhwWZCzhaftRxztviTd/ovgyNH8rg+MGE+4Y7r1ARqeFg8UFeWfEKv6b/CkBLv5ZM6D2BixIu0tCKJmrpt7tZ89M+2vWJ5OK7Ozm7HLenYKdg5/aqbFWszV5r77JNn+toAQAwYaJbRDfH5Ivk4GQnVirSfFXaKvlkyydMWz+N0qpSLCYLv+v4O/7Q/Q/4e/o7uzw5D5nbj/Dfv63FL8iLu14ZoIDewBTsFOyaFcMw2HV0F3PS5zA3Yy6bczfXOJ8cnMzQeHvI06LIIo1jzaE1/HnZn9l1dBcA3SO681Tfp2gf1t7JlUl9sFbaeO+RBVRV2Lj56d60iNX2Yg2pWQY7LXci1Q4WH2RexjzmZsxlxcEVVNmqHOda+LRgSPwQhiUMo090Hw3WFqlneWV5vLbqNf67+78AhHiHMD51PFe1uUr/qHIz37+xjvTN2l6sMTTLYFdNLXZyosKKQhbvX8yc9Dks3F9zUWRfD1/HosiD4gYR7B3sxEpFmraSyhI+2foJ72963/H/2XVtr2Ncj3GE+IQ4tzhpENXbiyV0CuOKP3Z3djlurS7ZRmtFiFsL9Ark0uRLuTT5Uiqtlaw8tJK56falVA6VHGLWvlnM2jcLs8lM94juDIkfwuD4wSQHJWvMiMhZqLRV8s3Ob3hr/VvklOYA0CGsA0/2eZLuLbs7tzhpUHEdwgA4sOMo1kobFk+1yLoCtdhJs2QYBlvytjgWRd55ZGeN8wmBCQyKG8SQ+CH0iOyBp1n72IqcyDAMftn3C2+sfYN9BfsA+zqTD17wIKOSR6nbtRkwDIMPHl9MaUEFV43r7gh6Uv/UFatgJ3W0v2g/8zPmsyBzASsOrqDSVuk4F+AZwIDYAQyOG8zA2IHqVpJmb3POZiYtn8SGnA0AhPmE8fuuv+fGdjfiadE/gpqT2dO3sG3ZQboOi2Pgje2cXY7bUrBTsJPzUFxZzNIDS5mfaQ96eWV5jnPVXbaD4wczJG4IycHqspXm42jZUf6x9h98ueNLDAx8PXy5q9Nd3NnpTi1f0kztWXeY//1zIwFh3tzxUn/9PmwgCnYKdlJPqvexnZ8xn/mZ89lxZEeN83EBcY5xeaktU9VaIW7JZtj4Zuc3vL7mdcfC4Je1uoxHUh8hwi/CucWJU1VVWPnXowupqrBxw8SetEzU37sNQcFOwU4ayIGiA8zPnM/8jPm1dtn2j+nPkPgh6rIVt7E5ZzMvLX+JjTkbAWgT0oYn+jxBr6heTq5MXMVPb29k99rDpI5MpO9VrZ1djltSsFOwk0ZQvcXZvMx5tXbZdovoxsDYgVwYeyEdwjqoi0KalN92u/p7+jO2+1hu7nCzJhNJDduXH+TXD7YQGu3Prc/2cXY5bqlZBjstUCzOZDNsbMrZxLyMebV22Yb7hnNh7IVcGHsh/WL6EeSlz6a4ptq6XS9vdTnjU8er21VqVV5SyfuPLsJmM7jt+b6ERPo5uyS30yyDXTW12IkryCrKYuH+hSzcv5DlWcsprSp1nLOYLPbWvDh7a1770PZqzROXsCV3Cy8te8kx27VNSBue7PMkPaN6OrkycXXf/WMdGVvy6HdNa3qMSHR2OW5HwU7BTlxIhbWC1YdWs2j/IhbuX8je/L01zrf0bcmA2AEMjBtI3+i+BHoFOqlSaa4KKgp4Y80bfL7jc2yGDT8PP8Z2H8stHW9Rt6uclU0L9jP/0+1EJgdx/eP6h0B9U7BTsBMXtr9oP4sy7SFvxcEVNVrzPEwedGt5fGxeu9B2as2TBmMYBt/v+Z4pq6Y4xoiOTBrJo70epaVfSydXJ01JcX450ycsBgPunDyAgFDtw12fFOwU7KSJKLeWs/rQahZmLmTR/kWkFaTVON/SryUDYgbQL6YffaL7EOajld2lfuw4soOXlr3Emuw1ACQHJ/NEnyfoG93XyZVJU/XVX1ZzcE8+g25uR5chcc4ux60o2CnYSROVUZjBov2LWLR/ESuyVlBmLatxvmNYR/rG9KVfdD96RPbA26J/FUvdFFUUMW39ND7d+ilWw4qvhy/3d72fO1Lu0DqMcl7WzkpnyVe7iGkbwjWP9HB2OW5FwU7BTtxAubWcVQdXseTAEpZmLT1pP1tvizc9WvagX0w/+sX0o11oO+3PKadkGAY/pf3Eqytf5XDpYQAuSriIP/X6E9EB0U6uTtxBYV4ZHz2xBExw5yR1x9YnBTsFO3FDOaU5LD2wlGVZy1h2YBnZpdk1zof5hNEnug/9ou1BL8o/ykmViqvZc3QPk5ZPYvnB5QDEB8bzRJ8nuDD2QidXJu7m61dXk7U7nwtvaEu34fHOLsdtKNgp2ImbMwyDPfl7WHpgKUuzlrLy4MoakzAAEoMS6RnZk15RvegV1UuD4ZsZwzBYk72G73Z/x3e7v6PKVoW3xZt7u9zL3Z3vVje+NIgNczNZOGOHZsfWMwU7BTtpZiqtlaw/vJ6lWUtZdmAZm3I3YTNsNa5JCkqiZ1RPekX2omdUTwU9N5VekM4Pe37gu93fsb9ov+P44LjBPN77ceID1YoiDac4v5wPJyzGMOD2F/sRFO7r7JLcgoKdgp00cwUVBaw9tJaVB1ey8tBKtuVtO23Q6xHZQ123TVhafhqz9s1i1r5ZbM3b6jju7+nPJYmXcGXrK7XIsDSa/76+lsxtR7RYcT1SsFOwE6nhbIJetH803SO6061lN7q37E770PZ4mD2cVLGcjmEY7D66m1n7ZvHLvl/YdXSX45zZZKZvdF+ubH0lwxKG4euhFhNpXFsWHWDuv7cRHh/ATU/2dnY5bkHBTsFO5LR+G/S2523HalhrXOPr4UuX8C50i+jGBS0voGtEV4K9g51UsVTPkl64fyELMheQUZjhOOdh8qBPdB8uTryYoQlDtd6hOFVZcSUfPGbfO/bW5/oQGuXv7JKavGYZ7KZOncrUqVOxWq3s2LFDwU6kDkoqS9iUs4m12WtZd3gd6w+vp7Ci8KTrWgW3onN4Zzq16ERKixQ6hHXAx8PHCRW7P8MwyCzKZOmBpSzMXMjygzX3HPY0e9I/pj8XJ17MkPghCt3iUn6Yup59G3PpdXkyvS9PdnY5TV6zDHbV1GIncv5sho09R/ew9vBa1mWvY132OtIL00+6zmKy0DqkNZ1adLJ/hXeiXWg7vCxeTqi66TtQdIAVB1fYW1IPriSrOKvG+Za+LRkYN9Cxr7C/p1pCxDVtX36QXz/YQmiUH7c820dbI54nBTsFO5F6l1uay8acjWzJ3cLm3M1sytnk2F/0RB4mDxKDEmkb2tb+FdKWNqFtiA2I1QLKJ6iwVrAtbxsbczay4fAG1h9eX2MWK9jfyy4RXRgYaw9z7UPb6y9IaRIqSqt4/7FFWKts3PRUL8LjAp1dUpOmYKdgJ9LgDMPgUMkhNuduZnPOZrbkbWFzzmaOlh+t9Xo/Dz/ahLShbWhbWgW3IjEokYSgBOIC4tx+K6viymJ2HtnJrqO72J63nU05m9h2ZBtVtqoa11lMFjqFd6J3VG96RfWie0R3/Dz9nFS1yPn539sb2bP2MD1GJNLvmtbOLqdJU7BTsBNxiuqwt/PITnYe3Wn/88hO9uTvodJWWetzLCYLMQExJAQlkBhoD3uxAbFE+UcR7R9NkFdQk2ilstqsHCo5REZhhuNr99Hd7DyykwPFB2p9Tqh3KF0iutAlvAtdw7vSrWU3da+K29i1Opuf391EQKg3t7/YD7NFLfbnqi7ZRmsZiEi9MZlMRPlHEeUfxcC4gY7jlbZK0gvSHWFvb/5e0gvSSS9Mp7Sq1BGEFrP4pHv6evja7+kXRaR/JKE+oYR5hxHqE2r/3sf+fZBXEH4efljMlnr7eQzDoLSqlMKKQooqizhSdoSc0hwOlx7mcOlhckrs3x8sPkhmUeZJLXAnaunbkrahbWkT0oZO4Z3oEt6F2IDYJhFaRc5FctdwfAI8KTpSzr5NuSR3i3B2Sc2CWuxExGkMw+Bw6WH2FexjX8E+R9jLKs7iYPHBWsfwnYmPxQc/Tz98PXzx8/Szhz2TBYvZgtlktn9/7MuGjUprJZU2+1eFtYJKWyVlVWUUVRZRWFF40jIwp+Nh9iAuII64wDjiA+NJDk6mbYh9rKFmrUpztOSrXaydlU5CpxZc8cduzi6nyVKLnYg0CSaTiZZ+LWnp15JeUb1OOl9WVcahkkMcLD5IVnEWh0sOk1eWx5HyIxwps3/lleVxpOwIFbYK+3OsZZRZy+q1TovJQoBXAEFeQUT4RhDhF0GEbwThvuFE+EUQ6RdJfGA8kX6R9dpiKNLUdRoUw9pZ6aRvyaUgp1RbjDUCBTsRcVk+Hj4kBiWSGHT6bYkMw6DSVklJZQnFVcWUVJZQUlVCcWUxpVWlWG1WbIaNKqMKm2HDarNiNaxYTBY8zB54WjzxNHviZfbC0+KJt8WbQM9AAr3sX74evuoyFTkHwRF+xKeEkbElj80LD2gSRSNQsBORJs9kMuFl8cLL4kUIIc4uR0RO0HlQLBlb8ti65AC9r0jG4qFJFA1J766IiIg0mKQuLfAP8aa0sJKdKw85uxy3p2AnIiIiDcZsMdN1aBwAq3/ah83mVnM2XY6CnYiIiDSozoNj8fbz4OihEvasPezsctyagp2IiIg0KC8fD7o4Wu3ScLOV1lyKgp2IiIg0uG5D4/HwtpCTUcS+TbnOLsdtuWSw++GHH2jfvj1t27blvffec3Y5IiIicp58AjzpPCgWgNX/26dWuwbicsGuqqqK8ePHM2fOHNasWcMrr7xCXl7dV58XERER19L9ongsHmYO7snnwI6jzi7HLblcsFuxYgWdOnUiNjaWwMBARo0axc8//+zsskREROQ8+Qd707F/NADLv9ujVrsGUO/BbsGCBVxxxRXExMRgMpn49ttvT7pm2rRpJCcn4+PjQ2pqKgsXLnScO3DgALGxsY7HcXFx7N+/v77LFBERESdIHZmExdNM1u580jZqrF19q/dgV1xcTLdu3XjzzTdrPT9jxgzGjRvHk08+ydq1axk4cCAjR44kPT0doNb0rq18RERE3ENAqDfdhtlnyC77drfWtatn9R7sRo4cyYsvvsi1115b6/nXXnuN0aNHc++999KxY0def/114uPjeeuttwCIjY2t0UKXmZlJdHR0fZcpIiIiTnLBJYl4+3mQd6CYHcsPOrsct9KoY+wqKipYvXo1l1xySY3jl1xyCUuWLAGgd+/ebNq0if3791NYWMjMmTMZMWLEKe9ZXl5OQUFBjS8RERFxXT7+nvQYkQjA8u/3UFVpdXJF7qNRg11OTg5Wq5XIyMgaxyMjIzl40J7YPTw8mDJlCkOHDuWCCy7gscceo0WLFqe85+TJkwkODnZ8xcfHN+jPICIiIuev69A4/EO8KcorZ8OcTGeX4zacMiv2t2PmDMOocezKK69kx44d7Nq1i9///venvdfEiRPJz893fGVkZDRIzSIiIlJ/PLws9L26FQArZ6ZRfLTcyRW5h0YNduHh4VgsFkfrXLXs7OyTWvHOlre3N0FBQTW+RERExPW17x1FVKsgqsqtLPpyp7PLcQuNGuy8vLxITU1l1qxZNY7PmjWL/v37n9e9p06dSkpKCr169Tqv+4iIiEjjMJlNDLypHSaziV2rstm1OtvZJTV59R7sioqKWLduHevWrQNg7969rFu3zrGcyfjx43nvvfd4//332bp1Kw8//DDp6emMGTPmvF537NixbNmyhZUrV57vjyAiIiKNpGViED1GJAAw/9PtlBRUOLmips2jvm+4atUqhg4d6ng8fvx4AO68806mT5/OTTfdRG5uLi+88AJZWVl07tyZmTNnkpiYWN+liIiISBPQ67Jk0jbkkru/iHmfbGPkmC5aw/YcmQw328+joKCA4OBg8vPzNd5ORESkicjJLOSLyauwWQ0uujuF9n2inF2Sy6hLtnG5vWLPlcbYiYiINF3hcYH0uiwJgIUzdlB0RLNkz4Va7ERERMQl2Kw2vvrLarL3FZLYpQWXPdBVXbI00xY7ERERadrMFjPD70rBbDGxb2Mu62Zpbdq6UrATERERlxEW7c+A69sCsPTb3WRuy3NyRU2L2wQ7jbETERFxD12GxNK+bxSGzeDn9zZTmFfm7JKaDI2xExEREZdTWWHl61dXk5NRRERCINc82gNPL4uzy3IKjbETERGRJs3Ty8KoP3TFJ8CTw+mFzP1oK27WFtUgFOxERETEJQWG+XDp7ztjNpvYuSqbVTPTnF2Sy1OwExEREZcV2y6UQbe0A2DF93vZvvygkytybW4T7DR5QkRExD11GhhL94vt+8nO+XArGVs1U/ZUNHlCREREXF71DNnda7Lx9LFwzSM9iIgPdHZZjUKTJ0RERMStmMwmLr47hdh2IVSWWfnhzfUU5JY6uyyXo2AnIiIiTYLF08yl93chLMafkvwKfnhjPWXFlc4uy6Uo2ImIiEiT4ePvyRV/7IZ/iDdHDpYwc9oGqiqtzi7LZbhNsNPkCRERkeYhINSHK/7YDS9fD7J25/Pzu5uxWW3OLsslaPKEiIiINEn7tx/h+zfWY62y0a53JMPvSsFsNjm7rHqnyRMiIiLi9mLbh3LJvZ0wm03sWHGIef/e1ux3p1CwExERkSarVfcIht/dEUywdUkWs97f0qzDnYKdiIiINGntekUx+Gb77hQ7Vx5i+Xd7nFyR8yjYiYiISJPXeXAcfa5qBcDq/+1jzS/7nFyRcyjYiYiIiFvoOTKJPlcmA7D0692sn5Ph5Ioan4KdiIiIuI3UkUn0uDQRgEWf7+THqeudXFHjcptgp3XsRERExGQy0feqVnQaFAtA2sZcvpmyxslVNR6tYyciIiJuxzAMvnx5Fdn7CgGI6xDKVeMucHJV50br2ImIiEizZjKZuO7xnnj5egCQue0IP7+7yclVNTwFOxEREXFLZrOJu/8yAN9ATwB2rc7mf//c6OSqGpaCnYiIiLgtD08Ld708gIiEQAD2rDvMZ39e7uSqGo6CnYiIiLg1s8XM9RN6EhTuA0Du/mI+fmqJk6tqGAp2IiIi4vbMZhO3PtfX8bggp4wPJy52YkUNQ8FOREREmgWLh5n7Xh/keFx0pNztWu4U7ERERKTZ8PLx4Pd/H+x4XJBTxifPLnNiRfXLbYKdFigWERGRs+HpbWHMm0Mcj48eKuGfD85zWj31SQsUi4iISLNUWW7lnf+b73jsG+TFPX+50IkV1U4LFIuIiIicgae3pcaYu9KCCr6YvNKJFZ0/BTsRERFptrx8PLj7hFa67H2FzPtkmxMrOj8KdiIiItKs+QV5cefkAY7HmxceYM0v+5xY0blTsBMREZFmLyDUm9teOL7O3dKvd7N/+xEnVnRuFOxEREREgJCWfox6oKvj8bd/W0tFaZUTK6o7BTsRERGRY5K7htNlcKzj8bsPL3BiNXWnYCciIiJygkG3tK/xeN6n251USd0p2ImIiIj8xgPThjq+37xgP6WFFU6s5uwp2ImIiIj8hsls4qanju9m9f5ji5xYzdlTsBMRERGpRXhcIN7+Ho7HOZlFTqzm7LhNsNNesSIiIlLf7px0fH27GS+ucGIlZ8dtgt3YsWPZsmULK1c27a1ARERExHV4elsIjfJzPC4+Wu7Eas7MbYKdiIiISEO44YnjvYEzXnLtVjsFOxEREZHT8PSyOL4vLazEMAwnVnN6CnYiIiIiZ3Db88e3G9u+7KATKzk9BTsRERGRMwiJPD7Obt4nrrtgsYKdiIiIyFnoOCAaAGuVzWW7YxXsRERERM5Cv6tbO74vzCtzYiWnpmAnIiIichZ8A70c38930f1jFexEREREzpKnj32GbPrmPCdXUjsFOxEREZGz1P/aNo7vqyqsTqykdgp2IiIiImepegIFwKYF+51YSe0U7ERERETOksVyPDot/nKXEyupnYKdiIiISB14eLpufHLdykRERERc0KixXR3fZ+8rcGIlJ1OwExEREamDuPahju9drTvWJYPdNddcQ2hoKNdff72zSxERERGpwWQyOb4/sPOo8wqphUsGu4ceeoiPPvrI2WWIiIiI1Kpd70jH94bNdbYXc8lgN3ToUAIDA51dhoiIiEit+lzZyvF9xlbXWay4zsFuwYIFXHHFFcTExGAymfj2229PumbatGkkJyfj4+NDamoqCxcurI9aRURERFxCYAsfx/fWKpsTK6nJo65PKC4uplu3btx9991cd911J52fMWMG48aNY9q0aQwYMIC3336bkSNHsmXLFhISEgBITU2lvLz8pOf+8ssvxMTEnMOPISIiItJ4TCYT9/z1Qjy9LXh4WpxdjkOdg93IkSMZOXLkKc+/9tprjB49mnvvvReA119/nZ9//pm33nqLyZMnA7B69epzLPdk5eXlNUJiQYFrTTsWERER9+Qb4OXsEk5Sr2PsKioqWL16NZdcckmN45dccglLliypz5dymDx5MsHBwY6v+Pj4BnkdEREREVdXr8EuJycHq9VKZGRkjeORkZEcPHjwrO8zYsQIbrjhBmbOnElcXBwrV6485bUTJ04kPz/f8ZWRkXHO9YuIiIg0ZXXuij0bJ67vAmAYxknHTufnn38+62u9vb3x9vY+6+tFRERE3FW9ttiFh4djsVhOap3Lzs4+qRVPREREROpXvQY7Ly8vUlNTmTVrVo3js2bNon///vX5UieZOnUqKSkp9OrV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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", "\n", @@ -1024,43 +899,10 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "6bfafa24", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2026-02-02 16:39:20,488 [install] Installing external packages at '/Users/matteoforconi/Documents/GitHub/LAT_MFLike'\n", - "\n", - "================================================================================\n", - "likelihood:soliket.lensing.LensingLikelihood\n", - "================================================================================\n", - "\n", - " 2026-02-02 16:39:20,495 [install] 'soliket.lensing.LensingLikelihood' could not be found as internal, trying external.\n", - " 2026-02-02 16:39:20,796 [install] Checking if dependencies have already been installed...\n", - " 2026-02-02 16:39:20,797 [install] External dependencies for this component already installed.\n", - " 2026-02-02 16:39:20,797 [install] Doing nothing.\n", - "\n", - "================================================================================\n", - "* Summary * \n", - "================================================================================\n", - "\n", - " 2026-02-02 16:39:20,798 [install] All requested components' dependencies correctly installed at /Users/matteoforconi/Documents/GitHub/LAT_MFLike\n" - ] - }, - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "install(\n", " {\"likelihood\": {\"soliket.lensing.LensingLikelihood\": None}},\n", @@ -1074,61 +916,10 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "72279810", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2026-02-02 16:39:23,626 [model] *WARNING* Ignored blocks/options: ['sampler', 'output']\n", - " 2026-02-02 16:39:23,644 [camb] `camb` module loaded successfully from /Users/matteoforconi/Documents/GitHub/CAMB-1.6.0/camb\n", - " 2026-02-02 16:39:27,355 [mflike.ttteee] Number of bins used: 3087\n", - " 2026-02-02 16:39:27,376 [mflike.ttteee] Initialized!\n", - " 2026-02-02 16:39:27,382 [soliket.lensinglikelihood] Initialising CMB Lensing...\n", - " 2026-02-02 16:39:27,382 [soliket.lensinglikelihood] Loading data from /Users/matteoforconi/Documents/GitHub/LAT_MFLike/data/LensingLikelihood/clkk_reconstruction_sim.fits...\n", - " 2026-02-02 16:39:27,416 [soliket.lensinglikelihood] Loading fiducial Cls from file: fiducial_lensing.sacc.fits\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/tracers.py:44: UserWarning: Unknown quantity cmb_lens_potential. If possible use a pre-defined quantity, or add to the list.\n", - " warnings.warn(f\"Unknown quantity {quantity}. \"\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2026-02-02 16:39:27,995 [soliket.lensinglikelihood] Loading correction factors from file: corrections_lensing.sacc.fits\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N0_00. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N0_ee. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N0_bb. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N0_0e. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N1_00. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N1_ee. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N1_bb. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n", - "/Users/matteoforconi/.local/lib/python3.11/site-packages/sacc/data_types.py:304: UserWarning: Unknown data_type value N1_0e. If possible use a pre-defined type, or add to the list.\n", - " warnings.warn(f\"Unknown data_type value {data_type}. \"\n" - ] - } - ], + "outputs": [], "source": [ "lensing_config = {\n", " \"data_folder\": \"LensingLikelihood/\",\n", @@ -1145,22 +936,10 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "11696698", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "loglike:\n", - "\tTTTEEE-> -48267.5\n", - " \tLensing-> 264.417\n", - "\n", - "logpost -> -48005.6\n" - ] - } - ], + "outputs": [], "source": [ "print(\"loglike:\")\n", "print(\n", @@ -1184,29 +963,10 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "ce239d81", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Filename: /Users/matteoforconi/Documents/GitHub/LAT_MFLike/data/LensingLikelihood/clkk_reconstruction_sim.fits\n", - "No. Name Ver Type Cards Dimensions Format\n", - " 0 PRIMARY 1 PrimaryHDU 5 () \n", - " 1 window:TopHat 1 BinTableHDU 17 0R x 3C [D, D, D] \n", - " 2 window:LogTopHat 1 BinTableHDU 17 0R x 3C [D, D, D] \n", - " 3 window:Bandpower 1 BinTableHDU 17 3000R x 2C [K, 19D] \n", - " 4 tracer:Misc 1 BinTableHDU 15 0R x 2C [D, D] \n", - " 5 tracer:Misc 1 BinTableHDU 15 0R x 2C [D, D] \n", - " 6 tracer:Map:ck:beam 1 BinTableHDU 19 3000R x 2C [K, D] \n", - " 7 tracer:Map:ck:beam 1 BinTableHDU 19 3000R x 2C [K, D] \n", - " 8 data:cl_00 1 BinTableHDU 24 19R x 6C [2A, 2A, D, K, D, K] \n", - " 9 covariance 1 ImageHDU 11 (19, 19) float64 \n" - ] - } - ], + "outputs": [], "source": [ "from astropy.io import fits\n", "\n", @@ -1218,51 +978,22 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "3ad29081", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ColDefs(\n", - " name = 'tracer_0'; format = '2A'\n", - " name = 'tracer_1'; format = '2A'\n", - " name = 'value'; format = 'D'\n", - " name = 'window'; format = 'K'\n", - " name = 'ell'; format = 'D'\n", - " name = 'window_ind'; format = 'K'\n", - ")" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "hdul[8].columns" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "ade748e0", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnQAAAHWCAYAAAD+VRS3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAgAVJREFUeJzt3Xd0VNXexvHvzKSHFAhpkNB7h9ClY0BAVKyACoqiKIiCqASwXhWsoFewCyKKyKWIoihKlxIIvXcIJRWSAOkz5/0jkNdAKqTzfNaapXNm7zO/CSE82efsvU2GYRiIiIiISJllLukCREREROTGKNCJiIiIlHEKdCIiIiJlnAKdiIiISBmnQCciIiJSxinQiYiIiJRxCnQiIiIiZZwCnYiIiEgZp0AnIiIiUsYp0IkUgVmzZmEymdiyZUtJl3KNGjVq8Mgjj5R0GaXKmTNneO2119i+fXtJl3LT0PehSOGyK+kCRKR4LVq0CHd395Iuo1Q5c+YMr7/+OjVq1KBFixYlXc5NQd+HIoVLgU7kJtOyZcuSLqHMS0xMxMXFpaTLKJOSkpJwdnbW96FIIdMlV5ESdOjQIQYPHoyPjw+Ojo40bNiQ6dOnZ2mzatUqTCYTc+fOZeLEiVSpUgV3d3duvfVWDhw4kKXttm3buP322zPPV6VKFfr168epU6cy21x9qasg5zcMg7fffpvq1avj5ORE69atWb58Od26daNbt255ft758+fTrl07PDw8cHFxoVatWgwbNuyaWubMmcPYsWPx8/PD2dmZrl27sm3btmvOt2XLFu644w4qVaqEk5MTLVu25Keffrqm3enTp3niiScIDAzEwcGBKlWqcO+99xIZGcmqVato06YNAI8++igmkwmTycRrr70GwCOPPEKFChXYtWsXvXr1ws3NjZ49ewKwfPly7rzzTgICAnBycqJOnTo8+eSTxMTE5Pm1AIiLi+P555+nVq1aODo64uPjQ9++fdm/f39mm3PnzvH0009TtWpVHBwcqFWrFhMnTiQlJSXLuUwmE6NGjWLmzJnUr18fZ2dnWrduzcaNGzEMg/fee4+aNWtSoUIFevToweHDh7P079atG02aNGHt2rW0b98eZ2dnqlatyssvv4zVas3S9vXXX6ddu3ZUqlQJd3d3WrVqxddff41hGFna1ahRg9tvv52FCxfSsmVLnJyceP311zNf+/f3oc1m480338ys3dPTk2bNmvHRRx9lOee6devo2bMnbm5uuLi40LFjR5YuXZqlzZVbHlauXMlTTz1F5cqV8fLy4u677+bMmTP5+rMRKWs0QidSQvbu3UvHjh2pVq0aH3zwAX5+fvzxxx+MHj2amJgYXn311SztJ0yYwC233MJXX31FQkICL730Ev3792ffvn1YLBYuXbpEcHAwNWvWZPr06fj6+hIREcHKlSu5cOFCnvXkdX6AiRMnMnnyZJ544gnuvvtuwsPDefzxx0lLS6NevXq5nn/Dhg088MADPPDAA7z22ms4OTlx4sQJVqxYkW0trVq14quvviI+Pp7XXnuNbt26sW3bNmrVqgXAypUrue2222jXrh2fffYZHh4e/PjjjzzwwAMkJiZmhoXTp0/Tpk0b0tLSmDBhAs2aNSM2NpY//viD8+fP06pVK2bOnMmjjz7KpEmT6NevHwABAQGZ9aSmpnLHHXfw5JNPMn78eNLT0wE4cuQIHTp04PHHH8fDw4Pjx4/z4Ycf0qlTJ3bt2oW9vX2OX48LFy7QqVMnjh8/zksvvUS7du24ePEia9as4ezZszRo0IDk5GS6d+/OkSNHeP3112nWrBlr165l8uTJbN++/Zog8+uvv7Jt2zamTJmCyWTipZdeol+/fgwdOpSjR4/yySefEB8fz9ixY7nnnnvYvn07JpMps39ERAQDBw5k/PjxvPHGGyxdupQ333yT8+fP88knn2S2O378OE8++STVqlUDYOPGjTzzzDOcPn2aV155JUtNW7duZd++fUyaNImaNWvi6uqa7dfj3Xff5bXXXmPSpEl06dKFtLQ09u/fT1xcXGab1atXExwcTLNmzfj6669xdHRkxowZ9O/fn7lz5/LAAw9kOefjjz9Ov379+OGHHwgPD+eFF17goYceyvZ7TqTMM0Sk0M2cOdMAjM2bN+fYpnfv3kZAQIARHx+f5fioUaMMJycn49y5c4ZhGMbKlSsNwOjbt2+Wdj/99JMBGBs2bDAMwzC2bNliAMbixYtzra169erG0KFDM5/n9/znzp0zHB0djQceeCBLuw0bNhiA0bVr11zf9/333zcAIy4uLsc2V2pp1aqVYbPZMo8fP37csLe3Nx5//PHMYw0aNDBatmxppKWlZTnH7bffbvj7+xtWq9UwDMMYNmyYYW9vb+zduzfH9928ebMBGDNnzrzmtaFDhxqA8c033+T6+Ww2m5GWlmacOHHCAIyff/451/ZvvPGGARjLly/Psc1nn31mAMZPP/2U5fg777xjAMaff/6ZeQww/Pz8jIsXL2YeW7x4sQEYLVq0yPL1nDZtmgEYO3fuzDzWtWvXbOsePny4YTabjRMnTmRbo9VqNdLS0ow33njD8PLyyvI+1atXNywWi3HgwIFr+l39fXj77bcbLVq0yPFrYRiG0b59e8PHx8e4cOFC5rH09HSjSZMmRkBAQOZ7X/n79/TTT2fp/+677xqAcfbs2VzfR6QsKlOXXNesWUP//v2pUqUKJpOJxYsXF+n71ahRI/Pyy78fI0eOLNL3lfIvOTmZv//+mwEDBuDi4kJ6enrmo2/fviQnJ7Nx48Ysfe64444sz5s1awbAiRMnAKhTpw4VK1bkpZde4rPPPmPv3r0Fqimv82/cuJGUlBTuv//+LO3at29PjRo18jz/lcua999/Pz/99BOnT5/Ose3gwYOzjBxVr16djh07snLlSgAOHz7M/v37efDBBwGu+fqdPXs283Lx77//Tvfu3WnYsGGeNebmnnvuueZYVFQUI0aMIDAwEDs7O+zt7alevToA+/bty/V8v//+O/Xq1ePWW2/Nsc2KFStwdXXl3nvvzXL8yujj33//neV49+7ds4yAXfnMffr0yfL1vHL8yp/tFW5ubtd8HwwePBibzcaaNWuy1HXrrbfi4eGBxWLB3t6eV155hdjYWKKiorL0b9asWZ6jtwBt27Zlx44dPP300/zxxx8kJCRkef3SpUts2rSJe++9lwoVKmQet1gsPPzww5w6deqaWwTy+p4WKU/KVKC7dOkSzZs3zzL0X5Q2b97M2bNnMx/Lly8H4L777iuW95fyKzY2lvT0dP773/9ib2+f5dG3b1+Aa+7D8vLyyvLc0dERyLjJHMDDw4PVq1fTokULJkyYQOPGjalSpQqvvvoqaWlpedaU1/ljY2MB8PX1vaZvdseu1qVLFxYvXkx6ejpDhgwhICCAJk2aMHfu3Gva+vn5ZXvsSg2RkZEAjBs37pqv39NPPw38/9cvOjo6y+XT6+Hi4nLNjEybzUavXr1YuHAhL774In///TehoaGZQfzK1y0n+akrNjYWPz+/LGEMwMfHBzs7u8yvxxWVKlXK8tzBwSHX48nJyVmOZ/fneOXP4sp7hYaG0qtXLwC+/PJL/vnnHzZv3szEiROBaz+3v79/rp/xipCQEN5//302btxInz598PLyomfPnplL/5w/fx7DMLI9X5UqVbLUeEVe39Mi5UmZuoeuT58+9OnTJ8fXU1NTmTRpEt9//z1xcXE0adKEd955J183a2fH29s7y/MpU6ZQu3Ztunbtel3nE7miYsWKmSMLOY341qxZs8Dnbdq0KT/++COGYbBz505mzZrFG2+8gbOzM+PHj7+hmq/843glTP1bREREvkbp7rzzTu68805SUlLYuHEjkydPZvDgwdSoUYMOHTpkOV9273GlhsqVKwMZIeDuu+/O9r3q168PZPw9/vekkOtxdaAC2L17Nzt27GDWrFkMHTo08/jVkw1ykp+6vLy82LRpE4ZhZKkhKiqK9PT0zK9DYcnpz/ZKLQA//vgj9vb2/Prrrzg5OWW2y+mKSXZfu+zY2dkxduxYxo4dS1xcHH/99RcTJkygd+/ehIeHU7FiRcxmM2fPnr2m75WJDoX99RApS8rUCF1eHn30Uf755x9+/PFHdu7cyX333cdtt93GoUOHbvjcqampzJkzh2HDhuX7B5RITlxcXOjevTvbtm2jWbNmtG7d+prH1aMLBWEymWjevDlTp07F09OTrVu33nDN7dq1w9HRkXnz5mU5vnHjxgJfwnJ0dKRr16688847ANfMYJ07d26WGZMnTpxg/fr1mb+c1a9fn7p167Jjx45sv3atW7fGzc0NyPhFcOXKlddcjru6HijYyM2VnwNX+l7x+eef56t/nz59OHjwYK436Pfs2ZOLFy9eE5Zmz56d+XphunDhAkuWLMly7IcffsBsNtOlSxcg43Pb2dllTpSBjK/bd999V2h1eHp6cu+99zJy5EjOnTvH8ePHcXV1pV27dixcuDDLn5PNZmPOnDkEBATk69KuSHlVpkbocnPkyBHmzp3LqVOnMoffx40bx7Jly5g5cyZvv/32DZ1/8eLFxMXFaWVzKZAVK1Zw/Pjxa4737duXjz76iE6dOtG5c2eeeuopatSowYULFzh8+DC//PJLgWfi/frrr8yYMYO77rqLWrVqYRgGCxcuJC4ujuDg4Bv+LJUqVWLs2LFMnjyZihUrMmDAAE6dOsXrr7+Ov78/ZnPuvx++8sornDp1ip49exIQEEBcXBwfffQR9vb214x6R0VFMWDAAIYPH058fDyvvvoqTk5OhISEZLb5/PPP6dOnD7179+aRRx6hatWqnDt3jn379rF161bmz58PwBtvvMHvv/9Oly5dmDBhAk2bNiUuLo5ly5YxduxYGjRoQO3atXF2dub777+nYcOGVKhQgSpVqmT+LMnOlX7jx4/HMAwqVarEL7/8knlrRl6ee+455s2bx5133sn48eNp27YtSUlJrF69mttvv53u3bszZMgQpk+fztChQzl+/DhNmzZl3bp1vP322/Tt2zfX+++uh5eXF0899RQnT56kXr16/Pbbb3z55Zc89dRTmTNa+/Xrx4cffsjgwYN54okniI2N5f33378m2BZU//79adKkCa1bt8bb25sTJ04wbdo0qlevTt26dQGYPHkywcHBdO/enXHjxuHg4MCMGTPYvXs3c+fO1S/bcnMryRkZNwIwFi1alPn8yow8V1fXLA87Ozvj/vvvNwzDMI4dO2YAuT5GjhyZ7fv16tXLuP3224vjo0k5cGWWXU6PY8eOGYaR8T05bNgwo2rVqoa9vb3h7e1tdOzY0XjzzTczz3Vl5uf8+fOzvMeV7+crMzP3799vDBo0yKhdu7bh7OxseHh4GG3btjVmzZqVpV9Os1zzOr9hZMzkfPPNN42AgADDwcHBaNasmfHrr78azZs3NwYMGJDr1+TXX381+vTpY1StWtVwcHAwfHx8jL59+xpr1669ppbvvvvOGD16tOHt7W04OjoanTt3NrZs2XLNOXfs2GHcf//9ho+Pj2Fvb2/4+fkZPXr0MD777LMs7cLDw41hw4YZfn5+hr29vVGlShXj/vvvNyIjIzPbzJ0712jQoIFhb29vAMarr75qGEbGLFdXV9dsP9PevXuN4OBgw83NzahYsaJx3333GSdPnszSPzfnz583nn32WaNatWqGvb294ePjY/Tr18/Yv39/ZpvY2FhjxIgRhr+/v2FnZ2dUr17dCAkJMZKTk7OcK7ufX1f+DN97770sx7P7M+/atavRuHFjY9WqVUbr1q0NR0dHw9/f35gwYcI1M4m/+eYbo379+oajo6NRq1YtY/LkycbXX3+d5XvbMDK+1/r165ftZ7/6+/CDDz4wOnbsaFSuXNlwcHAwqlWrZjz22GPG8ePHs/Rbu3at0aNHD8PV1dVwdnY22rdvb/zyyy9Z2uQ0y/zK5165cmW2NYmUZSbDuGolyDLCZDKxaNEi7rrrLgDmzZvHgw8+yJ49e7JcCgCoUKECfn5+pKWlceTIkVzPW7FixWtuDD5x4gS1atVi4cKF3HnnnYX6OUTKumPHjtGgQQNeffVVJkyYcEPnWrVqFd27d2f+/PnXzOyUotWtWzdiYmLYvXt3SZciIteh3FxybdmyJVarlaioKDp37pxtG3t7exo0aFDgc8+cORMfH5/MBUdFblY7duxg7ty5dOzYEXd3dw4cOMC7776Lu7s7jz32WEmXJyJy0ypTge7ixYtZZpAdO3aM7du3U6lSJerVq8eDDz7IkCFD+OCDD2jZsiUxMTGsWLGCpk2bZi4FUVA2m42ZM2cydOhQ7OzK1JdLpNC5urqyZcsWvv76a+Li4vDw8KBbt2689dZb+Vq6REREikaZuuR65XLM1YYOHcqsWbNIS0vjzTffZPbs2Zw+fRovLy86dOjA66+/TtOmTa/rPf/880969+7NgQMHNINKRERESqUyFehERERE5Frlah06ERERkZuRAp2IiIhIGVegu/wnT57MwoUL2b9/P87OznTs2JF33nknc4ud7OR039u+ffvyPePUZrNx5swZ3NzctHCkiIiI3BQMw+DChQtUqVIlz8XbCxToVq9ezciRI2nTpg3p6elMnDiRXr16sXfvXlxdXXPte+DAgSybW1+9T2puzpw5Q2BgYEFKFRERESkXwsPDCQgIyLVNgQLdsmXLsjy/sj5bWFhY5j5/OfHx8cHT07Mgb5fpyp6M4eHhWUKhiIiISHmVkJBAYGBgZg7KzQ0trBYfHw9k7PGYl5YtW5KcnEyjRo2YNGlStpdhc3LlMqu7u7sCnYiIiNxU8nO72XUHOsMwGDt2LJ06daJJkyY5tvP39+eLL74gKCiIlJQUvvvuO3r27MmqVatyHNVLSUkhJSUl83lCQsL1likiIiJS7l33OnQjR45k6dKlrFu3Ls/rulfr378/JpOJJUuWZPv6a6+9xuuvv37N8fj4eI3QiYiIyE0hISEBDw+PfOWf61q25JlnnmHJkiWsXLmywGEOoH379hw6dCjH10NCQoiPj898hIeHX0+ZIiIiIjeFAl1yNQyDZ555hkWLFrFq1Spq1qx5XW+6bds2/P39c3zd0dERR0fH6zq3iIiIFIzVaiUtLa2ky7jp2NvbY7FYCuVcBQp0I0eO5IcffuDnn3/Gzc2NiIgIADw8PHB2dgYyRtdOnz7N7NmzAZg2bRo1atSgcePGpKamMmfOHBYsWMCCBQsK5QOIiIjI9TEMg4iICOLi4kq6lJuWp6cnfn5+N7zOboEC3aeffgpAt27dshyfOXMmjzzyCABnz57l5MmTma+lpqYybtw4Tp8+jbOzM40bN2bp0qX07dv3hgoXERGRG3MlzPn4+ODi4qLF+4uRYRgkJiYSFRUFkOuVy/y47kkRxakgNwWKiIhI3qxWKwcPHsTHxwcvL6+SLuemFRsbS1RUFPXq1bvm8muRT4oQERGRsu3KPXMuLi4lXMnN7crX/0bvYVSgExERuYnpMmvJKqyvvwKdiIiISBmnQCflns1qJTI0lONLlxIZGorNai3pkkREpIgcP34ck8nE9u3bS7qUYnVDe7mKlHbhy5cTNnkyiZGRmcdcfH0JCgkhMDi4BCsTEZGCyuvy5NChQ3nttdeKp5hSRoFOyq3w5ctZO2YMXDWROzEqirVjxtB56lSFOhGRMuTs2bOZ/z9v3jxeeeUVDhw4kHnM2dmZ8+fPl0RpWK1WTCYTZnPJXPzUJVcpl2xWK2GTJ18T5oDMY2FTpujyq4hIGeLn55f58PDwwGQyXXPsiqNHj9K9e3dcXFxo3rw5GzZsyHKu9evX06VLF5ydnQkMDGT06NFcunQp8/Xz588zZMgQKlasiIuLC3369MmybemsWbPw9PTk119/pVGjRjg6OrJ27Vrs7e0zN1644vnnn6dLly5F9FXJoEAn5VJ0WFiWy6zXMAwSIyKI3LSp+IoSESnFDMMgMTW9RB5FsSTuxIkTGTduHNu3b6devXoMGjSI9PR0AHbt2kXv3r25++672blzJ/PmzWPdunWMGjUqs/8jjzzCli1bWLJkCRs2bMAwDPr27ZtleZHExEQmT57MV199xZ49e2jdujW1atXiu+++y2yTnp7OnDlzePTRRwv9M/6bLrlKuZQUHZ2vdqtHjcK/Qwf8OnbEr3173GvV0hR+EbkpJaVZafTKHyXy3nvf6I2LQ+FGknHjxtGvXz8AXn/9dRo3bszhw4dp0KAB7733HoMHD+a5554DoG7dunz88cd07dqVTz/9lPDwcJYsWcI///xDx44dAfj+++8JDAxk8eLF3HfffUDG2nEzZsygefPmme/72GOPMXPmTF544QUAli5dSmJiIvfff3+hfr6rKdBJueTs7Z2vdraUFE6vWsXpVasAcPHzw699e/w6dMC3fXucK1cuwipFRKSoNGvWLPP/r2yrFRUVRYMGDQgLC+Pw4cN8//33mW0Mw8Bms3Hs2DEOHTqEnZ0d7dq1y3zdy8uL+vXrs2/fvsxjDg4OWd4HMkb2Jk2axMaNG2nfvj3ffPMN999/P66urkX1UQEFOimnvIOCcPH1zfmyq8mEi68vnadNI3LTJs5u2ED01q0kRkRwdPFiji5eDIBnvXoZo3cdOuATFISds3O+3t9mtRIdFkZSdDTO3t54BwVhvmpLFxGR0sTZ3sLeN3qX2HsXNnt7+8z/v3LlxWazZf73ySefZPTo0df0q1atGgcPHsz2nIZhZLmK4+zsfM1VHR8fH/r378/MmTOpVasWv/32G6suDxoUJQU6KZfMFgtBISGsvTycnsXlv3xB48fj1bQpXk2b0ujxx0lPTiZ661Yi1q8nYsMGzu/fT9zBg8QdPMj+WbMw29vj3bJlZsCr2LBhtiFNS6WISFlkMpkK/bJnadWqVSv27NlDnTp1sn29UaNGpKens2nTpsxLrrGxsRw8eJCGDRvmef7HH3+cgQMHEhAQQO3atbnlllsKtf7s3Bx/cnJT8mnTBrO9Pbar9sdz8fUlaPz4a8KVnZMT/h074n/5L2/yuXNEbNxIxIYNRKxfnzGJIjSUyNBQdkybhoOHB77t2mXeg1chIEBLpYiIlAEvvfQS7du3Z+TIkQwfPhxXV1f27dvH8uXL+e9//0vdunW58847GT58OJ9//jlubm6MHz+eqlWrcuedd+Z5/t69e+Ph4cGbb77JG2+8UQyfSIFOyrEj//sftrQ0PBs0oNWLL5IcE1Ogy59OlSpRo29favTti2EYXDh+PCPcbdhAZGgoqfHxhP/5J+F//gmAa0AAybGxOS+VYjIRNmUKVXv00OVXEZES1KxZM1avXs3EiRPp3LkzhmFQu3ZtHnjggcw2M2fO5Nlnn+X2228nNTWVLl268Ntvv2W5lJsTs9nMI488wttvv82QIUOK8qNkMhlFMVe4kCUkJODh4UF8fDzu7u4lXY6UAbb0dJb07k1iRATt33yTWgMGFPr5Y3ftygx4MTt3YlyeDp+XnjNn4tu2baHWIyJSUMnJyRw7doyaNWvi5ORU0uWUO8OHDycyMpIlS5bk2i63P4eC5B+N0Em5dGrFChIjInCsWJHqffsW+vnNdnZ4t2yJd8uWNH36adIuXmTXp5+yf9asPPvmd0kVEREpe+Lj49m8eTPff/89P//8c7G9rxYWlnLp4OWp6HXuuw+Lo2ORv599hQpU7do1X23zu6SKiIiUPXfeeSd33HEHTz75JMHFeM+0Ruik2MUnphF+PhGbYWAzwGYYGMblNYAuP7cZBhhkeZ5xyMBmI7Mv/LtPxjmsx4+QvGULmM3sb9iF/ZtPYhjg7mxPh1peVHR1KJLPlblUSlRU9vfRXV4qxTsoqEjeX0RESl5xLFGSHQU6KXIp6Va2nohj3eFo1h2OZdepuMthrGjcvX0+rYGdfk35ccUZ4EzmayYTNA/wpFt9b7rW86ZZgCcWc+HsDJG5VMqYMRlvdHWoMwyCxo/XhAgRESl0CnRS6AzD4EDkBdYdimHd4Rg2HT1HUpo1SxtvN0ccLBlX/M1mMJtMmE0mTCYwkfX5v/9rNmWslWT+1/F/P3dMvkjL37YBkNS1Hz0CfMjIayZOnU9kf8QFtofHsT08jml/HcLTxZ7OdTPCXZd6lfFxu7EbgwODg+k8deo169ABuNeuTcCtt97Q+UVERLKjQCeFIiI+mXWHY/jncEaIi76QkuX1yhUc6VTHi051vbmljhf+HvnbcaGg9nz5JTus6VRq3JhpEwdfs4J3RHwyaw5Gs/pgNGsORROXmMYvO87wy46MUbzGVdzpWi8j4LWqXhF7S8FvMw0MDqZqjx6ZO0WYzGY2TJhAwpEjhP/5J9V6l8xK7CIiUn4p0Ml1uZiSzqajsaw9lBHiDkVdzPK6k72ZdjW96Fy3MrfUqUwDP7ci3/TelpbGoR9/BKD+Qw9l+35+Hk7c3yaQ+9sEkm61sT08jtWXA97OU/HsOZPAnjMJzFh1BDdHO26pU5muly/PVvHMfwg1WyxZliZJOHaMXdOns/Xdd6nSuTN2Li43/oFFREQuU6CTfEm32thxKp51lwPc1pPnSf/XjXAmEzSr6kGnywEuqHpFHO2K916xK0uVOHl5Ue222/Jsb2cx07pGJVrXqMTzveoTczGFtYeiWX0gmjWHYjh3KZVleyJYticCgLo+FS7fe+dDm5oF+3wNhw3j6KJFXDpzhr1ff02zZ5657s8pIiJyNQU6yZZhGByLucS6wzGsOxTDhqOxXEjOunButUoudKpbmc51KtOhtheeLkUzezS/DsyZA0Cd++/H4lDwWipXcGRAywAGtAzAZjPYfSaeVQcyRu+2nTzPoaiLHIq6yJdrj+Fsb6FDbS+61vOmW31vqnu55npuOycnWr7wAuvGjGHvN99Qa8AAKgQEXNfnFBERuZoCnWSKvZjCP0di+efyZIbTcUlZXvdwtueWOl50quNNpzqVqeZVei4bntu7l+itWzHZ2VH3X1u3XC+z2USzAE+aBXgyumdd4hPTWHc4htUHo1h9MJrIhBRW7I9ixf4oAGp4uWTce1ffm/a1vLLd4DowOBjfdu2I3LSJre+9R5ePPrrhOkVEJEO3bt1o0aIF06ZNK+lSSoQC3U3MZjPYeDSW1QejWXc4hj1nErK87mAxE1S9YsYoXN3KNK7iUWhLfBS2KwsJV+vdu0gW7vVwsadfM3/6NfPHMAz2R1zIuPfuQDRbTpzjeGwixzec4NsNJ3CwM9OuZiW61vOmZ0NfalbOGL0zmUwETZjA73ffzam//uLs+vX4d+xY6LWKiBQnm9WaOQmsIPtll6RVq1bRvXt3zp8/j6enZ0mXUygU6G5C6VYbS3edZfrKwxyMzDqZoYGfG53rVqZTXW/a1qiEs0Pp/ksJkBwby/GlS4GMyRBFzWQy0dDfnYb+7ozoWpuLKemsPxzD6oPRrDoQzem4JNYeimHtoRje+m0fD7QOZFzv+lSu4IhnnTrUGzSIA3PmEDZ5Mn0XLsScj42eRURKo/Dly69ZpsnF15egkBACi3GXBNHWXzeV1HQb8zafpOeHq3n2x+0cjLyIm6Md9wYF8NHAFmyeeCvLnuvCxH6N6FrPu0yEOYDD8+djS0vDq2lTKjdrVuzvX8HRjl6N/XhrQFPWvdSdv8Z25eXbG9GpTmUMA37cHE7391fx9bpjpFltNB05EseKFUk4epSDP/xQ7PWKiBSG8OXLWTtmzDVrbiZGRbF2zBjCly8vsve+dOkSQ4YMoUKFCvj7+/PBBx9keX3OnDm0bt0aNzc3/Pz8GDx4MFFRGbfIHD9+nO7duwNQsWJFTCYTjzzyCADLli2jU6dOeHp64uXlxe23386RI0eK7HMUJo3Q3QSS06zM2xzO56uPcCY+GYCKLvY83rkWD3eojrtT2R0h+vdSJfWKYXQuLyaTiTo+FajjU4HHOtUk7MQ5Xluyl12n4/nPr3uZG3qSV/s3ovlzzxH66qvsmjGD6v364Vy5ckmXLiI3OcMwsCYl5d2QjMusW95+O/ttDi8f2zJ5Mr7t2+fr8qvF2blAS1u98MILrFy5kkWLFuHn58eECRMICwujRYsWAKSmpvKf//yH+vXrExUVxZgxY3jkkUf47bffCAwMZMGCBdxzzz0cOHAAd3d3nJ0zlqW6dOkSY8eOpWnTply6dIlXXnmFAQMGsH37dszm0j0GZjKM7P40SpeEhAQ8PDyIj4/H3d29pMspMy6mpPP9xhN8ufYYMRczFvr1cXPkiS61GNyuWrY37pc1x3/7jfUvvIBT5crcuXz5dc1uLWpWm8H8LeG898cBYi+lAhDcwJv+S97l4oF91Lr7btr/5z8lXKWI3GySk5M5duwYNWvWxMnJifTERH5q06ZEarl/8+Z8r8958eJFvLy8mD17Ng9cngR37tw5AgICeOKJJ7KdFLF582batm3LhQsXqFChQr7voYuOjsbHx4ddu3bRpEmT6/loebr6z+HfCpJ/SnfclOsSn5jGR38dotM7K5j8+35iLqZQ1dOZ/9zVhDUvdufxzrXKRZiD/58MUfeBB0plmAOwmE0MbFuNFeO6MeyWmtiZTSzfH800354AHF24kJidO0u4ShGRsuHIkSOkpqbSoUOHzGOVKlWifv36mc+3bdvGnXfeSfXq1XFzc6Nbt24AnDx5Ms9zDx48mFq1auHu7k7NmjXz1a80KB//qguQsezI1+uOMXvDCS6mZKwZV7OyK093q81dLate1zZWpVnsrl3EbN+O2c6OOvfdV9Ll5MnD2Z5X+jdiUNtA3vh1L2sPwdaAIFqdCuOvSa9z/6KfSv3MMBEpvyzOzty/eXO+2kaFhbFqxIg823X77DN8goLy9d75ldeFxUuXLtGrVy969erFnDlz8Pb25uTJk/Tu3ZvU1NRc+/bv35/AwEC+/PJLqlSpgs1mo0mTJnn2Kw0U6MqBiPhkvlhzlB9CT5CcZgMyZqs+3b0O/Zr6l9qlRm7UgcsTCqr16VMkS5UUlbq+bswe1pbleyP50CmNxgt34XhkP+PGTGXYS8NpUtWjpEsUkZuQyWTK92VPv44dcfH1JTEqKvv76EwmXHx98evYsdB/Ua1Tpw729vZs3LiRatWqAXD+/HkOHjxI165d2b9/PzExMUyZMoXAwEAAtmzZkuUcDpev6Fit1sxjsbGx7Nu3j88//5zOnTsDsG7dukKtvSgp0JVh4ecS+XT1Ef635RSp1owg1zzAg1E96tKzgQ/mchrkAJKiozn5228A1H/wwRKupuBMJhO9GvvRpV5/5qQdhMXfUn/dfO51qMmAjnUZ16s+XhUcS7pMEZFsmS0WgkJCWDtmTMbej/8OdZcnNwSNH18kVx0qVKjAY489xgsvvICXlxe+vr5MnDgxc9JCtWrVcHBw4L///S8jRoxg9+7d/Oeq+5SrV6+OyWTi119/pW/fvjg7O1OxYkW8vLz44osv8Pf35+TJk4wfP77Q6y8q5esa3E3icNRFxv60nW7vr+KHTSdJtdpoW7MSs4e1ZfHIWwhu5FuuwxxcXqokPR2v5s3xatq0pMu5bk72Fh599Tmcq1XDLeUi3Q/8xdzQcLq9v4pvLi9zIiJSGgUGB9N56lRcfHyyHHfx9aXz1KlFug7de++9R5cuXbjjjju49dZb6dSpE0GXL+16e3sza9Ys5s+fT6NGjZgyZQrvv/9+lv5Vq1bl9ddfZ/z48fj6+jJq1CjMZjM//vgjYWFhNGnShDFjxvDee+8V2WcobJrlWobsPZPA9JWH+W332cxfhjrXrcyo7nVoV8urZIsrRtbUVH4ODiY5JoaO775LjX79SrqkG3Zm7dqM+1EsFn6552U2JGVc9qjrU4FX+zemU10tayIihSu32ZUFURZ3iihNCmuWqy65lgHbTp5n+srD/LUvKvNYcCNfRnWvQ/NAz5IrrISE//knyTExOPv4UK1Xr5Iup1BU6dyZKl27cmb1aoaf+pP+j0/i/T8PcijqIg99vYnejX2Z1K8RgZVKz/65IiKQcfnVt23bki7jpqdAV0oZhsGmY+f4ZMVh1h2OATJuS7i9WRVGdq9NA7+bd6TywL+WKilP22YFvfQSEevXE7F+PV0GHaHf892Y9vdBZm84wR97Ill5IJonu9TiqW61y82yMyIiUjh0D10pYxgGqw5Ecd9nGxj4xUbWHY7Bzmzi3qAA/hrblf8OanlTh7mYnTuJ3bkTs719mViqpCDcqlenweXtZ7a++y4VLDZe7d+Y35/tzC11vEhNt/HfFYfp+cFqluw4k+fUfRERuXko0JUSNpvBst0R3PHJPzwyczNbTpzHwWLmofbVWDmuG+/f15za3hVKuswSd2DOHACq9+2Lk1f5u2+w8fDhOPv4cDE8nP3ffgtAPV835jzWjs8eCiKgojNn45MZPXcbD3y+kd2n40u4YhERKQ0U6EpYutXGz9tPc9tHaxgxJ4xdp+NxtrfweKearH2pO2/e1VT3TV2WGBXFyT/+AMrmUiX5Ye/qSsvnnwdg9xdfkBgRAWQsc3JbEz/+GtuV54Pr4WxvIfT4Ofp/so4Ji3Zx7lLpX/RSRESKjgJdCdp8/Bx9PlrLsz9u52DkRdwc7RjVvQ7rXurOpNsb4et+/bOOyqPDP/2EkZ6Od8uWVGrcuKTLKTLV+/XDu2VLrElJbLtqqr2TvYVnetbl7+e70r95FQwDfth0km7vrWTWP8dI1zInIlJANpt+bpSkwvr6a9mSEhCflMY7y/bzw6aMveE8Xex5vFNNHu5QAw/n8nOTf2Gypqby8623khwbyy0ffED1224r6ZKK1Ll9+1h2331gGNz67bf4tG6dbbtNR2N57Ze97DubAEA93wq81r8xHetomRMRyZ3NZuPQoUNYLBa8vb1xcHDAZCrfa5iWJoZhkJqaSnR0NFarlbp162YujnxFQfKPAl0xMgyD33ZF8Nove4i+kALAwDaBjO/TAE+X0rmxfGlxbMkSNoSE4Ozry51//FGuZrfmJPSNNzg8bx6e9epx2/z5mO2yn9lqtRn8uPkk7/9xgPOJaQC80Ls+T3errR/OIpKr1NRUzp49S2JiYkmXctNycXHB398/czuyf9M6dKXQ6bgkXlm8m7/3Z6wlV8vblbcHNKX9TbQg8PUyDCNzMkS9gQNvijAH0OyZZzj5++/EHTzIkf/9j7oDB2bbzmI28WC76vRr6s87yw4wN/Qk7/1xgJOxibw5oAn2Ft1ZISLZc3BwoFq1aqSnp2fZ11SKh8Viwc7OrlB++dYIXRGz2gy+XX+c9/88QGKqFXuLiae61eHpbrVxstdK2vkRvX07yx98ELODA3f9/TdOlSqVdEnF5uAPP7Dlrbdw8PCg/2+/4ejpmWef7zYc59Ule7AZGTuJTH+wFe5ON0cIFhEpTwqSf/SrexHacyaeATP+4Y1f95KYaqVNjYr8NrozY4PrKcwVwMHLo3M1+vW7qcIcQJ3778ezXj1S4+PZ+d//5qvPwx1q8OWQ1rg4WFh7KIb7Pt3A6bikIq5URERKkgJdEUhKtTL5t33c8ck/7DwVj5uTHW8PaMq8JzpQ19etpMsrUxIjIzm5fDkA9crpUiW5MdvZERQSAmTM8j2/f3+++vVs6MtPT3bAx82RA5EXGDD9H61ZJyJSjinQFbLVB6PpNW01n685itVm0K+pP3+P7crgdtUwm3WDekEdmjcvY6mSoCAqNWxY0uWUCN+2bal2220YNhtb3n473ztENKnqweKRt9DAz42oCync//kG/t4XWcTViohISVCgKyQxF1N47sdtDP0mlPBzSVTxcOLroa2Z/mArfLSe3HWxpqRweP58oPwuJJxfLceNw+LkRHRYGCd+/z3f/ap4OjN/RAc6161MYqqV4bO3MHvD8aIrVERESoQC3Q0yDIOftoRz64erWbz9DGYTDLulJsvHdqVnQ9+SLq9MO/H776ScO4eLnx8BPXuWdDklytXfn8bDhwOw7f33SS/AEgNuTvZ880gbBrYJxGbAKz/v4c1f92Kzlfr5UCIikk8KdDfgaPRFBn+5iRf/t5O4xDQa+ruz6OlbeKV/I1wdtSLMjciyVMmgQTmuwXYzafjoo7gGBJAUGcmeL78sUF97i5nJdzflhd71Afhq3TGe/n4rSalapkBEpDxQoLvMZrUSGRrK8aVLiQwNxZbLejyp6TY+WXGI2z5ay4ajsTjZmwnp04Alo26heaBn8RVdjkVv3cr5ffuwODpS+957S7qcUsHi6EirF18EYN/MmVw4ebJA/U0mEyO71+HjQS1xsJhZtieCgV9uzFzkWkREyi4NewDhy5cTNnkyiZH/f8O4i68vQSEhBAYHZ2kbduIcIQt3cTDyIgBd6nnz1l1NCKzkUqw1l3cHv/8egBq3356vtdduFgE9euDXsSMR69ez7b336JLPpUz+7Y7mVfD3cGL47C3sCI9jwIx/mPVoG+r4aAa2iEhZddOP0IUvX87aMWOyhDmAxKgo1o4ZQ/jlJTMSktOYtHgX9362gYORF/FydeCjgS349tE2CnOF7NLZs4T/9Rdwcy5VkhuTyUTQ+PGY7Ow4tWIFZ9atu67ztKlRiYVPdaS6lwunzidx94z1bDgSW8jViohIcbmpA53NaiVs8mTIbhmIy8fCpkzhtx2nuPWD1czZeBLDgPuCAvhrbFfubFH1hrbrKMhl3pvJoXnzMKxWfNq0oWL9+iVdTqnjUbs29QYPBmDrlClYU1Ov6zy1vCuw8KmOBFWvSEJyOkO+2cTCracKs1QRESkmN/Ul1+iwsGtG5rIwDBIjIlj3Qgg1K1Wnkbc3Q/u0pG0Lb250J6WCXOYtKjarleiwMJKio3H29sY7KAizpWR3sEhPTubIlaVKHnqoRGspzZo+/TQnli4l4dgxDv7wAw0feeS6zuNVwZHvH2/H8/N3sHTnWcb+tIPwc0mM7lmnUPYWFBGR4nFT7+V6fOlS1l++yfx6OFasiLO3N06VK+Ps7Z3x8PHB+V/Pnby9sXPKug7dlcu814wMXv4HtPPUqUUe6kpDoMzOkYUL2fTyy7hWqUL/33/X7NZcHFm0iE2TJmHn6kr/pUtx9va+7nPZbAbv/nGAz1YfAeCeVgFMvrspDna5D+KXxl8KRETKi4Lkn5s60EWGhvL3o4/m2c69QyfcnR1Iio4mKTqa5JgYbOnp+X4feze3/w94lStzeuXKXNcRc/Hz444//yyyfxhLQ6DMjmEY/H7PPcQdOECL55+n0bBhxV5DWWLYbPwxaBDndu+m1l130f6tt274nD9sOsnLP+/GajPoUMuLzx4OwsM5++Ho0vpLgYhIeaFAl082q5UlwcEkRkVlex+dAbhmE64Mm42U+HiSoqIyAl50NEkxMf8f+KKjSbz8X2vK9S0JYba3x87FBTtnZ+ycnbE4O2Pn5PT//3/5eeb/X2l3uc3Vz6+0s9jb89s995CU06VmkwkXX98iDZQ5idqyhb+GDsXi5MRdf/+t2a35ELNzJ38OGgRAr7lzqdys2Q2fc9WBKEZ+v5VLqVbq+FRg5iPXTvwprb8UiIiUJwp0BZDjP0wAJtMN/cNkGAZpFy5kBr2kmBjOrFnDiaVLb7Dqotdz5kx827Yt1vdc+9xzhC9fTp377qPta68V63uXZRsnTuTo4sVUbNyYls8/T3JMzA1f/tx7JoFhszYTkZBM5QoOfD20TeYai5m/CJXCXwpERMoTBboCyvbSkZ8fQePHF/ooQ34v897y/vt41q1LenIy6UlJWJOTSU9MJD05GWtSUsbxxMSM41eeJyVlOZb52uWHNSkJw2bLV50d332XGv363ejHzbdLZ86wpHdvDJuNvosX41m3brG9d1mXFB3Nz717Y7tqNPhGL39GxCfz6MxNhJ84i1f6RV5o501DFyvR27ZxdOHCPPuXxC8FIiLlSUHyj+44BwKDg6nao0ex3NztHRSEi69vjpd5r4xuBPbqVejvbxgGERs2sPLynqC5SU1IKNT3zsuhH3/EsNnwbddOYa6AYrZvvybMwf+vpZjdKLMtLS3zNoHkf90u8O/7RJOioxkSG4txeTmdhBWwqQB1JUVH38jHEhGRAlCgu8xssRTLaILZYiEoJCTjMq/JlDXUXb7/KGj8+CIJkyaTCd927XIPlJdtefNNwv/8k0bDh+PXoUORLmGRnpTE4f/9D9BSJQWVuZZidi7/+W6YOJHwFStIjonJDGop58/n/01MJtJd3ImyuHDB0Z2qFZ2ocHBHnt1uZNatiIgUjAJdCQgMDqbz1KnZzxAsgsu8/5ZnoDQMfNu1IyosjMjQUCJDQ6nUuDGNhw8noGdPTObCX4v6+NKlpMbH4xoQQJWuXQv9/OVZnmspAumXLnF8yZJrjpvs7HCuXDnrsjve3hnH/vXcqVIlTHZ2fLHmKJ/8vh+TYePlsydxuhiX6yizd1BQIX1KERHJiwJdCSnOy7zZvXdegfLS2bPsnzWLw//7H+f27GHtc8/hXqsWjR57jBr9+mG2v8GVlS8zDIMDc+YAUG/QIN1EX0D5vaxZ7bbbqNKpU5ag5ujpWaCA/mTX2gRUdGHMT9tZUP92Bm/5jpzGbYtqlFlERLJXoEkRkydPZuHChezfvx9nZ2c6duzIO++8Q/08tmdavXo1Y8eOZc+ePVSpUoUXX3yRESNG5LvIop4UcbPKz6KwyefOcWDOHA7+8ANpFy4A4OLvT8NHH6X2Pfdcs2hyQUVu2sTfw4ZhcXZmwIoVOOjPt0DyO8mmMCcohJ04x/DZYfgfDuOOvb/glhiX5fX6Dz9M0PjxhfJeIiI3s4LknwJdP1u9ejUjR45k48aNLF++nPT0dHr16sWlS5dy7HPs2DH69u1L586d2bZtGxMmTGD06NEsWLCgIG8tReDKfYM1+vXDt23bbEdUnCpVovno0dz111+0GDsWJy8vEs+eJeztt/k5OJg9X35J6uWgdz0OfP89ALXuuENh7jpcmWRDTvc4mky4+PkV6uXPoOqVWPR0R5KatmdKj/F83+VpvMa+TK0BAwA4+88/2pdYRKSY3dCyJdHR0fj4+LB69Wq6dOmSbZuXXnqJJUuWsG/fvsxjI0aMYMeOHWzYsCFf76MRutIjPTmZY4sXs/ebb7h0+jQA9hUqUHfQIBo8/DBOXl75PtfFU6f4pU8fDJuNfj//jEedOkVVdrmWuZYiZDvJpqgW+T1/KZUnvwsj9Pg5HCxmvnmgIRFPDiI1IaHYl70RESmPimyE7mrx8fEAVKpUKcc2GzZsoFevXlmO9e7dmy1btpCWlpZtn5SUFBISErI8pHSwc3Ki7sCB9F+6lA5TpuBRuzZpFy+y98sv+Tk4mC1vvcWlM2fyda4rS5X4deyoMHcDrtwT6eLjk+W4i69vke7YUNHVge8eb0uvRr6kWm2MWHCAigMeAGD3Z59plE5EpBhd96QIwzAYO3YsnTp1okmTJjm2i4iIwNfXN8sxX19f0tPTiYmJwd/f/5o+kydP5vXXX7/e0qQYmO3tqdm/PzX69eP0qlXs+eILYnft4uAPP3Dop5+o0bcvjR5/HI/atbP0u3Lf3qXTpzk4bx4A9R98sCQ+QrlSUpNsHO0sfDyoJY/MDGXj0XO8bNTiOTd3Eo4e5eSyZRqlExEpJtc9Qjdq1Ch27tzJ3Llz82x79RpmV67y5rS2WUhICPHx8ZmP8PDw6y1TipjJbCagRw96zZ1Lj6+/xrd9e4z0dI4tWcLSO+9kzbPPErtrF5BxaXBJcDB/P/ooGydNwpqYiMliwZqaWsKfonzIzz2RRcHJ3sKXQ1rTuIo7Z1It/FO7M6BROhGR4nRdge6ZZ55hyZIlrFy5koCAgFzb+vn5ERERkeVYVFQUdnZ2eOVwv5WjoyPu7u5ZHlK6mUwm/Nq3p+fXX9P7xx8JuPVWMAxO/fUXfwwcyG8DBrD2ueeuWTPNsFpZN3Ys4cuXl1DlUhjcnOyZ9Whbqnu5sMyvLSmOLpmjdCIiUvQKFOgMw2DUqFEsXLiQFStWULNmzTz7dOjQgeVX/WP9559/0rp1a+wLaS0zKV28mjaly0cf0e/nn6l5xx1gNhN38GCufcKmTNFoThnn7ebId8Pa4V7JkzU1Mkbpdn36qf5cRUSKQYEC3ciRI5kzZw4//PADbm5uREREEBERQVJSUmabkJAQhgwZkvl8xIgRnDhxgrFjx7Jv3z6++eYbvv76a8aNG1d4n0JKJY86degweTId33kn94aGQWJEBNFhYcVTmBSZal4ufPtoW3Y16EKivTMXjh3j+G+/l3RZIiLlXoEC3aeffkp8fDzdunXD398/8zHv8s3tAGfPnuXkyZOZz2vWrMlvv/3GqlWraNGiBf/5z3/4+OOPueeeewrvU0jpls+VcbSZe/nQqIo70x/vzMY6Gdu4rXr/I6zp6SVclYhI+VagWa75WbJu1qxZ1xzr2rUrW7duLchbSTmS303atZl7+dGulhfnX3qKU8NX4xJzhhnvzuSZCcNLuiwRkXKr8HdaF7lKSexmICXvtjZ1cO1/HwDWn3/gq9WHSrgiEZHyS4FOipzZYiEoJCTjydWh7vJzbeZePt3z4tPYXCrgczGKJV/NY9G2UyVdkohIuaRAJ8WipHYzkJLl4OZG88ceBaDHwb958aftrDwQVcJViYiUPze0l2tx0V6u5ceVnSKKczcDKVmpFy6wpFcvUhMS+LHVIA7WCOL7x9sTVL1iSZcmIlKqFdteriIFVVK7GUjJcXBzo8HQoQDcfnwVKanpDJu1mYORF0q4MhGR8kOBTkSKXL0HH8TB3Z0K587SP+Uw8UlpDPk6lNNxSXl3FhGRPCnQiUiR+/coXc9Df1OvsgsRCck8/PUmzl3SXr4iIjdKgU5EisWVUbpLx4/xbvU4qng4cTT6Eo/O2sylFC08LCJyIxToRKRY/HuU7tTsb/j2kdZUdLFnR3gcI+aEkZpuK+EKRUTKLgU6ESk2V0bpEo4exX7rOmY+2hYXBwtrD8Uwbv4ObLZSP+leRKRUUqATkWLz71G63Z99RrMqbnz2UBB2ZhNLdpzhjV/35muLQRERyUqBTkSK1b9H6U4uW0aXet58cH9zAGatP870lYdLuEIRkbJHgU5EitXVo3Q2q5U7W1Tl1f6NAHj/z4P8sOlkSZYoIlLmKNCJSLG7epQO4NFbavJMjzoATFq8i2W7z5ZkiSIiZYoCnYgUu+xG6QDGBtdjUNtq2AwYPXc764/ElGSZIiJlhgKdiJSI+g89dM0onclk4s27mnBbYz9SrTaemB3G7tPxJVypiEjpp0AnIiXCvkIFGjzyCJB1lM5iNjFtYAva16rExZR0HpkZyvGYSyVYqYhI6adAJyIlpn4299IBONlb+HJIaxpXcSfmYioPf7OJqITkEqxURKR0U6ATkRKT0ygdgJuTPbMebUt1LxfCzyUxdOZm4pPSSqhSEZHSTYFOREpUTqN0AN5ujnw3rB3ebo7sO5vA8NlbSE6z5nAmEZGblwKdiJSo3EbpAKp5ufDto21xc7Qj9Ng5npm7jXSr9n0VEfk3BToRKXG5jdIBNKrizldDW+NgZ2b53kgmLNqlLcJERP5FgU5ESlxeo3QA7Wp58cmglphN8NOWU7z3x4FirlJEpPRSoBORUiHLKN3vv2fbpldjPybf3RSAGauOsCDsVHGWKCJSainQiUipkJ9ROoAH2lRj9OUtwkIW7WLnqbhiqlBEpPRSoBORUiNzlO7YsRxH6QCeu7Uetzb0ITXdxpPfhRF9IaUYqxQRKX0U6ESk1MjvKJ3ZbGLqAy2o7e3K2fhknv4+jNR0zXwVkZuXAp2IlCr5HaVzc7LniyGtcXO0Y/Px87zx655irFJEpHRRoBORUiW/o3QAtb0r8NGgFphMMGfjSeaGniymKkVEShcFOhEpdfI7SgfQo4EvzwfXA+CVn3cTduJccZQoIlKqKNCJSKlTkFE6gJHd69C3qR9pVoMRc7YSEZ9cDFWKiJQeCnQiUioVZJTOZDLx3r3Nqe/rRvSFFJ6cE6Y9X0XkpqJAJyKlUkFH6Vwd7fhiSBAezvbsCI/jlZ93a3swEblpKNCJSKlVkFE6gOpernwy+P+3B/tu44liqFJEpOQp0IlIqVXQUTqAznW9CenTEIA3ftnLxqOxRVmiiEipoEAnIqVaQUfpAB7vXJM7W1Qh3Wbw9PdbOR2XVMRVioiULAU6ESnVrmeUzmQyMeXuZjSu4s65S6k8MXsLSamaJCEi5ZcCnYiUetczSufsYOHzh4Oo5OrAnjMJhCzcqUkSIlJuKdCJSKn371G6XZ9+ytmNGzm+dCmRoaG5jtgFVHRh+uBWWMwmFm8/w1drjxVTxSIixctklIFfWRMSEvDw8CA+Ph53d/eSLkdESkDaxYss6taN9KSs98O5+PoSFBJCYHBwjn2/XX+cV5fswWyCb4e1pXNd76IuV0TkhhUk/2iETkTKhIgNG64JcwCJUVGsHTOG8OXLc+w7pEN17gsKwGbAqB+2cTI2sShLFREpdgp0IlLq2axWwiZPzv7FyxcZwqZMyfHyq8lk4j93NaF5oCfxSWk88d0WLqWkF1W5IiLFToFOREq96LAwEiMjc25gGCRGRBAdFpZjEyd7C58/FIS3myP7Iy7wwv92aJKEiJQbCnQiUuolRUcXSjs/Dyc+e6gV9hYTv+2KYMaqI4VRnohIiVOgE5FSz9k7f5MY8tMuqHol3rizCQDv/3mAFftzGfkTESkjFOhEpNTzDgrCxdcXTKYc27j4+eEdFJSv8w1qW40H21XDMODZuds5En2xsEoVESkRCnQiUuqZLRaCQkIynuQQ6pqPGYPZYsn3OV/t35g2NSpyISWdJ2Zv4UJyWmGUKiJSIhToRKRMCAwOpvPUqbj4+GR9wZzxY+z0ypUFmuTgYGdmxoNB+Lk7cST6EmPmbcdm0yQJESmbtLCwiJQpNquV6LAwkqKjcfb2xmSx8PewYRjp6QRNmED9Bx8s0Pl2hMdx3+cbSE23MbpnXcYG1yuiykVECkYLC4tIuWW2WPBt25Ya/frh27YtPkFBtHz+eQC2vfsuMTt3Fuh8zQM9mTygKQAf/32IZbsjCr1mEZGipkAnImVe/YcfJjA4GFt6OuvGjCElLq5A/e8JCuDRW2oA8PxP2zkYeaHwixQRKUIKdCJS5plMJtq/+SZu1auTGBHB+pdewrDZCnSOCX0b0qGWF5dSrQyfvYX4RE2SEJGyQ4FORMoF+woV6DR1KhZHR86uW8eeL74oWH+LmekPtqKqpzMnYhN55sdtWDVJQkTKCAU6ESk3KtavT5uXXwZg5yefELFhQ4H6V3J14IshQTjZm1lzMJr3/jhQFGWKiBQ6BToRKVdqDRhA7XvuAcPgnxdfzH0P2Gw0ruLBu/c2B+Cz1UdYsuNMUZQpIlKoFOhEpNwJmjABz/r1STl3jn+efx5bWsHuh7ujeRWe7FoLgBf/t4M9Z+KLokwRkUKjQCci5Y6dkxOdp07FvkIFordtY/u0aQU+x4u9G9ClnjfJaTaemB3GuUuphV+oiEghUaATkXLJrXp12r/1FgD7Z80ifPnyAvW3mE38d2BLqnu5cDouiZHfbyXdWrCZsyIixUWBTkTKrcBbb6XBI48AsHHSJC6cOFGg/h4u9nw5pDUuDhY2HI3lrd/2FUGVIiI3ToFORMq1Fs89h3fLlqRdvMjaMWNIT04uUP96vm58eH/GJImZ/xxn8bbTRVGmiMgNUaATkXLNbG/PLR98gGOlSsQdOEDY228X+By3NfHnmR51ABi/cCf7ziYUdpkiIjdEgU5Eyj0XX19uee89MJk4smABRxYtKvA5nru1Hp3rViY5zcZTc8KIT9JOEiJSeijQichNwa99e5qNGgXAlv/8h/P79xeov8Vs4qOBLanq6czx2ETGzd+BTTtJiEgpoUAnIjeNxk88gX/nzlhTUlg7ZgypFy4UqH8lVwdmPNgKB4uZ5Xsj+XT1kSKqVESkYBToROSmYTKb6ThlCi7+/lw8eZJNkyZhGAUbZWse6MnrdzYG4IM/D/DP4ZiiKFVEpEAKHOjWrFlD//79qVKlCiaTicWLF+faftWqVZhMpmse+wt4uUNEpDA4enrS6cMPMdvZEf7XXxyYPbvA5xjYJpD7ggKwGfDM3G2ciUsqgkpFRPKvwIHu0qVLNG/enE8++aRA/Q4cOMDZs2czH3Xr1i3oW4uIFIrKzZrR6qWXANj24YdEb91aoP4mk4n/3NWExlXcOXcplae+30pKurUoShURyZcCB7o+ffrw5ptvcvfddxeon4+PD35+fpkPi8VS0LcWESk0dQcNonqfPhjp6awbN47k2NgC9Xeyt/DZQ0F4ONuzIzyO//y6t4gqFRHJW7HdQ9eyZUv8/f3p2bMnK1euzLVtSkoKCQkJWR4iIoXJZDLR9vXXca9Vi6TISNa/9BI2a8FG2QIruTBtYAtMJpiz8SQLwk4VUbUiIrkr8kDn7+/PF198wYIFC1i4cCH169enZ8+erFmzJsc+kydPxsPDI/MRGBhY1GWKyE3I3tWVTlOnYnF2JmLDBnZ/+mmBz9G9vg+je2TcQjJh0S72nIkv7DJFRPJkMgo6xevfnU0mFi1axF133VWgfv3798dkMrFkyZJsX09JSSElJSXzeUJCAoGBgcTHx+Pu7n695YqIZOvYr7+y4aWXwGSi26efUqVz5wL1t9kMhn27mVUHoqlWyYVfRnXCw8W+iKoVkZtFQkICHh4e+co/JbJsSfv27Tl06FCOrzs6OuLu7p7lISJSVGrefjt1HngADIMN48dz6cyZAvU3m01Me6AFARWdOXkukbE/bdeiwyJSrEok0G3btg1/f/+SeGsRkWwFvfQSlRo3JiUujnXPP481NbVA/T1dHPjsoSAc7Mz8vT+K6SsPF1GlIiLXKnCgu3jxItu3b2f79u0AHDt2jO3bt3Py5EkAQkJCGDJkSGb7adOmsXjxYg4dOsSePXsICQlhwYIFjLq8BY+ISGlgcXSk04cfYu/uTuzOnWz74IMCn6NJVQ/evLMJAB/+dZA1B6MLu0wRkWwVONBt2bKFli1b0rJlSwDGjh1Ly5YteeWVVwA4e/ZsZrgDSE1NZdy4cTRr1ozOnTuzbt06li5dWuBlT0REilqFgAA6vP02AAfnzOHEsmUFPsf9bQIZ1DYQw4Bnf9zGqfOJhV2miMg1bmhSRHEpyE2BIiI3avvUqez96ivsXFy47aefcK9Zs0D9k9Os3PfZBnadjqdZgAc/PdkBJ3utvSkiBVPqJ0WIiJRmzZ55Bp82bUhPTGTtc8+ReuECkaGhHF+6lMjQ0DzXq3Oyt/DpQ63wdLFn56l4Xv9Fiw6LSNHSCJ2ISDaSoqP5/d57SY6JweLkhDU5OfM1F19fgkJCCAwOzvUcaw5GM3RmKIYB797bjPtba01NEck/jdCJiNwgZ29v6g4cCJAlzAEkRkWxdswYwpcvz/UcXep5M+bWegBMWryb3ae16LCIFA0FOhGRbNisVo7Mn5/9i5cvbIRNmZLn5ddR3evQo4EPqek2RswJIy6xYMuhiIjkhwKdiEg2osPCSIyMzLmBYZAYEUF0WFiu5zGbTUy9vwXVKrlw6nwSz83TosMiUvgU6EREspEUnb815PLTzsPFnk8faoWjnZlVB6L5eEXOO+WIiFwPBToRkWw4e3sXarvGVTx4a0BTAD76+xCrDkRdd20iIldToBMRyYZ3UBAuvr5gMuXYxtnXF++goHyf896gAB5sV+3yosPbCT+nRYdFpHAo0ImIZMNssRAUEpLxJIdQ5+LrmzlBIr9e6d+I5oGexCel8dT3YSSn5T6pQkQkPxToRERyEBgcTOepU3Hx8cly3LFSJUwWC7E7d7Lx5ZcxbLZ8n9PRzsKMB1tRydWB3acTePXnPYVdtojchLSwsIhIHmxWK9FhYSRFR+Ps7Y13UBBnVq9m7XPPYVit1B00iNYTJ2LK5fLs1dYdimHIN5uwGTDl7qYMbFutCD+BiJRFWlhYRKQQmS0WfNu2pUa/fvi2bYvZYiGgRw/av/02mEwcmjuXHR99VKBzdqpbmed71QfglSV72HkqrggqF5GbhQKdiMh1qnn77bR55RUA9n75JXu+/LJA/Z/qWptbG/qSmm7jqTlbOX9Jiw6LyPVRoBMRuQF177+fFmPHArBj2jQOzp2b775ms4kP7m9ODS8XTscl8ey87Vi16LCIXAcFOhGRG9Toscdo/MQTAGx5802O/fJLvvt6ONvz6UNBONmbWXMwmo/+OlhUZYpIOaZAJyJSCJqNHk29Bx8EYOPEiYT//Xe++zb0d2fy3RmLDn+84jAr9uey5ZiISDYU6ERECoHJZCJo/Hhq3nknhtXKP88/z9n16/Pdf0DLAIZ0qA7Acz9u52SsFh0WkfxToBMRKSQms5l2b7xBYHAwtrQ01oweTfS2bfnuP6lfI1pW8yQhOZ0Rc7TosIjknwKdiEghMtvZ0fHdd/Hr2BFrUhKrnnqK8/v25auvg52ZGQ+2wsvVgb1nE5i4aDdlYKlQESkFFOhERAqZxcGBLh99hHerVqRduMCKJ54g/ujRfPX193Dmv4NaYjbBgq2n+CH0ZBFXKyLlgQKdiEgRsHNxoeuMGVRs1IiUc+dYOXw4l86cyVffjnUq8+JtDQB4fcletp48X5Slikg5oEAnIlJEHNzc6P7557jXqkViRAR/DxtGUnR0vvo+2aUWtzX2I9VqY8R3YUQmJBdxtSJSlinQiYgUIadKlejx1Ve4BgRwMTycFcOHkxIXl2c/k8nE+/c3p76vG1EXUnjiO02SEJGcKdCJiBQxF19fen71Fc7e3sQfOsTKJ58k7dKlPPtVcLTjyyGt8XSxZ0d4nCZJiEiOFOhERIpBhcBAun/1FY6enpzbvZvVI0eSnpz3ZdRqXi5MH9wKi9nEgq2n+Oaf40VfrIiUOQp0IiLFxLNOHbp/8QV2rq5Ebd7MurFjsaam5tnvljqVmdi3IQBvLd3LukMxRV2qiJQxCnQiIsWoUuPGdPv0UyxOTpxZvZoNEyZgs+Z9b9yjt9TgvqAAbAaM/GErJ2LzvmQrIjcPBToRkWLmExRE52nTMNvZcfL339n8+ut53htnMpl4c0ATWlbzJD4pjeGzt3AxJb2YKhaR0k6BTkSkBFTp3JmO776LyWzmyIIFbHvvvTxDnaOdhc8fCsLX3ZGDkRcZO287NpsmSYiIAp2ISImp1rs3bd94A4D9337L7k8/zbOPj7sTnz/cGgc7M3/ujeSjvw8VdZkiUgYo0ImIlKDaAwYQFBICwK7p09k/e3aefVoEejJ5QFMAPvr7EMt2ny3SGkWk9FOgExEpYfUfeohmzzwDwNZ33uHIggXYrFYiQ0M5vnQpkaGh10ycuCcogMc61QRg7E872B+RUOx1i0jpYTLKwCqVCQkJeHh4EB8fj7u7e0mXIyJS6AzDYPsHH7Bv5kwAHDw8SI2Pz3zdxdeXoJAQAoODM4+lW208MnMz6w7HEFDRmSWjOlHJ1aHYaxeRolGQ/KMROhGRUsBkMtHi+efx69ABIEuYA0iMimLtmDGEL1+eeczOYuaTwS2pVsmFU+eTGPn9VtKstmKtW0RKBwU6EZFSwrDZiD96NIcXMy6mhE2ZkuXyq6eLA18NbY2rg4UNR2N5a+m+4ihVREoZBToRkVIiOiyMpMjInBsYBokREUSHhWU5XM/XjQ8faAHArPXH+WlzeBFWKSKlkQKdiEgpkRQdfd3tejf2Y8yt9QCYtHg3YSfOF2ptIlK6KdCJiJQSzt7eN9TumR51uK2xH6lWGyPmhBERn1yY5YlIKaZAJyJSSngHBeHi6wsmU45tXPz88A4KyvY1s9nEB/c3p76vG9EXUnjyuy0kp+W9T6yIlH0KdCIipYTZYslcZDinUOfTpg1miyXHc7g62vHlkNZ4utiz41Q8ExbuynNLMREp+xToRERKkcDgYDpPnYqLj0+W4/aX16A6/uuvnFqxItdzVPNyYfrgVljMJhZuO83X644VWb0iUjpoYWERkVLIZrVmzHqNjsbZ2xvvoCDC3nqLQ/PmYefsTPD331Oxfv1czzHzn2O8/stezCb4dlhbOtfN3z16IlI6FCT/KNCJiJQRtrQ0Vo4YQeTGjbj4+9P7xx9xrlw5x/aGYfDi/3YyP+wUHs72/DzyFmpUdi3GikXkRminCBGRcshsb0/nDz/ErXp1Es+eZe2zz2JNTc2xvclk4s0BTWhZzZP4pDSGz97CxZT0YqxYRIqLAp2ISBni4OFB1+nTsXd3J2b7dkJffTXXSQ+OdhY+fygIX3dHDkVdZMy87dhspf7CjIgUkAKdiEgZ416zJp0//BCTxcKxJUvY9/XXubb3cXfi84db42BnZvneSKb9faiYKhWR4qJAJyJSBvl16JC5xMn2adPynPnaItCTyQOaAvDx34f4fdfZIq9RRIqPAp2ISBlVb9Ag6g4cCIbB+hdf5Pz+/bm2vycogMc61QTg+fk72Hc2oTjKFJFioEAnIlKGBY0fj2/79qQnJbF61CiSYmJybR/SpwGd6lQmMdXK8NlbOHcp50kVIlJ2KNCJiJRhmTNfa9T4/5mvKSk5trezmPlkcEuqVXLh1PkkRn6/lTSrrRgrFpGioEAnIlLGXT3zddNrr+U689XTxYGvhrbG1cHChqOxvLV0XzFWKyJFQYFORKQccK9RI3Pm6/F8zHyt5+vGhw+0AGDW+uP8tDm8GKoUkaKiQCciUk4UdOZr78Z+jLm1HgCTFu8m7MT5Iq9RRIqGAp2ISDlSb9Ag6g4alO+Zr8/0qMNtjf1ItdoYMSeMiPjkYqpURAqTAp2ISDkTNH48fh06ZMx8HTky15mvZrOJD+5vTn1fN6IvpPDkd1tITrMWY7UiUhgU6EREyhmznR2dPvggY+ZrRESeM19dHe34ckhrPF3s2XEqngkLd+U6qUJESh8FOhGRcqigM1+rebkwfXArLGYTC7edZuryg8VYrYjcKAU6EZFy6uqZr3u/+irX9rfUqcx/7mwCwMcrDvPdhuPFUKWIFAYFOhGRcsyvQwdaT5gAwI6PPiL8779zbT+4XbXMma+vLNnDb9rzVaRMUKATESnn6g4cmDnzdcNLL+U583V0zzo82K4ahgHP/bidDUdii6lSEbleCnQiIjeBa2a+Rkfn2NZkMvHGnU0ylzN5YvYW9p5JKMZqRaSgFOhERG4CV898XZPHzFeL2cS0gS1oV7MSF1LSGTozlPBzicVYsYgUhAKdiMhN4t8zX2N37GDTq6/mOvPVyd7CF0Na08AvY426h7/eRMzFnEOgiJQcBToRkZuIe40adJ46NWPm6y+/5Dnz1cPZnm+HtaWqpzPHYxMZNmszl1LSi6laEckvBToRkZuMX/v2/z/zddq0PGe++ro78d1jbank6sDOU/GMmBNGarqtOEoVkXwqcKBbs2YN/fv3p0qVKphMJhYvXpxnn9WrVxMUFISTkxO1atXis88+u55aRUSkkNQdOJB6gwcDZMx83bcv1/a1vCvwzSNtcLa3sPZQDC/8bwc2m3aTECktChzoLl26RPPmzfnkk0/y1f7YsWP07duXzp07s23bNiZMmMDo0aNZsGBBgYsVEZHC0+qll/Dr2DFj5uuoUSRGRBAZGsrxpUuJDA3FZs26p2uLQE8+fagVdmYTP28/w1u/7dMWYSKlhMm4gb+NJpOJRYsWcdddd+XY5qWXXmLJkiXs+9dvfyNGjGDHjh1s2LAhX++TkJCAh4cH8fHxuLu7X2+5IiJyldT4eP588EESjh3DbG+PLS0t8zUXX1+CQkIIDA7O0mfRtlOMmbcDgJA+DXiya+1irVnkZlGQ/FPk99Bt2LCBXr16ZTnWu3dvtmzZQtq/fnCIiEjxc/DwoO7lS6+2q34mJ0ZFsXbMGMKXL89yfEDLACb2bQjA5N/3syDsVPEUKyI5KvJAFxERga+vb5Zjvr6+pKenExMTk22flJQUEhISsjxERKTw2axW9uU00/XyBZywKVOuufw6vEsthneuCcCLC3ay8kBUkdYpIrkrllmuJpMpy/MrV3mvPn7F5MmT8fDwyHwEBgYWeY0iIjej6LAwEiMjc25gGCRGRBAdFnbNSyF9GjKgZVWsNoOn52xl28nzRVipiOSmyAOdn58fERERWY5FRUVhZ2eHl5dXtn1CQkKIj4/PfISHhxd1mSIiN6XctgDLq53ZbOLde5vRpZ43SWlWhs3azJHoi4VdoojkQ5EHug4dOrD8qvsv/vzzT1q3bo29vX22fRwdHXF3d8/yEBGRwufs7X1D7ewtZj59sBXNAzw4n5jGkK9DiYhPLswSRSQfChzoLl68yPbt29m+fTuQsSzJ9u3bOXnyJJAxujZkyJDM9iNGjODEiROMHTuWffv28c033/D1118zbty4wvkEIiJy3byDgnDx9YUcboEBcPbxwTsoKMfXXR3t+OaRNtSs7MrpuCSGfhNKfJImvYkUpwIHui1bttCyZUtatmwJwNixY2nZsiWvvPIKAGfPns0MdwA1a9bkt99+Y9WqVbRo0YL//Oc/fPzxx9xzzz2F9BFEROR6mS0WgkJCMp7kEOocPD3BlvvOEF4VHJk9rC0+bo4ciLzA8G+3kJxmzbWPiBSeG1qHrrhoHToRkaIVvnw5YZMnZ5kg4eTlReqFC9hSU6l9zz20ff31HCezXbHvbAL3f7aBCynp9G7sy4wHg7CYc+8jItkrSP5RoBMRESBjCZPosDCSoqNx9vbGOyiIs//8w5qRIzFsNpo/9xyNhw/P8zwbj8Yy5JtQUtNtDGpbjbcHNMkzCIrItUrVwsIiIlI2mC0WfNu2pUa/fvi2bYvZYqFqly6Zl2R3TJvGiWXL8jxP+1pefPRAC0wmmBt6kml/HSrq0kVuegp0IiKSq3qDB1P/4YcB2BASQvTlSXG56dPUn//c2QSAj/4+xJyNJ4qyRJGbngKdiIjkqeULL1C1e3dsqamsGTWKi/lYH/Sh9tUZ3bMuAC//vJtlu88WdZkiNy0FOhERyZPZYuGWd9+lYqNGpJw/z6qnniI1Pj7PfmNurcugttUwDBj943Y2Ho0thmpFbj4KdCIiki92Li50nT4dFz8/Eo4dY+1zz2FNTc21j8lk4s27mtCrkS+p6TaGf7uFvWe0P7dIYVOgExGRfHPx8aHrjBnYubgQGRrK5tdfJ6/FEixmEx8PaknbGpW4kJLO0JmhhJ9LLKaKRW4OCnQiIlIgFevXp9OHH2KyWDi6eDF7vvgizz5O9ha+HNqa+r5uRF9IYeg3ocReTCmGakVuDgp0IiJSYFU6d6b1hAkA7Pz4Y44vXZpnHw9ne74d1paqns4cjbnEsFmbuZSSXtSlitwUFOhEROS61B04kAaPPALAxokTiQoLy7OPn4cT3w5rS0UXe3aciuep77eSZs19WzERyZsCnYiIXLcWY8cS0LMntrQ01o4ezYUTea83V8enAt880gZnewtrDkbz4v92YrOV+k2LREo1BToREbluZouFjlOmUKlxY1Li4lj19NOkxMXl2a9ltYrMeKgVFrOJRdtO8/Zv+/KcXCEiOVOgExGRG5K5nIm/PxeOH2fts8/muZwJQPf6Prx7TzMAvlp3jLeWKtSJXC8FOhERuWHO3t50+/RT7CtUIGrLFja9+mq+wtk9QQG8fkdjICPUTVq8W5dfRa6DAp2IiBQKz7p1M5czOb5kCbs//TRf/YZ2rMG79zTDZILvN51k3PwdpGuihEiBKNCJiEih8b/lFtpMmgTArunTOfbLL/nqd3+bQKY90AKL2cTCbacZ/eM2UtMV6kTyS4FOREQKVZ3776fho48CsOnll/O1nAnAnS2qMuPBVjhYzPy2K4IRc8JITrMWZaki5YYCnYiIFLoWY8cSGByMLS2NNc88Q0I+ljMB6N3Yjy+HtsbRzsyK/VFafFgknxToRESk0JnMZjpMnoxX06akxsezasSIfC1nAtC1njffDmuLq4OF9UdiGfJNKAnJaUVbsEgZp0AnIiJFws7ZmS6ffIJrlSpcPHmSNc88k6/lTADa1/Liu8fb4e5kR9iJ8zz45SbOX8pfX5GbkQKdiIgUGefKlel6eTmT6K1b2ThpUr7XmmtVrSJzn2hPJVcHdp2OZ+AXG4m6kFzEFYuUTQp0IiJSpDzr1KHztGmY7Ow4sXQpu6ZPz3ffxlU8+OnJ9vi4OXIg8gIPfL6RM3FJRVitSNmkQCciIkXOr0MH2rz8MgC7P/2Uoz//nO++dXzcmD+iA1U9nTkWc4n7PtvAidhLRVWqSJmkQCciIsWizr330ujxxwEIfeUVIjdvxma1EhkayvGlS4kMDcVmzX6Zkupervw0ogM1vFw4HZfE/Z9v4HDUxeIsX6RUMxllYOO8hIQEPDw8iI+Px93dvaTLERGR62TYbPwzbhwn//gDO2dn7FxdSY6JyXzdxdeXoJAQAoODs+0flZDMQ19v4mDkRbxcHfjusXY0qqJ/F6R8Kkj+0QidiIgUG5PZTPu336ZC9eqkJyVlCXMAiVFRrB0zhvDly7Pt7+PuxI9PdKBJVXdiL6Uy8IsNbA+PK4bKRUo3BToRESlWZnt7rImJ2b94+aJR2JQpOV5+reTqwPePt6dVNU8SktN56KtNbDoaW1TlipQJCnQiIlKsosPCSIqOzrmBYZAYEUF0LluGeTjb891j7ehQy4uLKekMnRnKmoO5nFOknFOgExGRYpVrmCtAO1dHO2Y+2obu9b1JTrPx+Ldb+HNPRGGUKFLmKNCJiEixcvb2LrR2TvYWPn+4NX2a+JFqtfHU91tZsuPMjZYoUuYo0ImISLHyDgrCxdcXTKYc2zhVrox3UFC+zudgZ+a/g1oyoGVVrDaDZ3/cxk9bwgurXJEyQYFORESKldliISgkJONJDqEu7eJFojZvzvc57SxmPrivOYPaVsMw4MX/7WT2huOFUK1I2aBAJyIixS4wOJjOU6fi4uOT5bizjw/utWphTU5m5ZNPFmhHCbPZxNsDmjDslpoAvPLzHj5bfaRQ6xYprbSwsIiIlBib1Zo569XZ2xvvoCCM9HQ2TpzIid9/B6DpyJE0eeopTLlcov03wzD4cPlB/rviMACje9ZlzK11891fpLQoSP5RoBMRkVLHsNnYMW0ae7/+GoBad91F29dew2xvn+9zTF95mPf+OADA8M41mdC3oUKdlCnaKUJERMo0k9lMi7FjafPKK5jMZo4uXsyqp58m7WL+928d2b0Or/ZvBMCXa48xafFubLZSP4Yhcl0U6EREpNSq+8ADdPnkE+ycnYlYv57lDz9MYkT+15p79JaaTLm7KSYTfL/pJOP+t4N0q60IKxYpGQp0IiJSqlXt2pVbv/0Wp8qViTt4kD8GD+b8/v357j+wbTWmPdACi9nEwq2nefbH7aSmK9RJ+aJAJyIipV6lxo3pPXcuHrVrkxQZyfIhQzj7zz/57n9ni6pMH9wKe4uJpbvOMmJOGMlp2e8VK1IWKdCJiEiZ4FqlCsFz5uDTpg3ply6x6qmnOLJwYb7739bEjy+HtMbRzsyK/VEMm7WZ+KS0IqxYpPgo0ImISJnh4O5O9y++oMbtt2NYrWx6+WV2/ve/5HfBhm71fZj1aFtcHCysPxLLgOn/cCQ6/xMtREorBToRESlTLA4OdJgyhcZPPgnA7s8+Y8OECVhTU/PVv0NtL356sgNVPJw4GnOJu6b/w6oDUUVZskiRU6ATEZEyx2Qy0Xz0aNq98QYmi4XjS5aw6sknSU1IyFf/JlU9+HlUJ4KqV+RCcjrDZm3mq7VH8z3SJ1LaKNCJiEiZVfuee+g6YwZ2Li5Ehoay/OGHuXTmTL76ers58sPwdtzfOgCbAW8u3ce4+Ts1WULKJAU6EREp06p06kTw7Nk4+/gQf/gwfwwaxLm9e/PV19HOwjv3NOPV/o0wm2DB1lMM+nIjUQnJRVy1SOFSoBMRkTKvYsOG9PrhBzzr1SM5Joa/hgzh9Jo1+eprMpl49JaafDusLe5Odmw7Gccdn/zDrlPxRVy1SOFRoBMRkXLB1d+fW2fPxq9DB9KTklgzahSHf/op3/071/Xm51GdqO3tSkRCMvd+tp4lO/J3+VakpCnQiYhIueHg5kbXGTOoddddGFYroa+/zvZp0zBs+dsZomZlVxaNvIXu9b1JSbcxeu423vtjv/aAlVJPgU5ERMoVi4MD7d58k6YjRwKw98svWf/SS/le1sTdyZ6vhrbhya61AJi+8ghPfBfGxZT0IqtZ5EYp0ImISLljMplo+vTTtH/rLUx2dpz47TdWDh9OSlwcNquVyNBQji9dSmRoKDbrtbNaLWYTIX0aMvWB5jjYmflrXyR3z/iHk7GJJfBpRPJmMsrAojsJCQl4eHgQHx+Pu7t7SZcjIiJlSMSGDax97jnSLl7E2dcXw2olOSYm83UXX1+CQkIIDA7Otv/28DiemL2FqAspeLrYM+PBVnSsXbm4ypebWEHyj0boRESkXPPr0IHg777DwdOTpMjILGEOIDEqirVjxhC+fHm2/VsEerJkVCeaBXgQl5jGw1+H8t2G48VQuUj+KdCJiEi55167NmZ7++xfvHyhKmzKlGwvvwL4eTjx05MduLNFFaw2g5d/3sPERbtIs+ZvsoVIUVOgExGRci86LIzk6OicGxgGiRERRIeF5djEyd7CtAda8NJtDTCZ4PtNJ3noq02cu5S/yRYiRUmBTkREyr2k3MJcAdqZTCae6labr4a0poKjHZuOneOOT9ax72z+9pAVKSoKdCIiUu45e3sXarueDX1Z9HRHqnu5cOp8Evd8up4/9kTcSIkiN0SBTkREyj3voCBcfH3BZMq13amVK/O9Xl1dXzd+HnkLt9TxIjHVypPfhfHfvw9RBhaPkHJIgU5ERMo9s8VCUEhIxpOrQ92/nh+YPZu/hg7l4unT+Tqvp4sDsx5tyyMdawDwwfKDjJq7jaTU7CdXiBQVBToREbkpBAYH03nqVFx8fLIcd/H1pfO0aXT++GPs3d2J3bmT3++5J8dlTK5mbzHz2h2NmXx3U+wtJpbuPMu9n63nTFxSUXwMkWxpYWEREbmp2KxWosPCSIqOxtnbG++gIMwWCwCXzpxh3bhxxO7YAUC9wYNp+cILWBwc8nXu0GPneGpOGLGXUqlcwZHPH25FUPVKRfZZpHwrSP5RoBMREfkXW1oaOz7+mH3ffANAxUaN6PT++7hVr56v/qfOJ/L4t1vYH3EBB4uZNwc04f7WgUVZspRT2ilCRETkOpnt7Wn5/PN0++wzHCtW5Pzevfx+330c/+23fPUPqOjCgqc60qeJH6lWGy/+byf/+XUv6VqEWIqQAp2IiEg2qnTuTJ8FC/AOCiL90iXWv/ACoa+9Rnpycp59XR3tmD64Fc/dWheAr9cd49FZm4lPTCvqsuUmpUAnIiKSAxdfX3p+8w1NRowAk4nD8+fzx8CBxB85kmdfs9nEc7fW49MHW+Fsb2HtoRj6f7KOsBPni6Fyudko0ImIiOTCbGdHs2eeoceXX+Lk5UX8oUMse+ABji5enK/+fZr687+nOlDV05mT5xK577P1fPjnAe0DK4XqugLdjBkzqFmzJk5OTgQFBbF27doc265atQqTyXTNY//+/dddtIiISHHz69CBPgsW4Nu+PdakJDZOnMiGCRNIu3Qpz76Nq3jw+3OdGdCyKjYDPl5xmHs/Xc/R6IvFULncDAoc6ObNm8dzzz3HxIkT2bZtG507d6ZPnz6cPHky134HDhzg7NmzmY+6deted9EiIiIlwdnbm+5ffEGz0aMxmc0c+/ln/hg4kLiDB/Ps6+5kz9QHWvDfQS1xd7Jjx6l4+n28jjkbT2h3CblhBV62pF27drRq1YpPP/0081jDhg256667mDx58jXtV61aRffu3Tl//jyenp7XVaSWLRERkdImassW/nnhBZKiorA4OhIUEkLte+/FlMf2YgBn45MYN38H/xyOBaBHAx/euacZ3m6ORV22lCFFtmxJamoqYWFh9OrVK8vxXr16sX79+lz7tmzZEn9/f3r27MnKlSsL8rYiIiKljk/r1vRZsAD/zp2xpqQQ+tprrH/hBdIu5n0Z1d/Dme+GtWNSv4Y42JlZsT+K26atYfneyGKoXMqjAgW6mJgYrFYrvr6+WY77+voSERGRbR9/f3+++OILFixYwMKFC6lfvz49e/ZkzZo1Ob5PSkoKCQkJWR4iIiKljVOlSnSbMYMWzz+Pyc6OE7//zu/33ce5vXvz7Gs2m3i8cy1+GdWJBn5uxF5KZfjsLYQs3MmllPRiqF7Kk+uaFHH1cLJhGDkOMdevX5/hw4fTqlUrOnTowIwZM+jXrx/vv/9+juefPHkyHh4emY/AQK2wLSIipZPJbKbRsGHc+u23uPj7c/HkSf4cPJgD33+feW+czWolMjSU40uXEhkais1qzexf38+Nn0fdwhNdamEywdzQcPp9vJatJ7W8ieRfge6hS01NxcXFhfnz5zNgwIDM488++yzbt29n9erV+TrPW2+9xZw5c9i3b1+2r6ekpJCSkpL5PCEhgcDAQN1DJyIipVpKXBybXnmFU3//DUDArbcS2KMHOz76iMTI/7+c6uLrS1BICIHBwVn6bzgSy/M/bedMfDIWs4lR3eswqkcd7C1aZexmVGT30Dk4OBAUFMTy5cuzHF++fDkdO3bM93m2bduGv79/jq87Ojri7u6e5SEiIlLaOXp60vmjjwgKCcFsZ8epv/5iw4QJWcIcQGJUFGvHjCH8qn9PO9T24vfnunBniypYbQYf/X2Iez/bwLGYvJdGkZtbgSP/2LFj+eqrr/jmm2/Yt28fY8aM4eTJk4wYMQKAkJAQhgwZktl+2rRpLF68mEOHDrFnzx5CQkJYsGABo0aNKrxPISIiUkqYTCbqP/QQt373HSaLJftGly+OhU2ZkuXyK4CHsz0fDWzJx1eWNwmPo+9Ha/lh00ktbyI5sitohwceeIDY2FjeeOMNzp49S5MmTfjtt9+oXr06AGfPns2yJl1qairjxo3j9OnTODs707hxY5YuXUrfvn0L71OIiIiUMtbkZIyrwloWhkFiRATRYWH4tm17zct3NK9C6+oVGTd/B+uPxDJh0S7+3hfJFC1vItko8Dp0JUHr0ImISFlzfOlS1r/4Yp7tOr77LjX69cvxdZvN4Jt/jvHusgOkWm14uTrwzj3NuLWRb459pHwosnvoREREJH+cvb0Lpd2V5U1+HnVL5vImj8/eQsjCXSSmankTyaBAJyIiUgS8g4Jw8fWFPHaO2D97NhdPn87zfA393Vk88haGd64JwNzQk/T9aC3btLyJoEAnIiJSJMwWC0EhIRlPcgp1ZjOnV65k6R13sPvzz7GmpuZ6Tid7CxP7NeKHx9vh7+HE8dhE7v1sA9P+Oki61VbIn0DKEgU6ERGRIhIYHEznqVNx8fHJctzFz4/O06bRd9EifNq0wZqczM6PP+a3AQM4m8dWmgAd61Rm2bNduKN5xvIm0/7S8iY3O02KEBERKWI2q5XosDCSoqNx9vbGOygI8+UlTQzD4MTSpWx97z2SY2IAqNa7N61efBEXP788z/3z9tNMWrybC8npONtbeKV/Iwa2CcxxBycpOwqSfxToRERESoHUCxfYNX06B3/4AcNqxc7ZmSZPP039hx7C4uCQa9/TcUk8/9N2Nh49B8CtDX2Zck9TKlfQ8iZlmQKdiIhIGXV+/362vPkm0du2AeBeqxZtJk3Ct127XPvZbAZfrzvGe39kLG9SuULG8iY9G2p5k7JKgU5ERKQMM2w2ji1ZwrYPPiDlXMaoW/W+fWn14ot5LnOy72wCz/24nQORFwC4u2VVXritPv4ezkVetxQuBToREZFyIDU+nh3//S+H583DsNmwc3Wl2ahR1Bs8GLNdzps9JadZef+PA3y17hgATvZmnuxSmye71sLFocCbREkJUaATEREpR87t3cvm//yH2J07AfCsV4/WkybhExSUa78d4XG8uXQvm49nrFXn4+bIC73rc0+rAMxmTZoo7RToREREyhnDZuPIwoXsmDqVlLg4AGrecQctnn8e58qVc+5nGCzbHcHk3/dz8lwiAI2ruDOpXyM61PYqjtLlOinQiYiIlFMpcXHsmDaNw//7HxgG9m5uNHvmGeoOHJi5FEq2/dKtfLv+OP/9+zAXUjK2DOvVyJeQvg2pWdm1uMqXAlCgExERKedidu5ky5tvcm7PHgAqNmxIm5dfpnLz5kDOa9/FXkzho78P8f2mk1htBnZmE0M61GB0zzp4uuS+PIoULwU6ERGRm4DNauXI/Pls/+gj0hISAKh9zz1UbtWKXR9/TGJkZGZbF19fgkJCCAwOBuBw1AXeWrqPlQeiAfBwtufZnnV5uEN17C3aSKo0UKATERG5iSSfO8f2qVM5unBhzo0u7xzReerUzFAHsOZgNG8t3Ze5zEmtyq6E9G3IrQ19tNtECVOgExERuQlFhYXx97BhGOnp2TcwmXDx9eWOP//Mcr9dutXG/LBTfPDnAWIupgLQoZYXE/s1pElVj+IoXbJRkPyjMVUREZFywrBacw5zAIZBYkQE0WFhWQ7bWcwMaluNleO68XS32jjYmdlwNJb+n6zjhfk7iExILuLK5UYp0ImIiJQTSdHRN9TOzcmeF29rwIrnu3JH8yoYBswPO0X391fx8d+HSEq1Fma5UogU6ERERMqJvLYFu+LgDz9wbt++HF8PqOjCx4NasvDpjrSq5kliqpUPlx+k+/urWLj1FDZbqb9b66aje+hERETKCZvVypLgYBKjoiAf/7wH3HorTZ9+mor16+fYxjAMft15lim/7+d0XBIATat6MKlfQ9rV0sLERUmTIkRERG5S4cuXs3bMmIwn//4n/vKM1aCQEGJ37uT40qWZrwf26kXTp5/Gs27dHM+bnGZl5j/Hmb7yMBcvL0x8W2M/Qvo2oLqXFiYuCgp0IiIiN7Hw5csJmzw56zp0fn4EjR+fuWRJ/JEj7P70U04sW5YR7Ewmqt92G02eegqP2rVzPHfMxRSmLj/I3NCT2Aywt5h4pGMNRvWoi4ezfZF/tpuJAp2IiMhNLqedIq4Wd+gQu2bMIPzPPzMOmEzU6NePJk89hXuNGjme/0DEBd5cupe1h2IAqOhiz3O31mNwu2pamLiQKNCJiIhIgZw/cIBdM2Zw6q+/ADCZzdS4/XaajBiBW/XqOfZbdSCKt5bu41DURQBqebsyoktt7mhRBSf7nPeWlbwp0ImIiMh1ObdvH7umT+f0ypUAmCwWat5xB02efJIKgYHZ9km32pi7OZypyw9y7lLGwsQVXewZ1LYaD7WvThVP52KrvzxRoBMREZEbErt7N7umT+fMmjUAmOzsqHXnnTQZMQLXKlWy7ZOQnMbcTSeZveFE5oxYi9lE78a+PNKxJm1qVNR2YgWgQCciIiKFImbnTnZ98gln//kHALOdHbXuvpvGTzyBq79/Zrt/37Pn4FWZHS4BzN4YzoajsZltGvm780jHGrocm08KdCIiIlKoordtY9f06URs2ACA2d6e2vfeS+Phw4ndufPaWbW+vgSFhHCpaTu+XX+cRdtOk5xmA3Q5Nr8U6ERERKRIRIWFseuTT4gMDQUyLsVmu3/s5UurnadOJTA4mLjEVOZtDtfl2AJQoBMREZEiFblpEzs++YSYrVtzbmQy4eLryx1//pm5ZEq61cZf+6L4dv1xXY7NgwKdiIiIFLmITZtYMWxYnu16zpyJb9u21xzfH5Ggy7G5KEj+0cp/IiIicl2SY2Ly1S7+8OFsjzfwc2fy3c3YGNKTkD4NqOrpzPnENGasOkLnd1fy9PdhhB47RxkYeypxGqETERGR6xIZGsrfjz6ad0OTiSqdO1P77rup0rUrFgeHbJvpcmxWuuQqIiIiRc5mtbIkOJjEqKiM/WCzYba3x5aWlvncsVIlavbvT62778azTp0cz63LsQp0IiIiUkzCly9n7ZgxGU/+HSn+NcvVo25dji5axLGffyYpOjqziVfTptS++26q9emDg5tbtue/mWfHKtCJiIhIsQlfvvzadej8/AgaP57A4ODMY7b0dM6uW8eRRYs4vWpV5nInFicnAnv1ovbdd+PTunW2AS23y7EPd6jObY39qOia/aXcskqBTkRERIrVv3eKcPb2xjsoKHOpkuwkxcRw/NdfObJgAQlHj2YerxAYSK0BA6h11124+Ppm2ze7y7EWs4l2NSvRp4kfvRr74evuVLgfsAQo0ImIiEiZYBgGsTt3cmThQk78/jvply4BYDKb8bvlFmrffTdVu3XLdiLF+QtJLPjxD7buPMq+JDuOe9XEMGUs4BFUvSK3NfbjtiZ+BFZyKdbPVFgU6ERERKTMSU9M5OSff3J00SKitmzJPO5YsSI1br+d2vfcg2fdukD2l3ltnl5sbH8fv9rXynLexlXcM8NdXd/s79UrjRToREREpExLOHEiYyLF4sVZJlJUatKEig0bcmT+/Gs7Xb73rslb77LdpxHLdkew8Wgstn8lndrertzWxI/bGvvTpKp7qZ5QoUAnIiIi5YItPZ2z//zD0YULOfWviRQ5umq7sXOXUvlrbyS/7z7LusMxpFn/P/ZU9XTOCHdN/AiqVhGzuXSFOwU6ERERKXeSY2PZOX06h+fNy7NtdtuNJSSnsXJ/FMt2R7DqQDRJadbM17zdHOnVyJc+TfxpV6sS9paS30xLgU5ERETKpeNLl7L+xRfzbFela1fqDR6MT6tW2LlcOykiKdXK6oPR/LEngr/2RXIh+f9H/jyc7bm1oS99mvjRqW7lEtudQoFOREREyqV8bzd2mdnensotWuDXoQN+HTpQqVEjzHZ2WdqkpttYfySGP/ZE8OeeSGIvpWa+5upgoXsDH25r4ke3+j5UcLS7+i2KjAKdiIiIlEv52W7MwcODqj16ELlhA4kREVles3dzw7dNG3zbt8evQwfca9bMMjHCajPYfPwcy3ZH8MeeCM7GJ///ee3MdKnrzW1N/Li1oQ+eLkW7kLECnYiIiJRb+dluLDA4GMMwuHDyJJEbNhCxYQMRoaGkJSRkOZezry9+l8OdX/v2OHt7Z75msxnsPB3Pst0RLNt9luOxiZgMGzVij+GZeoHXh3ShSfeOuS6gfCMU6ERERKRcy+92Y/9ms1o5v3cvERs3ErFhA9Fbt2JLS8vSxqNOncyA59OmDfaurkDGAsgb//cLh6e9jyXu/7cec/H1JSgkJMf3vBEKdCIiIlLuFXS7saulJyURvW1bxujdxo2c37cvy4ifyc6Oyk2b4tuhA2aLhZ2ffHLtZd6rRgULkwKdiIiISAElnz9PVGhoZsC7GB6ev45XrX1XWAqSf4pvqoaIiIhIKeZUsSLVevemWu/eAFwMDydi40aOL11K1ObNOXc0DBIjIogOC7tm7bviokAnIiIiko0KgYHUCQzEzsUl90B32b+3KCtuJb8MsoiIiEgp9u+Zr4XRrigo0ImIiIjkwjsoCBdf38wJENcwmXDx88M7KKh4C/sXBToRERGRXJgtFoJCQjKeXB3qLj8PGj++yNajyw8FOhEREZE8BAYH03nqVFx8fLIcd/H1LZIlSwpKkyJERERE8iEwOJiqPXrc0Np3RUWBTkRERCSfzBZLiS1NkhtdchUREREp4xToRERERMo4BToRERGRMk6BTkRERKSMU6ATERERKeMU6ERERETKOAU6ERERkTJOgU5ERESkjFOgExERESnjysROEYZhAJCQkFDClYiIiIgUjyu550oOyk2ZCHQXLlwAIDAwsIQrERERESleFy5cwMPDI9c2JiM/sa+E2Ww2zpw5g5ubGyaTqaTLKXRt2rRh8+bNJV1GoSqNn6kkairq9yzs8xfG+W70HAXtn5CQQGBgIOHh4bi7u1/3+0reSuPf6xtVWj9Teft5VRp/Vt3oea6nb0F/XhmGwYULF6hSpQpmc+53yZWJETqz2UxAQEBJl1FkLBZLufuHqDR+ppKoqajfs7DPXxjnu9FzXG9/d3f3Uvc9V96Uxr/XN6q0fqby9vOqNP6sutHz3Ejfgvy8ymtk7gpNiigFRo4cWdIlFLrS+JlKoqaifs/CPn9hnO9Gz1Eav3ckQ3n8symtn6m8/bwqjT+rbvQ8pe17p0xcchURyUlCQgIeHh7Ex8eXypEWEZErivLnlUboRKRMc3R05NVXX8XR0bGkSxERyVVR/rzSCJ2IiIhIGacROhEREZEyToFOREREpIxToBMREREp4xToRERERMo4BToRKbfCw8Pp1q0bjRo1olmzZsyfP7+kSxIRucaFCxdo06YNLVq0oGnTpnz55ZcFPodmuYpIuXX27FkiIyNp0aIFUVFRtGrVigMHDuDq6lrSpYmIZLJaraSkpODi4kJiYiJNmjRh8+bNeHl55fscZWLrLxGR6+Hv74+/vz8APj4+VKpUiXPnzinQiUipYrFYcHFxASA5ORmr1UpBx9t0yVVESq01a9bQv39/qlSpgslkYvHixde0mTFjBjVr1sTJyYmgoCDWrl2b7bm2bNmCzWYjMDCwiKsWkZtNYfysiouLo3nz5gQEBPDiiy9SuXLlAtWgQCcipdalS5do3rw5n3zySbavz5s3j+eee46JEyeybds2OnfuTJ8+fTh58mSWdrGxsQwZMoQvvviiOMoWkZtMYfys8vT0ZMeOHRw7dowffviByMjIAtWge+hEpEwwmUwsWrSIu+66K/NYu3btaNWqFZ9++mnmsYYNG3LXXXcxefJkAFJSUggODmb48OE8/PDDxV22iNxkrvdn1b899dRT9OjRg/vuuy/f76sROhEpk1JTUwkLC6NXr15Zjvfq1Yv169cDYBgGjzzyCD169FCYE5ESkZ+fVZGRkSQkJACQkJDAmjVrqF+/foHeR5MiRKRMiomJwWq14uvrm+W4r68vERERAPzzzz/MmzePZs2aZd7T8t1339G0adPiLldEblL5+Vl16tQpHnvsMQzDwDAMRo0aRbNmzQr0Pgp0IlKmmUymLM8Nw8g81qlTJ2w2W0mUJSKSRW4/q4KCgti+ffsNnV+XXEWkTKpcuTIWiyXzN9wroqKirvlNWESkpBTXzyoFOhEpkxwcHAgKCmL58uVZji9fvpyOHTuWUFUiIlkV188qXXIVkVLr4sWLHD58OPP5sWPH2L59O5UqVaJatWqMHTuWhx9+mNatW9OhQwe++OILTp48yYgRI0qwahG52ZSGn1VatkRESq1Vq1bRvXv3a44PHTqUWbNmARmLdb777rucPXuWJk2aMHXqVLp06VLMlYrIzaw0/KxSoBMREREp43QPnYiIiEgZp0AnIiIiUsYp0ImIiIiUcQp0IiIiImWcAp2IiIhIGadAJyIiIlLGKdCJiIiIlHEKdCIiIiJlnAKdiIiISBmnQCciIiJSxinQiYjkYt26ddSvX5++ffuWdCkiIjlSoBMRycXo0aMZPXo0x48fL+lSRERypEAnIpKDAwcOEBkZiZ+fH02bNi3pckREcqRAJyKSg2XLlhEcHMyyZct0yVVESjUFOhGRHKxdu5ZWrVqxbt067rvvvpIuR0QkR3YlXYCISGm1e/duKlWqxMCBA3FxcSnpckREcmQyDMMo6SJEREqjChUqULduXdasWYObm1tJlyMikiNdchURyYFhGLz00ksKcyJS6inQiYhkY8mSJSQmJlKjRg127NjBihUrSrokEZEc6R46EZGrpKSkMGvWLL7++muGDRtGtWrVmDVrVkmXJSKSI91DJyIiIlLG6ZKriIiISBmnQCciIiJSxinQiYiIiJRxCnQiIiIiZZwCnYiIiEgZp0AnIiIiUsYp0ImIiIiUcQp0IiIiImWcAp2IiIhIGadAJyIiIlLGKdCJiIiIlHEKdCIiIiJl3P8BqwwaMWviJkEAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "my_lensing = model.likelihood[\"soliket.LensingLikelihood\"]\n", + "my_lensing = resolve_aliases(model).lensing\n", "\n", "# Plotting data against smooth theory\n", "plt.title(\"Lensing spectra comparison\")\n", @@ -1288,7 +1019,7 @@ ], "metadata": { "kernelspec": { - "display_name": "SO_namaster", + "display_name": "soliket", "language": "python", "name": "python3" }, @@ -1302,7 +1033,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.13" + "version": "3.12.13" } }, "nbformat": 4, diff --git a/notebooks/quickstart.ipynb b/notebooks/quickstart.ipynb new file mode 100644 index 00000000..65a54313 --- /dev/null +++ b/notebooks/quickstart.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cae3fa1a", + "metadata": {}, + "source": [ + "# SOLikeT in a few lines\n", + "\n", + "SOLikeT provides cosmology likelihoods for the [Cobaya](https://cobaya.readthedocs.io) sampler.\n", + "This notebook is the **30-second on-ramp**: it gets you a working likelihood you can poke at,\n", + "using the `soliket.presets` helpers.\n", + "\n", + "> For the full, hand-built configuration (parameter blocks, theory wiring, YAML, running MCMC)\n", + "> see [`first_step_tutorial.ipynb`](first_step_tutorial.ipynb) — the detailed deep-dive." + ] + }, + { + "cell_type": "markdown", + "id": "0e6914de", + "metadata": {}, + "source": [ + "## 1. A working likelihood, instantly\n", + "\n", + "`quickstart` wires a preset's likelihood + theory + fiducial parameters into a Cobaya model and\n", + "returns a `Session`. Available presets: `mflike`, `lensing`, `multigaussian`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13f06aff", + "metadata": {}, + "outputs": [], + "source": [ + "from soliket.presets import quickstart\n", + "\n", + "s = quickstart(\"mflike\") # MFLike TT/TE/EE + foreground + CAMB, at the fiducial point\n", + "s.loglike() # evaluate the log-likelihood" + ] + }, + { + "cell_type": "markdown", + "id": "ca35427f", + "metadata": {}, + "source": [ + "## 2. Reach the pieces by name\n", + "\n", + "No fragile `model.components[3]` indexing — the `Session` exposes role aliases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47b3d186", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"likelihood :\", type(s.mflike).__name__)\n", + "print(\"foreground :\", type(s.foreground).__name__)\n", + "print(\"cosmology :\", type(s.cosmo).__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "39f36d24", + "metadata": {}, + "source": [ + "## 3. Plot the theory spectrum\n", + "\n", + "`notebooks/_nb.py` holds notebook-only conveniences (plotting, data download)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4c963a0", + "metadata": {}, + "outputs": [], + "source": [ + "import _nb\n", + "\n", + "dls = _nb.theory_dls(s) # CMB D_ell from the evaluated model\n", + "_nb.plot_dls(dls, spectra=(\"tt\", \"te\", \"ee\"));" + ] + }, + { + "cell_type": "markdown", + "id": "d22072e3", + "metadata": {}, + "source": [ + "## 4. Vary a parameter\n", + "\n", + "Parameters are *fixed at fiducial* by default. Pass `sample=[...]` to turn a parameter into a\n", + "sampled one (it gets its prior back), then run an MCMC with `s.run()` (Cobaya, Python-native)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5070de7", + "metadata": {}, + "outputs": [], + "source": [ + "s_tau = quickstart(\"mflike\", sample=[\"tau\"])\n", + "print(\"tau is now sampled:\", \"prior\" in s_tau.info[\"params\"][\"tau\"])\n", + "\n", + "# s_tau.run(output=\"chains/mflike\") # uncomment to launch a full MCMC" + ] + }, + { + "cell_type": "markdown", + "id": "cca0a240", + "metadata": {}, + "source": [ + "## Where to go next\n", + "\n", + "- **[`first_step_tutorial.ipynb`](first_step_tutorial.ipynb)** — build the same configuration by hand\n", + " to understand every block.\n", + "- **`soliket.presets`** — `load_params`, `build_info`, `quickstart`, `resolve_aliases`.\n", + "- **`ISO_sims/`** — create simulated datasets, build cross-covariances, and run analyses." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "soliket", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/soliket/ccl_tracers/ccl_tracers.py b/soliket/ccl_tracers/ccl_tracers.py index 7530a168..b464c7ba 100644 --- a/soliket/ccl_tracers/ccl_tracers.py +++ b/soliket/ccl_tracers/ccl_tracers.py @@ -47,11 +47,8 @@ def _get_nz( ) -> np.ndarray: if self.z_nuisance_mode == "deltaz": bias = params_values[f"{tracer_name}_deltaz"] - nz_biased = tracer.get_dndz(z - bias) - - # nz_biased /= np.trapezoid(nz_biased, z) - - return nz_biased + return tracer.get_dndz(z - bias) + raise ValueError(f"Unknown z_nuisance_mode {self.z_nuisance_mode!r}") def _get_ia_bias( self, @@ -65,7 +62,13 @@ def _get_ia_bias( elif self.ia_mode == "nla": A_IA = params_values["A_IA"] eta_IA = params_values["eta_IA"] - z0_IA = trapezoid(z_tracer * nz_tracer) + # n(z)-weighted mean redshift of this tracer, used as the NLA pivot. + # CCL's WeakLensingTracer (use_A_ia=True, the default) supplies the + # C1·rho_crit·Omega_m/D(z) normalization (Joachimi 2011 Eq. 6), so we + # pass only the dimensionless ((1+z)/(1+z0))^eta evolution shape. + z0_IA = trapezoid(z_tracer * nz_tracer, x=z_tracer) / trapezoid( + nz_tracer, x=z_tracer + ) return (z_tracer, A_IA * ((1 + z_tracer) / (1 + z0_IA)) ** eta_IA) elif self.ia_mode == "nla-perbin": A_IA = params_values[f"{tracer_name}_A_IA"] @@ -86,9 +89,36 @@ class CCLTracersCrossLikelihood(CCLTracersLikelihood): ncovsims: int | None provider: Provider + # Physical quantities :meth:`_get_tracer` knows how to build. The base + # ``_check_tracers`` restricts the SACC data to ``_allowable_tracers``; + # ``_check_buildable_tracers`` then guarantees ``_allowable_tracers`` itself + # stays within this set, so every quantity reaching ``_get_tracer`` is buildable. + _BUILDABLE_QUANTITIES: ClassVar[list[str]] = [ + "cmb_convergence", + "galaxy_density", + "galaxy_shear", + ] + def initialize(self): super().initialize() self._check_is_cross() + self._check_buildable_tracers() + + def _check_buildable_tracers(self): + """Reject (at init) any allowed quantity ``_get_tracer`` cannot build. + + Catches the developer-side mismatch where a subclass lists a quantity in + ``_allowable_tracers`` that ``_get_tracer`` has no branch for -- regardless + of whether the current SACC data happens to exercise it. + """ + unbuildable = set(self._allowable_tracers or ()) - set(self._BUILDABLE_QUANTITIES) + if unbuildable: + raise LoggedError( + self.log, + f"{self.__class__.__name__} allows tracer quantities " + f"{sorted(unbuildable)} that it cannot build; _get_tracer supports " + f"only {list(self._BUILDABLE_QUANTITIES)}.", + ) def _check_is_cross(self): for tracer_comb in self.sacc_data.get_tracer_combinations(): @@ -104,6 +134,69 @@ def _check_is_cross(self): ini file.".format(self.__class__.__name__), ) + def _get_tracer(self, ccl: CCL, cosmo: dict, tracer_name: str, params_values: dict): + """Build the CCL tracer for a SACC tracer, by its physical quantity.""" + quantity = self.sacc_data.tracers[tracer_name].quantity + if quantity == "cmb_convergence": + return ccl.CMBLensingTracer(cosmo, z_source=self.provider.get_param("zstar")) + + z = self.sacc_data.tracers[tracer_name].z + nz = self.sacc_data.tracers[tracer_name].nz + if quantity == "galaxy_density": + return ccl.NumberCountsTracer( + cosmo, + has_rsd=False, + dndz=(z, nz), + bias=(z, params_values["b1"] * np.ones(len(z))), + mag_bias=(z, params_values["s1"] * np.ones(len(z))), + ) + if quantity == "galaxy_shear": + ia_z = self._get_ia_bias(z, nz, tracer_name, params_values) + tracer = ccl.WeakLensingTracer(cosmo, dndz=(z, nz), ia_bias=ia_z) + if getattr(self, "z_nuisance_mode", None) is not None: + nz = self._get_nz(z, tracer, tracer_name, **params_values) + tracer = ccl.WeakLensingTracer(cosmo, dndz=(z, nz), ia_bias=ia_z) + return tracer + raise ValueError( + f"Tracer {tracer_name!r} has unsupported quantity {quantity!r}; " + f"{self.__class__.__name__} can build tracers only for " + f"{list(self._BUILDABLE_QUANTITIES)}." + ) + + def _get_unbinned_theory(self, **params_values) -> list[np.ndarray]: + """Unbinned Limber spectra per tracer combination (binning done by base). + + Shared by all CCL cross-correlation likelihoods: build both tracers from + their physical quantity, take the Limber ``angular_cl`` on each tracer + pair's bandpower-window support, and apply shear multiplicative bias. + """ + ccl, cosmo = self._get_CCL_results() + cl_unbinned_list: list[np.ndarray] = [] + + for tracer_comb in self.sacc_data.get_tracer_combinations(): + tracer1 = self._get_tracer(ccl, cosmo, tracer_comb[0], params_values) + tracer2 = self._get_tracer(ccl, cosmo, tracer_comb[1], params_values) + ells_theory, _ = self.get_binning(tracer_comb) + + cl_unbinned = ccl.cells.angular_cl(cosmo, tracer1, tracer2, ells_theory) + + shear_name = next( + ( + name + for name in tracer_comb + if self.sacc_data.tracers[name].quantity == "galaxy_shear" + ), + None, + ) + if shear_name is not None and ( + getattr(self, "m_nuisance_mode", None) is not None + ): + # note shear x shear (both tracers) is not handled here + cl_unbinned = (1 + params_values[f"{shear_name}_m"]) * cl_unbinned + + cl_unbinned_list.append(cl_unbinned) + return cl_unbinned_list + class CCLTracersAutoLikelihood(CCLTracersLikelihood): r""" @@ -143,37 +236,7 @@ class GalaxyKappaLikelihood(CCLTracersCrossLikelihood): _allowable_tracers: ClassVar[list[str]] = ["cmb_convergence", "galaxy_density"] params: dict - def _get_theory(self, **params_values) -> np.ndarray: - ccl, cosmo = self._get_CCL_results() - - tracer_comb = self.sacc_data.get_tracer_combinations() - - for tracer in np.unique(tracer_comb): - if self.sacc_data.tracers[tracer].quantity == "cmb_convergence": - cmbk_tracer = tracer - elif self.sacc_data.tracers[tracer].quantity == "galaxy_density": - gal_tracer = tracer - - z_gal_tracer = self.sacc_data.tracers[gal_tracer].z - nz_gal_tracer = self.sacc_data.tracers[gal_tracer].nz - - # this should use the bias theory! - tracer_g = ccl.NumberCountsTracer( - cosmo, - has_rsd=False, - dndz=(z_gal_tracer, nz_gal_tracer), - bias=(z_gal_tracer, params_values["b1"] * np.ones(len(z_gal_tracer))), - mag_bias=(z_gal_tracer, params_values["s1"] * np.ones(len(z_gal_tracer))), - ) - tracer_k = ccl.CMBLensingTracer(cosmo, z_source=self.provider.get_param("zstar")) - - ells_theory_gk, w_bins_gk = self.get_binning((gal_tracer, cmbk_tracer)) - - cl_gk_unbinned = ccl.cells.angular_cl(cosmo, tracer_k, tracer_g, ells_theory_gk) - - cl_gk_binned = np.dot(w_bins_gk, cl_gk_unbinned) - - return cl_gk_binned + # Theory comes from the shared CCLTracersCrossLikelihood._get_unbinned_theory. class ShearKappaLikelihood(CCLTracersCrossLikelihood): @@ -189,102 +252,6 @@ class ShearKappaLikelihood(CCLTracersCrossLikelihood): ia_mode: str | None params: dict - def _get_theory(self, **params_values) -> np.ndarray: - ccl, cosmo = self._get_CCL_results() - cl_binned_list: list[np.ndarray] = [] - - for tracer_comb in self.sacc_data.get_tracer_combinations(): - if self.sacc_data.tracers[tracer_comb[0]].quantity == "cmb_convergence": - tracer1 = ccl.CMBLensingTracer( - cosmo, z_source=self.provider.get_param("zstar") - ) - - elif self.sacc_data.tracers[tracer_comb[0]].quantity == "galaxy_shear": - sheartracer_name = tracer_comb[0] - - z_tracer1 = self.sacc_data.tracers[tracer_comb[0]].z - nz_tracer1 = self.sacc_data.tracers[tracer_comb[0]].nz - - ia_z = self._get_ia_bias( - z_tracer1, nz_tracer1, sheartracer_name, params_values - ) - - tracer1 = ccl.WeakLensingTracer( - cosmo, dndz=(z_tracer1, nz_tracer1), ia_bias=ia_z - ) - - if self.z_nuisance_mode is not None: - nz_tracer1 = self._get_nz( - z_tracer1, tracer1, tracer_comb[0], **params_values - ) - - tracer1 = ccl.WeakLensingTracer( - cosmo, dndz=(z_tracer1, nz_tracer1), ia_bias=ia_z - ) - - if self.sacc_data.tracers[tracer_comb[1]].quantity == "cmb_convergence": - tracer2 = ccl.CMBLensingTracer( - cosmo, z_source=self.provider.get_param("zstar") - ) - - elif self.sacc_data.tracers[tracer_comb[1]].quantity == "galaxy_shear": - sheartracer_name = tracer_comb[1] - - z_tracer2 = self.sacc_data.tracers[tracer_comb[1]].z - nz_tracer2 = self.sacc_data.tracers[tracer_comb[1]].nz - - ia_z = self._get_ia_bias( - z_tracer2, nz_tracer2, sheartracer_name, params_values - ) - - tracer2 = ccl.WeakLensingTracer( - cosmo, dndz=(z_tracer2, nz_tracer2), ia_bias=ia_z - ) - - if self.z_nuisance_mode is not None: - nz_tracer2 = self._get_nz( - z_tracer2, tracer2, tracer_comb[1], **params_values - ) - - tracer2 = ccl.WeakLensingTracer( - cosmo, dndz=(z_tracer2, nz_tracer2), ia_bias=ia_z - ) - - bpw_idx = self.sacc_data.indices(tracers=tracer_comb) - bpw = self.sacc_data.get_bandpower_windows(bpw_idx) - ells_theory = np.asarray(bpw.values, dtype=int) - w_bins = bpw.weight.T - - cl_unbinned = ccl.cells.angular_cl(cosmo, tracer1, tracer2, ells_theory) - if self.m_nuisance_mode is not None: - # note this allows wrong calculation, as we can do - # shear x shear if the spectra are in the sacc - # but then we would want (1 + m1) * (1 + m2) - m_bias = params_values[f"{sheartracer_name}_m"] - cl_unbinned = (1 + m_bias) * cl_unbinned - - cl_binned = np.dot(w_bins, cl_unbinned) - cl_binned_list.append(cl_binned) - - cl_binned_total = np.concatenate(cl_binned_list) - return cl_binned_total - - def _get_tracer(self, ccl: CCL, cosmo: dict, tracer_name: str, params_values: dict): - tracer_data = self.sacc_data.tracers[tracer_name] - if tracer_data.quantity == "cmb_convergence": - return ccl.CMBLensingTracer(cosmo, z_source=self.provider.get_param("zstar")) - elif tracer_data.quantity == "galaxy_shear": - z_tracer = self.sacc_data.tracers[tracer_name].z - nz_tracer = self.sacc_data.tracers[tracer_name].nz - ia_z = self._get_ia_bias(z_tracer, nz_tracer, tracer_name, params_values) - - tracer = ccl.WeakLensingTracer( - cosmo, dndz=(z_tracer, nz_tracer), ia_bias=ia_z - ) - if self.z_nuisance_mode is not None: - nz_tracer = self._get_nz(z_tracer, tracer, tracer_name, **params_values) - tracer = ccl.WeakLensingTracer( - cosmo, dndz=(z_tracer, nz_tracer), ia_bias=ia_z - ) - return tracer - return None + # Theory comes from the shared CCLTracersCrossLikelihood._get_unbinned_theory, + # which builds shear tracers (with IA / redshift nuisance) and applies the + # multiplicative-bias nuisance. diff --git a/soliket/cross_covariance/__init__.py b/soliket/cross_covariance/__init__.py new file mode 100644 index 00000000..71c260a2 --- /dev/null +++ b/soliket/cross_covariance/__init__.py @@ -0,0 +1,31 @@ +"""Cross-covariance computation between CMB primary, CMB lensing and LSS.""" + +from ._build import ( + camb_lensing_derivatives_from_sacc, + cmb_combs_from_spec_meta, + cmb_lensing_crosscov, + lensing_induced_cov, + shear_kappa_crosscov, + shear_kappa_limber, +) +from ._derivatives import camb_lensing_derivatives +from ._kernels import ( + cmb_lensing_block, + lensing_induced_block, + n1_crosscov_block, + shear_kappa_block, +) + +__all__ = [ + "camb_lensing_derivatives", + "camb_lensing_derivatives_from_sacc", + "cmb_combs_from_spec_meta", + "cmb_lensing_block", + "cmb_lensing_crosscov", + "lensing_induced_block", + "lensing_induced_cov", + "n1_crosscov_block", + "shear_kappa_block", + "shear_kappa_crosscov", + "shear_kappa_limber", +] diff --git a/soliket/cross_covariance/_build.py b/soliket/cross_covariance/_build.py new file mode 100644 index 00000000..7b9444f3 --- /dev/null +++ b/soliket/cross_covariance/_build.py @@ -0,0 +1,205 @@ +"""Extract kernel inputs from a model and assemble cross-covariance blocks. + +This is the convenience layer: it pulls fsky, the fiducial cosmology/accuracy, +the CMB bandpower windows and the kappa-side binning from a likelihood/Session, +runs :func:`camb_lensing_derivatives`, and calls the pure kernels. The physics +lives in ``_kernels``; this module only does the wiring. +""" + +import ast + +import numpy as np + +from ._derivatives import camb_lensing_derivatives +from ._kernels import cmb_lensing_block, lensing_induced_block, shear_kappa_block + + +def _cosmo_camb_kwargs(cosmo): + """Map a SACC-metadata cosmology dict to ``camb.set_params`` keywords.""" + return { + "cosmomc_theta": cosmo["cosmomc_theta"], + "As": 1e-10 * np.exp(cosmo["logA"]), + "ombh2": cosmo["ombh2"], + "omch2": cosmo["omch2"], + "ns": cosmo["ns"], + "Alens": cosmo["Alens"], + "tau": cosmo["tau"], + } + + +# spec_meta polarisation -> CAMB lensed-Cl derivative row (TT=0, EE=1, BB=2, TE=3). +_POL_TO_CAMB = {"tt": 0, "ee": 1, "bb": 2, "te": 3} + + +def cmb_combs_from_spec_meta(spec_meta): + """Per spectrum, the ``(ind_camb, support, weight)`` triple, in MFLike's own + data-vector (auto-covariance) order. + + Driven by ``mflike.spec_meta`` -- the *same* per-spectrum windows and ordering + MFLike uses to build its auto-covariance (and the smooth-data binner) -- so a + cross-covariance block built from these is aligned with the auto-covariance + **by construction**: no reliance on the SACC's tracer-combination order, robust + to a reordered cov_Bbl file or TE/ET symmetrization (the lensed-Cl derivative is + per spectrum type, so the TE window is the right row even when ET is folded in). + The rows are already scale-cut, so no further trimming is needed. + """ + return [ + (_POL_TO_CAMB[m["pol"]], np.asarray(m["bpw"].values), m["bpw"].weight.T) + for m in spec_meta + ] + + +def cmb_lensing_crosscov( + mflike_sacc, + lensing, + combs, + *, + fsky=None, + cosmo=None, + accuracy=None, + lmax=None, + derivatives=None, +): + """Compute the CMB-primary x CMB-lensing cross-covariance block. + + Low-level entry point. ``mflike_sacc`` is the MFLike SACC (with covariance + metadata); ``lensing`` is an evaluated ``LensingLikelihood``. ``combs`` are the + per-spectrum ``(ind_camb, support, weight)`` triples for the CMB rows -- build + them with :func:`cmb_combs_from_spec_meta`, which keeps the block rows in + MFLike's own data-vector order. ``fsky``, ``cosmo``, ``accuracy`` and ``lmax`` + default to the values stored in the MFLike SACC metadata and may be overridden. + ``derivatives`` optionally supplies a precomputed ``camb_lensing_derivatives`` + bundle (see :func:`camb_lensing_derivatives_from_sacc`) so a joint covariance + shares one CAMB run across blocks; when omitted it is computed here. + """ + fsky, cosmo, accuracy, lmax = _resolve_inputs( + mflike_sacc.metadata, fsky, cosmo, accuracy, lmax + ) + _, clp, dCllens = ( + derivatives + if derivatives is not None + else camb_lensing_derivatives(cosmo, accuracy, lmax) + ) + + lmax_kk = lensing.binning_matrix.shape[1] + cl_kk = np.pi / 2 * lensing.provider.get_Cl(ell_factor=True)["pp"][:lmax_kk] + return cmb_lensing_block(dCllens, clp, cl_kk, fsky, combs, lensing.binning_matrix) + + +def _resolve_inputs(md, fsky, cosmo, accuracy, lmax): + """Fill fsky/cosmo/accuracy/lmax from MFLike SACC metadata where not given.""" + + def meta(key): + if key not in md: + raise KeyError( + f"MFLike SACC metadata has no {key!r}; pass the corresponding " + "argument to the cross-covariance call explicitly." + ) + return md[key] + + if fsky is None: + fsky = float(meta("f_sky_LAT")) + if accuracy is None: + accuracy = ast.literal_eval(meta("accuracy_params")) + if lmax is None: + lmax = int(meta("lmax")) + 1 + if cosmo is None: + cosmo = _cosmo_camb_kwargs(ast.literal_eval(meta("cosmo_params"))) + return fsky, cosmo, accuracy, lmax + + +def camb_lensing_derivatives_from_sacc( + mflike_sacc, *, cosmo=None, accuracy=None, lmax=None +): + """CAMB lensed-Cl derivative bundle for an MFLike SACC, computed once. + + Resolves ``cosmo``/``accuracy``/``lmax`` from the MFLike SACC metadata (each + overridable) and runs CAMB a single time. Pass the returned ``(cls, clp, + dCllens)`` bundle as ``derivatives=`` to :func:`cmb_lensing_crosscov`, + :func:`lensing_induced_cov` and :func:`shear_kappa_crosscov` so a joint + covariance shares one CAMB run instead of recomputing the (expensive) + derivative per block. + """ + _, cosmo, accuracy, lmax = _resolve_inputs( + mflike_sacc.metadata, None, cosmo, accuracy, lmax + ) + return camb_lensing_derivatives(cosmo, accuracy, lmax) + + +def lensing_induced_cov( + mflike_sacc, + combs, + *, + fsky=None, + cosmo=None, + accuracy=None, + lmax=None, + derivatives=None, +): + """Compute the lensing-induced covariance within the MFLike CMB block. + + Low-level entry point mirroring :func:`cmb_lensing_crosscov`; ``combs`` are the + per-spectrum triples (see :func:`cmb_combs_from_spec_meta`), and ``fsky``, + ``cosmo``, ``accuracy`` and ``lmax`` default to the MFLike SACC metadata. + ``derivatives`` optionally supplies a precomputed ``camb_lensing_derivatives`` + bundle to share one CAMB run across blocks. Returns the symmetric + ``(n_cmb_data, n_cmb_data)`` matrix. + """ + fsky, cosmo, accuracy, lmax = _resolve_inputs( + mflike_sacc.metadata, fsky, cosmo, accuracy, lmax + ) + _, _, dCllens = ( + derivatives + if derivatives is not None + else camb_lensing_derivatives(cosmo, accuracy, lmax) + ) + return lensing_induced_block(dCllens, fsky, combs) + + +def shear_kappa_limber(shearkappa_like, params_values): + """Unbinned shear x CMB-lensing spectra and bandpower windows per LSS tracer. + + Thin wrapper over the likelihood's own theory: ``get_unbinned_theory`` returns + the Limber spectra (including IA, redshift- and multiplicative-bias nuisance + handling) and ``get_binning`` the bandpower windows. Returns + ``(cl_unbinned_list, w_bins_list)``. + """ + sklike = shearkappa_like + cl_unbinned_list = sklike.get_unbinned_theory(**params_values) + w_bins_list = [ + sklike.get_binning(comb)[1] for comb in sklike.sacc_data.get_tracer_combinations() + ] + return cl_unbinned_list, w_bins_list + + +def shear_kappa_crosscov( + mflike_sacc, + shearkappa_like, + params_values, + combs, + *, + fsky=None, + cosmo=None, + accuracy=None, + lmax=None, + derivatives=None, +): + """Compute the CMB-primary x shear/galaxy-kappa cross-covariance block. + + Low-level entry point; ``fsky``/``cosmo``/``accuracy``/``lmax`` default to the + MFLike SACC metadata. ``params_values`` are the nuisance parameters the LSS + Limber spectra depend on. ``combs`` are the per-spectrum triples for the CMB + (row) side -- see :func:`cmb_combs_from_spec_meta`. ``derivatives`` optionally + supplies a precomputed ``camb_lensing_derivatives`` bundle to share one CAMB run + across blocks. + """ + fsky, cosmo, accuracy, lmax = _resolve_inputs( + mflike_sacc.metadata, fsky, cosmo, accuracy, lmax + ) + _, clp, dCllens = ( + derivatives + if derivatives is not None + else camb_lensing_derivatives(cosmo, accuracy, lmax) + ) + cl_list, w_list = shear_kappa_limber(shearkappa_like, params_values) + return shear_kappa_block(dCllens, clp, cl_list, w_list, fsky, combs) diff --git a/soliket/cross_covariance/_derivatives.py b/soliket/cross_covariance/_derivatives.py new file mode 100644 index 00000000..afcd4440 --- /dev/null +++ b/soliket/cross_covariance/_derivatives.py @@ -0,0 +1,26 @@ +"""CAMB lensing-derivative computation feeding the cross-covariance kernels.""" + + +def camb_lensing_derivatives(cosmo, accuracy, lmax): + """Compute the CAMB lensed-Cl derivative w.r.t. the lensing potential. + + Wraps the ``camb.set_params`` -> ``get_results`` -> ``lensed_cl_derivatives`` + chain. ``cosmo`` and ``accuracy`` are passed straight through to + ``camb.set_params`` (so ``cosmo`` may use either ``H0`` or ``cosmomc_theta``, + and ``accuracy`` carries keys like ``lens_potential_accuracy``). + + Returns ``(cls, clp, dCllens)``: + + - ``cls`` -- unlensed total CMB spectra (muK^2), shape ``(lmax+1, 4)``, + - ``clp`` -- lensing-potential power (muK), shape ``(lmax+1,)``, + - ``dCllens`` -- ``∂ C_ell^XY / ∂ C_L^φφ``, shape ``(4, lmax+1, lmax+1)``. + """ + import camb + from camb.correlations import lensed_cl_derivatives + + pars = camb.set_params(lmax=lmax, **cosmo, **accuracy) + pars.set_for_lmax(lmax) + results = camb.get_results(pars) + cls = results.get_unlensed_total_cls(CMB_unit="muK")[: lmax + 1, :] + clp = results.get_lens_potential_cls(CMB_unit="muK")[: lmax + 1, 0] + return cls, clp, lensed_cl_derivatives(cls, clp) diff --git a/soliket/cross_covariance/_kernels.py b/soliket/cross_covariance/_kernels.py new file mode 100644 index 00000000..ec4f2553 --- /dev/null +++ b/soliket/cross_covariance/_kernels.py @@ -0,0 +1,187 @@ +"""Pure cross-covariance kernels. + +These functions take the CAMB lensing derivative ``dCllens`` (``∂ C_ell^XY / +∂ C_L^φφ`` from :func:`camb.correlations.lensed_cl_derivatives`) and fiducial +spectra as arrays, and return covariance blocks. They are deliberately free of +SACC / likelihood coupling so they can be unit-tested in isolation; the +extraction of windows/fsky/spectra from a model lives in the ``CrossCov`` +convenience layer. + +A CMB tracer combination is described by a tuple ``(ind_camb, support, weight)``: + +- ``ind_camb`` -- row of ``dCllens`` for the spectrum (0=TT, 1=EE, 3=TE), +- ``support`` -- the multipoles the combination's bandpower window spans, +- ``weight`` -- the bandpower weights, shape ``(n_bins, len(support))``. +""" + +import numpy as np + + +def cmb_lensing_block(dCllens, clp, cl_kk, fsky, cmb_combs, kk_binning): + """Cross-covariance between binned CMB spectra and binned ``C_L^kk``. + + Parameters + ---------- + dCllens : ndarray, shape (n_spec, lmax+1, lmax+1) + CAMB lensed-Cl derivative w.r.t. the lensing-potential power. + clp : ndarray + Fiducial lensing-potential power ``C_L^φφ`` (indexed by multipole). + cl_kk : ndarray, shape (lmax_kk,) + Fiducial convergence power ``C_L^kk`` on the kappa side. + fsky : float + Sky fraction. + cmb_combs : list of (int, ndarray, ndarray) + One ``(ind_camb, support, weight)`` per CMB tracer combination. + kk_binning : ndarray, shape (n_kk_bins, lmax_kk) + Binning matrix on the kappa side. + + Returns + ------- + ndarray, shape (n_cmb_data, n_kk_bins) + """ + lmax_kk = kk_binning.shape[1] + ell_kk = np.arange(lmax_kk) + cl_kk = np.asarray(cl_kk) + kk_weight = 2.0 * cl_kk**2 / (2 * ell_kk + 1) / fsky + + n_cmb = sum(weight.shape[0] for _, _, weight in cmb_combs) + out = np.zeros((n_cmb, kk_binning.shape[0])) + + row = 0 + for ind_camb, support, weight in cmb_combs: + deriv = dCllens[ind_camb][np.asarray(support)][:, :lmax_kk] + xcov = (2.0 / np.pi) * deriv / clp[:lmax_kk] * kk_weight + xcov[:, 0:2] = 0.0 + out[row : row + weight.shape[0], :] = weight @ (xcov @ kk_binning.T) + row += weight.shape[0] + return out + + +def lensing_induced_block(dCllens, fsky, cmb_combs): + """Lensing-induced covariance mixing the CMB spectra among themselves. + + Captures the covariance the lensing potential induces between (binned) CMB + bandpowers across tracer combinations. Returns the symmetric joint matrix. + + Parameters + ---------- + dCllens : ndarray, shape (n_spec, lmax+1, lmax+1) + CAMB lensed-Cl derivative w.r.t. the lensing-potential power. + fsky : float + Sky fraction. + cmb_combs : list of (int, ndarray, ndarray) + One ``(ind_camb, support, weight)`` per CMB tracer combination. + + Returns + ------- + ndarray, shape (n_cmb_data, n_cmb_data) + """ + lmax = dCllens.shape[1] + ell = np.arange(lmax) + factor = 2.0 / (2 * ell + 1) / fsky + + # Bandpower-weighted derivatives per combination, shape (n_bins, lmax). + binned_deriv = [ + weight @ dCllens[ind_camb][np.asarray(support)] + for ind_camb, support, weight in cmb_combs + ] + offsets = np.cumsum([0] + [a.shape[0] for a in binned_deriv]) + out = np.zeros((offsets[-1], offsets[-1])) + + for i, a_i in enumerate(binned_deriv): + for j in range(i, len(binned_deriv)): + a_j = binned_deriv[j] + block = (a_i[:, None, :] * a_j[None, :, :] * factor).sum(axis=2) + out[offsets[i] : offsets[i + 1], offsets[j] : offsets[j + 1]] = block + + return np.triu(out, 0) + np.triu(out, 1).T + + +def shear_kappa_block(dCllens, clp, lss_spectra, lss_binnings, fsky, cmb_combs): + """Cross-covariance between binned CMB spectra and binned shear/galaxy-kappa. + + Each LSS tracer contributes the same kernel as :func:`cmb_lensing_block` with + its own theory spectrum and bandpower windows; the per-tracer blocks are + concatenated along the LSS axis. + + Parameters + ---------- + dCllens : ndarray, shape (n_spec, lmax+1, lmax+1) + CAMB lensed-Cl derivative w.r.t. the lensing-potential power. + clp : ndarray + Fiducial lensing-potential power. + lss_spectra : list of ndarray + Unbinned theory ``C_ell^{shear x kappa}`` per LSS tracer. + lss_binnings : list of ndarray + Bandpower-window matrix per LSS tracer, shape ``(n_bins, len(spectrum))``. + fsky : float + Sky fraction. + cmb_combs : list of (int, ndarray, ndarray) + One ``(ind_camb, support, weight)`` per CMB tracer combination. + + Returns + ------- + ndarray, shape (n_cmb_data, sum_of_lss_bins) + """ + blocks = [ + cmb_lensing_block(dCllens, clp, spectrum, fsky, cmb_combs, binning) + for spectrum, binning in zip(lss_spectra, lss_binnings) + ] + return np.hstack(blocks) + + +def n1_crosscov_block(dCllens, clp, n1_normed_mat, fsky, cmb_combs, kk_binning): + """N1-bias contribution to the CMB-spectra x ``C_L^kk`` cross-covariance. + + Applies a precomputed, normalised N1 transfer matrix (produced externally via + ``lensitbiases``; see the create_cross_covariance notebook) to the lensing + derivative. Pure given ``n1_normed_mat`` -- the lensitbiases dependency lives + only in generating that matrix. + + This is a **diagnostic**, not part of the assembled covariance: it sizes the N1 + contribution to the kappa cross-block, and no covariance builder (including + :meth:`CrossCov.from_cmb_lensing`) calls it. The dev notebook computes, saves and + plots it, then stops there. + + Parameters + ---------- + dCllens : ndarray, shape (n_spec, lmax+1, lmax+1) + CAMB lensed-Cl derivative w.r.t. the lensing-potential power. + clp : ndarray + Fiducial lensing-potential power. + n1_normed_mat : ndarray, shape (>= lmax_kk, n_ell) + Normalised, smoothed N1 transfer matrix. **Rows must be indexed by lensing + multipole from L=0**, so that row ``L`` aligns with column ``L`` of + ``kk_binning``; only the first ``lmax_kk`` rows are read. Note the + lensitbiases recipe in the notebook builds its rows over + ``Ls_n1 = arange(lminbox, ...)`` (``lminbox=20``), i.e. row ``i`` is + ``L = lminbox + i`` -- such a matrix must be zero-padded up to L=0 before it + is passed here, or every row lands ``lminbox`` multipoles off. + fsky : float + Sky fraction. + cmb_combs : list of (int, ndarray, ndarray) + One ``(ind_camb, support, weight)`` per CMB tracer combination. + kk_binning : ndarray, shape (n_kk_bins, lmax_kk) + Binning matrix on the kappa side. + + Returns + ------- + ndarray, shape (n_cmb_data, n_kk_bins) + """ + n_ell = n1_normed_mat.shape[1] + ell = np.arange(n_ell) + factor = 2 * clp[:n_ell] / (2 * ell + 1) / fsky + factor[1:] *= 2 * np.pi / (ell[1:] * (ell[1:] + 1)) ** 2 + + lmax_kk = kk_binning.shape[1] + binned_n1 = kk_binning @ n1_normed_mat[:lmax_kk, :] * np.pi / 2.0 + + n_cmb = sum(weight.shape[0] for _, _, weight in cmb_combs) + out = np.zeros((n_cmb, kk_binning.shape[0])) + row = 0 + for ind_camb, support, weight in cmb_combs: + a1 = weight @ dCllens[ind_camb][np.asarray(support)] + product = a1[:, None, :n_ell] * binned_n1[None, :, :] * factor + out[row : row + weight.shape[0], :] = product.sum(axis=2) + row += weight.shape[0] + return out diff --git a/soliket/gaussian/gaussian.py b/soliket/gaussian/gaussian.py index 97ad579a..c225ea4a 100644 --- a/soliket/gaussian/gaussian.py +++ b/soliket/gaussian/gaussian.py @@ -277,17 +277,74 @@ def _get_data_spectrum(self) -> np.ndarray: return self.y def get_binning(self, tracer_comb: tuple) -> tuple[np.ndarray, np.ndarray]: - bpw_idx = self.sacc_data.indices(data_type="cl_00", tracers=tracer_comb) - bpw = self.sacc_data.get_bandpower_windows(bpw_idx) - ells_theory = bpw.values - ells_theory = np.asarray(ells_theory, dtype=int) - w_bins = bpw.weight.T - - return ells_theory, w_bins - - def _get_theory(self, **kwargs) -> np.ndarray: + """Bandpower support multipoles and window matrix for a tracer pair. + + The result depends only on the (fixed) SACC file, so it is memoised per + tracer combination: each likelihood evaluation would otherwise re-derive it + twice -- once in :meth:`_get_unbinned_theory` for the theory's ell support + and once in :meth:`_get_theory` to bin -- each time hitting the SACC index + and bandpower-window lookups. + + The window is looked up by tracers alone (not by data type: the shear + cross-correlations are ``cl_0e``/``cl_e0``, not ``cl_00``), so a pair + carrying several data types would silently yield a window spanning all of + them. Rejected here, in the shared lookup, rather than in each caller -- + :meth:`_get_unbinned_theory` reads the ell support from here too, and would + otherwise spend a Limber calculation on the doubled grid before + :meth:`_get_theory` noticed. + """ + cache = self.__dict__.setdefault("_binning_cache", {}) + if tracer_comb not in cache: + dtypes = self.sacc_data.get_data_types(tracers=tracer_comb) + if len(dtypes) != 1: + raise ValueError( + f"tracers {tracer_comb} carry data types {dtypes}; a likelihood " + "using the default binning assumes exactly one spectrum per " + "tracer pair and must otherwise override _get_theory." + ) + bpw_idx = self.sacc_data.indices(tracers=tracer_comb) + bpw = self.sacc_data.get_bandpower_windows(bpw_idx) + ells_theory = np.asarray(bpw.values, dtype=int) + w_bins = bpw.weight.T + cache[tracer_comb] = (ells_theory, w_bins) + return cache[tracer_comb] + + def _get_unbinned_theory(self, **kwargs) -> list[np.ndarray]: + """Unbinned theory spectra on the fine multipole grid, one per tracer + combination. + + Subclasses that compute a finely-sampled theory and then bin it (e.g. the + Limber cross-correlations) override this and inherit binning from the + default :meth:`_get_theory`. Subclasses that produce binned theory + directly override :meth:`_get_theory` instead. + """ raise NotImplementedError + def get_unbinned_theory(self, **params_values) -> list[np.ndarray]: + """The unbinned theory spectra, one array per tracer combination. + + Public accessor used by cross-covariance computations that need the theory + before bandpower binning. + """ + return self._get_unbinned_theory(**params_values) + + def _get_theory(self, **params_values) -> np.ndarray: + """Bin the unbinned theory per tracer combination via :meth:`get_binning`.""" + cl_unbinned = self.get_unbinned_theory(**params_values) + combs = self.sacc_data.get_tracer_combinations() + binned = [] + for comb, cl in zip(combs, cl_unbinned): + w_bins = self.get_binning(comb)[1] + if w_bins.shape[1] != len(cl): + raise ValueError( + f"Binning for tracers {comb} expects {w_bins.shape[1]} " + f"multipoles but the unbinned theory has {len(cl)}. The tracer " + "pair likely carries more than one data type; such a likelihood " + "must override _get_theory rather than use the default binning." + ) + binned.append(np.dot(w_bins, cl)) + return np.concatenate(binned) + def logp(self, **params_values) -> float: theory = self._get_theory(**params_values) return self.data.loglike(theory) diff --git a/soliket/gaussian/gaussian_data.py b/soliket/gaussian/gaussian_data.py index 7362743a..fd984eb9 100644 --- a/soliket/gaussian/gaussian_data.py +++ b/soliket/gaussian/gaussian_data.py @@ -368,8 +368,7 @@ def _infer_component_info(self): for name, n in ((name1, cov.shape[0]), (name2, cov.shape[1])): if name in sizes and sizes[name] != n: raise ValueError( - f"Inconsistent sizes for component '{name}': " - f"{sizes[name]} vs {n}" + f"Inconsistent sizes for component '{name}': {sizes[name]} vs {n}" ) sizes[name] = n @@ -558,6 +557,93 @@ def load(cls, path: str | None) -> Optional["CrossCov"]: return cross_cov + @classmethod + def from_cmb_lensing(cls, mflike, lensing, **overrides): + """Compute the CMB-primary x CMB-lensing cross-covariance, labelled by id. + + Convenience entry point: pulls fsky, fiducial cosmology/accuracy, the CMB + bandpower windows and the kappa binning from the two evaluated likelihoods + ``mflike`` and ``lensing`` (e.g. ``resolve_aliases(model).mflike`` and + ``.lensing`` -- the concrete handles, not a ``Session``), runs CAMB and the + cross-covariance kernel, and returns a single labelled cross block ready to + :meth:`save`. ``overrides`` (``fsky``/``cosmo``/``accuracy``/``lmax``) are + forwarded to the low-level + :func:`soliket.cross_covariance.cmb_lensing_crosscov`. + + Every block (the cross term and both auto-covariances) carries its bandpower + identity, so the order in which any of them is built is irrelevant: + :meth:`to_canonical` realigns each to the data by identity at assembly. The + CMB rows use mflike's ``spec_meta`` vocabulary ``(pol, hasYX_xsp, (t1, t2), + leff)`` -- exactly what :func:`soliket.gaussian.gaussian.bandpower_ids` + reconstructs for mflike -- and the kappa columns reuse the lensing data's own + ids. The full-kappa columns are trimmed to the bins the lensing likelihood + keeps, so the cross block and the lensing auto share one column identity. + + The component auto-covariances are carried alongside the cross block (keyed by + the components' real ``GaussianData`` names) so the saved file is a + self-contained joint covariance that drops straight into a + :class:`MultiGaussianLikelihood`. Both likelihoods' model must already be + evaluated, and ``mflike`` must expose ``spec_meta``. + """ + import os + + import numpy as np + import sacc + + from soliket.cross_covariance import ( + cmb_combs_from_spec_meta, + cmb_lensing_crosscov, + ) + from soliket.gaussian.gaussian import bandpower_ids + + spec_meta = getattr(mflike, "spec_meta", None) + if spec_meta is None: + raise ValueError( + "from_cmb_lensing requires the mflike likelihood to expose " + "spec_meta (the per-spectrum bandpower metadata) so the cross-cov " + "rows can be labelled by bandpower identity." + ) + + # The covariance+bandpower SACC carries the metadata and windows; fall back + # to input_file if MFLike was wired without a separate cov_Bbl_file. + sacc_file = mflike.cov_Bbl_file or mflike.input_file + mflike_sacc = sacc.Sacc.load_fits(os.path.join(mflike.data_folder, sacc_file)) + + mflike_data = mflike._get_gauss_data() + lensing_data = lensing._get_gauss_data() + + # Build the block from mflike's per-spectrum windows. The build order does + # not matter: the labels below carry each row's true identity. + combs = cmb_combs_from_spec_meta(spec_meta) + block = cmb_lensing_crosscov(mflike_sacc, lensing, combs, **overrides) + + # Per-row CMB bandpower identities, in the block's own (spec_meta) build + # order, in the same vocabulary as gaussian.bandpower_ids for mflike. + row_ids = [ + (m["pol"], bool(m["hasYX_xsp"]), (m["t1"], m["t2"]), float(leff)) + for m in spec_meta + for leff in np.asarray(m["leff"]) + ] + # Trim the full-kappa columns to the bins the lensing likelihood keeps, so the + # cross block and the lensing auto share one column order/identity. + lens_indices = lensing_data.indices + block = block[:, lens_indices] + col_ids = ( + [k for k, keep in zip(lensing_data.ids, lens_indices) if keep] + if lensing_data.ids is not None + else None + ) + + result = cls() + # The mflike auto-cov is in its own data-vector order; CrossCov realigns the + # cross rows (built in spec_meta order) to it by identity at save. + result.add_component(mflike_data.name, mflike_data.cov, ids=bandpower_ids(mflike)) + result.add_component(lensing_data.name, lensing_data.cov, ids=col_ids) + result.add_cross_covariance( + mflike_data.name, lensing_data.name, block, ids1=row_ids, ids2=col_ids + ) + return result + class MultiGaussianData(GaussianData): """Combined Gaussian data from multiple components with cross-covariances. diff --git a/soliket/presets/__init__.py b/soliket/presets/__init__.py new file mode 100644 index 00000000..3a27b49c --- /dev/null +++ b/soliket/presets/__init__.py @@ -0,0 +1,17 @@ +"""Ready-to-run SOLikeT configurations for notebooks and quickstart use.""" + +from ._aliases import AliasView, resolve_aliases +from ._loader import build_params, load_fiducial_map, load_params +from ._session import PRESETS, Session, build_info, quickstart + +__all__ = [ + "PRESETS", + "AliasView", + "Session", + "build_info", + "build_params", + "load_fiducial_map", + "load_params", + "quickstart", + "resolve_aliases", +] diff --git a/soliket/presets/_aliases.py b/soliket/presets/_aliases.py new file mode 100644 index 00000000..2de8d873 --- /dev/null +++ b/soliket/presets/_aliases.py @@ -0,0 +1,62 @@ +"""Resolve role aliases on a Cobaya model. + +Replaces fragile positional ``model.components[i]`` access with named roles +(``.mflike``, ``.lensing``, ``.foreground``, ``.cosmo``), matched by class so they +survive reordering and recurse into ``MultiGaussianLikelihood.likelihoods``. +""" + +from cobaya.theories.camb.camb import CAMB +from cobaya.theories.classy import classy + +from ..gaussian import MultiGaussianLikelihood +from ..lensing import LensingLikelihood, LensingLiteLikelihood + +# Role names, in priority order. The class tuple for each role is resolved lazily +# (see ``_role_classes``) so importing ``soliket.presets`` does not require the +# optional ``mflike`` package -- only resolving aliases on a real model does. +_ROLES = ("mflike", "lensing", "foreground", "cosmo") + + +def _role_classes(): + """Map each role to the classes whose instances fill it. + + Imports ``mflike`` lazily: it is an optional dependency, so only callers that + actually resolve aliases on a built model need it installed. + """ + from mflike import Foreground + from mflike.mflike import _MFLike + + return { + "mflike": (_MFLike,), + "lensing": (LensingLikelihood, LensingLiteLikelihood), + "foreground": (Foreground,), + "cosmo": (CAMB, classy), + } + + +class AliasView: + """A model's Likelihood/Theory members, reachable by role name.""" + + def __init__(self, roles): + for role in _ROLES: + setattr(self, role, roles.get(role)) + + +def _walk(component): + """Yield a component and, for a MultiGaussianLikelihood, its sub-likelihoods.""" + if isinstance(component, MultiGaussianLikelihood): + yield from component.likelihoods + else: + yield component + + +def resolve_aliases(model): + """Return an :class:`AliasView` of ``model``'s components keyed by role.""" + role_classes = _role_classes() + roles = {} + for component in model.components: + for member in _walk(component): + for role, classes in role_classes.items(): + if role not in roles and isinstance(member, classes): + roles[role] = member + return AliasView(roles) diff --git a/soliket/presets/_loader.py b/soliket/presets/_loader.py new file mode 100644 index 00000000..6d999069 --- /dev/null +++ b/soliket/presets/_loader.py @@ -0,0 +1,178 @@ +"""Load the Fiducial map and turn it into a Cobaya ``params`` dict. + +A *dual parameter* carries a sampling spec (``prior``/``ref``/``proposal``/ +``latex``). It is emitted in its full sampling form only when its name appears in +the explicit ``sample`` list; otherwise it collapses to ``{value: }``, +where the central value is the spec's ``ref`` (or prior centre). Everything else +on the parameter (``latex``, ``drop``, ``renames``) is carried over. +""" + +from importlib import resources +from pathlib import Path + +import yaml + +# Sampling-only keys, dropped when a dual parameter is fixed to its central value. +_SAMPLING_KEYS = ("prior", "ref", "proposal") + + +def _central_value(pspec): + """The fixed value a dual parameter takes when it is not being sampled.""" + if "value" in pspec: + return pspec["value"] + ref = pspec.get("ref") + if isinstance(ref, dict): + return ref["loc"] + if ref is not None: + return ref + prior = pspec["prior"] + if "loc" in prior: + return prior["loc"] + if "min" in prior and "max" in prior: + return 0.5 * (prior["min"] + prior["max"]) + raise ValueError( + f"cannot derive a fixed value from prior {prior!r}; add an explicit 'ref'" + ) + + +def build_params(spec, sample=None, groups=None): + """Flatten the grouped Fiducial map ``spec`` into a Cobaya ``params`` dict. + + ``groups`` optionally restricts the output to the named top-level groups + (e.g. ``["cosmo"]``); by default every group is included. + """ + sample = set(sample or []) + if groups is None: + selected = spec + else: + missing = [g for g in groups if g not in spec] + if missing: + raise ValueError( + f"missing parameter group(s) {missing}: no defaults/.yaml " + f"provides them; available groups: {sorted(spec)}" + ) + selected = {g: spec[g] for g in groups} + # Only dual parameters (those carrying a prior) can be sampled, so an unknown + # name -- a typo, a param from a group this preset does not load, or one fixed + # by value -- would otherwise be dropped in silence and yield a fully-fixed run. + sampleable = {n for g in selected.values() for n, p in g.items() if "prior" in p} + unknown = sample - sampleable + if unknown: + raise ValueError( + f"cannot sample {sorted(unknown)}: not a dual parameter in group(s) " + f"{sorted(selected)}; sampleable here: {sorted(sampleable)}" + ) + params = {} + for group in selected.values(): + for name, pspec in group.items(): + if "prior" in pspec and name not in sample: + fixed = {k: v for k, v in pspec.items() if k not in _SAMPLING_KEYS} + fixed["value"] = _central_value(pspec) + params[name] = fixed + else: + params[name] = pspec + return params + + +def _theory_renames(): + """Map each native parameter name to its common aliases, per Cobaya's tables. + + Cobaya's camb/classy theories share a common alias namespace (e.g. the alias + ``omegabh2`` maps to ``ombh2`` in CAMB and ``omega_b`` in CLASS). Sourcing + aliases from here keeps the Fiducial map theory-portable without us tracking + the mapping ourselves. + """ + from cobaya.theories.camb.camb import CAMB + from cobaya.theories.classy.classy import classy + + by_native = {} + for cls in (CAMB, classy): + for alias, native in cls.get_defaults().get("renames", {}).items(): + by_native.setdefault(native, set()).add(alias) + return by_native + + +def _attach_renames(params): + """Add Cobaya's common aliases to each parameter's ``renames`` (in place).""" + by_native = _theory_renames() + for name, pspec in params.items(): + aliases = by_native.get(name) + if aliases: + merged = set(pspec.get("renames", [])) | aliases + params[name] = {**pspec, "renames": sorted(merged)} + return params + + +def _bundled_defaults_dir(): + """The packaged ``presets/defaults/`` directory (a Traversable).""" + return resources.files(__package__).joinpath("defaults") + + +# ``theory.yaml`` is the neutrino/theory overlay rather than a param group, so it is +# not listed in _GROUPS and is loaded via ``load_theory``. +_THEORY_GROUP = "theory" + +# The param groups, one bundled ``defaults/.yaml`` each. An override folder +# can only replace a bundled group's file, never add a group, so this is the full +# set by construction; adding one means shipping the yaml AND naming it here (plus +# in the presets that want it -- see PRESETS[""]["groups"]). +_GROUPS = ("cosmo", "foreground", "systematics") + + +def _as_mapping(text, source): + """Parse a group YAML file, requiring a mapping of param specs.""" + doc = yaml.safe_load(text) + if not isinstance(doc, dict): + raise ValueError( + f"{source}: expected a mapping of param specs, got {type(doc).__name__}" + ) + return doc + + +def _read_group(group, defaults_dir): + """Read one group's param dict, preferring an override file in ``defaults_dir``. + + Per-file fallback: ``defaults_dir/.yaml`` replaces the bundled file for + that group when present; otherwise the bundled file is used. + """ + if defaults_dir is not None: + override = Path(defaults_dir) / f"{group}.yaml" + if override.is_file(): + return _as_mapping(override.read_text(), override) + bundled = _bundled_defaults_dir().joinpath(f"{group}.yaml") + return _as_mapping(bundled.read_text(), bundled) + + +def load_fiducial_map(defaults_dir=None): + """Parse the Fiducial map (packaged defaults, optionally with per-group + overrides) into its grouped form. + + Each top-level group is one ``defaults/.yaml`` file. ``defaults_dir`` + optionally supplies override files; any ``.yaml`` it contains replaces + the bundled file for that group (per-file fallback, mflike-style). + """ + return {group: _read_group(group, defaults_dir) for group in _GROUPS} + + +def load_theory(defaults_dir=None): + """Read the theory overlay (``theory.yaml``): the neutrino-sector ``extra_args`` + keyed by Boltzmann code, plus any code-specific neutrino params. + + Per-file fallback like the param groups: ``defaults_dir/theory.yaml`` replaces + the bundled file wholesale when present; otherwise the bundled file is used. + """ + return _read_group(_THEORY_GROUP, defaults_dir) + + +def load_params(sample=None, groups=None, defaults_dir=None): + """Return the Cobaya ``params`` dict for the Fiducial map (packaged defaults, + optionally with per-group overrides). + + ``sample`` is the explicit list of dual parameters to vary; every other dual + parameter is fixed to its fiducial central value. ``groups`` optionally + restricts the output to the named groups. ``defaults_dir`` optionally points at + a directory of override ``.yaml`` files (per-file fallback). + """ + spec = load_fiducial_map(defaults_dir=defaults_dir) + params = build_params(spec, sample=sample, groups=groups) + return _attach_renames(params) diff --git a/soliket/presets/_session.py b/soliket/presets/_session.py new file mode 100644 index 00000000..e80d18e5 --- /dev/null +++ b/soliket/presets/_session.py @@ -0,0 +1,204 @@ +"""Presets and the running layer: assemble a Cobaya ``info`` dict and drive it. + +A *preset* names a ready-to-run configuration (which likelihood, which theory +blocks, which Fiducial-map groups). ``build_info`` turns a preset into a Cobaya +``info`` dict; sampling runs Python-native via ``cobaya.run`` (no run YAML). +""" + +from importlib import resources +from pathlib import Path + +import yaml +from cobaya.tools import recursive_update + +from ._aliases import resolve_aliases +from ._loader import load_params, load_theory + +# Boltzmann codes the presets can target; the neutrino baseline for each lives in +# the Fiducial map's ``theory.yaml`` (folder-overridable), not here. +_THEORY_CODES = ("camb", "classy") + +# preset name -> info template file + the Fiducial-map groups it needs +PRESETS = { + "mflike": { + "template": "mflike.yaml", + "groups": ["cosmo", "foreground", "systematics"], + }, + "lensing": { + "template": "lensing.yaml", + "groups": ["cosmo"], + }, + "multigaussian": { + "template": "multigaussian.yaml", + "groups": ["cosmo", "foreground", "systematics"], + # Per-component options and precision are composed from these member + # presets (single source of truth), not duplicated in the skeleton. + "members": ["mflike", "lensing"], + }, +} + + +def _load_template(filename, defaults_dir=None): + """Load the packaged info skeleton, optionally overlaid by a folder template. + + Unlike the param groups (wholesale per-file replacement), a folder + ``defaults_dir/templates/`` is layered onto the packaged skeleton via + cobaya's ``recursive_update`` (last wins): it patches only the keys it sets -- + e.g. a single ``theory_lmax`` -- and inherits the rest, so it tracks future + package-template changes. Absent the file, the packaged skeleton is used as-is. + """ + text = resources.files(__package__).joinpath("templates", filename).read_text() + info = yaml.safe_load(text) + if defaults_dir is not None: + override = Path(defaults_dir) / "templates" / filename + if override.is_file(): + info = recursive_update(info, yaml.safe_load(override.read_text())) + return info + + +def build_info(preset, sample=None, theory="camb", defaults_dir=None): + """Assemble the Cobaya ``info`` dict for ``preset``. + + ``sample`` is the explicit list of dual parameters to vary; the rest are fixed + to their fiducial values. ``theory`` selects the Boltzmann solver: ``"camb"`` + (default) or ``"classy"``. ``defaults_dir`` optionally points at a directory of + override files: ``.yaml`` for the param groups and ``theory.yaml`` for + the neutrino sector (per-file wholesale replacement, fallback to the bundled + defaults), plus ``templates/.yaml`` to overlay likelihood/theory options + onto the packaged skeleton (``recursive_update``, last wins). A relative + ``defaults_dir`` is resolved against the process working directory. Returns a + fresh dict each call. + """ + if preset not in PRESETS: + raise ValueError(f"unknown preset {preset!r}; choose from {sorted(PRESETS)}") + if theory not in _THEORY_CODES: + raise ValueError( + f"unknown theory {theory!r}; choose from {sorted(_THEORY_CODES)}" + ) + spec = PRESETS[preset] + info = _load_template(spec["template"], defaults_dir=defaults_dir) + if "members" in spec: + _compose_members(info, spec["members"], defaults_dir=defaults_dir) + info["params"] = load_params( + sample=sample, groups=spec["groups"], defaults_dir=defaults_dir + ) + _apply_theory(info, theory, defaults_dir=defaults_dir) + return info + + +def _compose_members(info, members, defaults_dir=None): + """Fill a multi-component preset's per-component options and theory from its + member presets, so each member's config lives in ONE place (its own template) + instead of being duplicated in the joint skeleton. + + Options are matched to the skeleton's ``components`` by likelihood class name and + emitted in that order -- the positional list ``MultiGaussianLikelihood`` zips + against ``components``. Theory blocks are unioned across members via + ``recursive_update``, then the skeleton's own theory is applied last as the + joint-level override. The merge is **last-wins, so member order is significant**: + a coherent joint analysis is expected to carry coherent member precision (we do + not reconcile conflicting accuracy keys). Because the members are loaded through + ``_load_template``, a folder override on a member (``templates/.yaml``) + flows in here too -- one override reaches both the standalone member preset and + this joint preset, keeping e.g. an imprint and its fit consistent by construction. + """ + member_infos = [ + _load_template(PRESETS[m]["template"], defaults_dir=defaults_dir) for m in members + ] + options_by_class = {} + for minfo in member_infos: + for cls, opts in minfo.get("likelihood", {}).items(): + options_by_class[cls] = opts or {} + + _, mgl = next(iter(info["likelihood"].items())) + components = mgl["components"] + missing = [c for c in components if c not in options_by_class] + if missing: + raise ValueError( + f"preset members {members} provide no options for component(s) {missing}; " + f"members expose: {sorted(options_by_class)}" + ) + mgl["options"] = [options_by_class[c] for c in components] + + composed = {} + for minfo in member_infos: + composed = recursive_update(composed, minfo.get("theory", {})) + info["theory"] = recursive_update(composed, info.get("theory", {})) + + +def _apply_theory(info, theory, defaults_dir=None): + """Overlay the Fiducial map's theory fragment (``theory.yaml``) for ``theory``. + + The skeleton template owns the preset-specific precision ``extra_args``; this + layers the neutrino-sector ``extra_args`` on top via cobaya's ``recursive_update`` + (last wins). The neutrino baseline is wholesale-replaceable per-file: a folder + ``theory.yaml`` supersedes the bundled one, so a different neutrino setup (e.g. + the ISO normal hierarchy) never collides with the packaged single-massive keys. + + For ``classy``, drops the camb Boltzmann block (its precision ``extra_args`` do + not translate) while preserving non-Boltzmann theory entries (e.g. + ``mflike.BandpowerForeground``), and swaps the camb-native ``mnu`` param for the + classy-native ``m_ncdm`` carried in ``theory.yaml``. + """ + block = load_theory(defaults_dir).get(theory, {}) + if theory == "classy": + info["theory"].pop("camb", None) + info["theory"].setdefault("classy", {"stop_at_error": True}) + info["params"].pop("mnu", None) # classy uses m_ncdm (from theory.yaml) + for name, pspec in (block.get("params") or {}).items(): + info["params"][name] = pspec + info["theory"][theory] = recursive_update( + info["theory"].get(theory, {}), {"extra_args": block.get("extra_args", {})} + ) + + +class Session: + """A wired Cobaya model plus its role aliases and Fiducial map. + + Exposes the model's Likelihood/Theory members by role (``.mflike``, + ``.lensing``, ``.foreground``, ``.cosmo``), the ``info`` dict it was built + from, and the fiducial ``params`` dict. ``run()`` samples Python-native via + ``cobaya.run``. + """ + + def __init__(self, info, model): + self.info = info + self.model = model + self.fiducial = info["params"] + roles = resolve_aliases(model) + self.mflike = roles.mflike + self.lensing = roles.lensing + self.foreground = roles.foreground + self.cosmo = roles.cosmo + + def loglike(self, point=None): + """Total log-likelihood at ``point`` (default: the fiducial point).""" + loglikes, _ = self.model.loglikes(point or {}) + return float(sum(loglikes)) + + def run(self, **kwargs): + """Run the preset with Cobaya's Python API; forwards to ``cobaya.run``.""" + from cobaya import run + + return run(self.info, **kwargs) + + +def quickstart( + preset, *, sample=None, theory="camb", packages_path=None, defaults_dir=None +): + """Build a ready-to-use :class:`Session` for ``preset``. + + ``sample`` lists the dual parameters to vary (default: all fixed, ready to + evaluate at the fiducial point). ``theory`` selects the Boltzmann solver + (``"camb"`` or ``"classy"``). ``packages_path`` overrides + Cobaya's default location for installed likelihood data. ``defaults_dir`` + optionally points at a directory of override ``.yaml`` files (per-file + fallback to the bundled defaults). A relative ``defaults_dir`` is resolved + against the process working directory. + """ + from cobaya.model import get_model + from cobaya.tools import resolve_packages_path + + info = build_info(preset, sample=sample, theory=theory, defaults_dir=defaults_dir) + info["packages_path"] = packages_path or resolve_packages_path() + return Session(info, get_model(info)) diff --git a/soliket/presets/defaults/cosmo.yaml b/soliket/presets/defaults/cosmo.yaml new file mode 100644 index 00000000..4fb28e4f --- /dev/null +++ b/soliket/presets/defaults/cosmo.yaml @@ -0,0 +1,46 @@ +# SOLikeT Fiducial map — cosmology group. +# One file per top-level group; the loader keys the group off the filename. +# A "dual" parameter (carrying a `prior`) is sampled only when its name is passed +# to load_params(sample=[...]); otherwise it collapses to `value: `, +# keeping `latex`/`drop`/`renames`. CAMB-first; `renames:` keep CLASS recognisable. +H0: + prior: {min: 40, max: 100} + ref: {dist: norm, loc: 67.7, scale: 0.1} + latex: 'H_0' +logA: + drop: true + prior: {min: 1.6, max: 4.0} + ref: {dist: norm, loc: 3.05, scale: 0.001} + latex: '\log(10^{10} A_\mathrm{s})' +As: + value: 'lambda logA: 1e-10*np.exp(logA)' + latex: 'A_\mathrm{s}' +ombh2: + prior: {min: 0.022, max: 0.023} + ref: {dist: norm, loc: 0.0224, scale: 0.0001} + latex: '\Omega_\mathrm{b} h^2' +omch2: + prior: {min: 0.09, max: 0.15} + ref: {dist: norm, loc: 0.1202, scale: 0.001} + latex: '\Omega_c h^2' +ns: + prior: {min: 0.9, max: 1.1} + ref: {dist: norm, loc: 0.9649, scale: 0.001} + latex: 'n_s' +tau: + prior: {dist: norm, loc: 0.0544, scale: 0.0073} + ref: {dist: norm, loc: 0.0544, scale: 0.0073} + proposal: 0.0073 + latex: '\tau_\mathrm{reio}' +Alens: + value: 1.0 + latex: 'A_\mathrm{lens}' +mnu: + value: 0.06 + latex: '\sum m_\nu' +omega_de: {derived: true, latex: '\Omega_\Lambda'} +omegam: {derived: true, latex: '\Omega_\mathrm{m}'} +omegamh2: + derived: 'lambda omegam, H0: omegam*(H0/100)**2' + latex: '\Omega_\mathrm{m} h^2' +sigma8: {derived: true, latex: '\sigma_8'} diff --git a/soliket/presets/defaults/foreground.yaml b/soliket/presets/defaults/foreground.yaml new file mode 100644 index 00000000..589669a9 --- /dev/null +++ b/soliket/presets/defaults/foreground.yaml @@ -0,0 +1,23 @@ +# SOLikeT Fiducial map — foreground group. +a_tSZ: {prior: {min: 1.5, max: 5}, ref: 3.30, proposal: 0.05, latex: 'a_\mathrm{tSZ}'} +a_kSZ: {prior: {min: 0.0, max: 10}, ref: 1.60, proposal: 0.1, latex: 'a_\mathrm{kSZ}'} +a_p: {prior: {min: 2, max: 12}, ref: 6.90, proposal: 0.075, latex: 'a_p'} +beta_p: {prior: {min: 1, max: 4.0}, ref: 2.20, proposal: 0.03, latex: '\beta_p'} +a_c: {prior: {min: 0, max: 10}, ref: 4.90, proposal: 0.12, latex: 'a_c'} +beta_c: {prior: {min: 1.0, max: 4.0}, ref: 2.20, proposal: 0.03, latex: '\beta_c'} +a_s: {prior: {min: 1.0, max: 7}, ref: 3.10, proposal: 0.01, latex: 'a_s'} +a_gtt: {prior: {min: 0.0, max: 10}, ref: 2.80, proposal: 0.14, latex: 'a_\mathrm{dust}^\mathrm{TT}'} +a_gte: {prior: {min: 0, max: 11}, ref: 0.1, proposal: 0.007, latex: 'a_\mathrm{dust}^\mathrm{TE}'} +a_gee: {prior: {min: 0.0, max: 5.0}, ref: 0.1, proposal: 0.0006, latex: 'a_\mathrm{dust}^\mathrm{EE}'} +a_psee: {prior: {min: 0, max: 1}, ref: 0, proposal: 0.05, latex: 'a_\mathrm{ps}^\mathrm{EE}'} +a_pste: {prior: {min: -1, max: 1}, ref: 0, proposal: 0.05, latex: 'a_\mathrm{ps}^\mathrm{TE}'} +xi: {prior: {min: 0, max: 0.2}, ref: 0.1, proposal: 0.05, latex: '\xi'} +alpha_tSZ: {prior: {min: -5.0, max: 5.0}, ref: 0, proposal: 0.1, latex: '\alpha_\mathrm{tSZ}'} +beta_s: {prior: {min: -3.6, max: -1.5}, ref: -2.5, proposal: 0.2, latex: '\beta_s'} +T_d: {value: 9.7, latex: 'T_d'} +T_effd: {value: 19.6, latex: 'T_{\mathrm{dust},\mathrm{eff}}'} +beta_d: {value: 1.5, latex: '\beta_\mathrm{dust}'} +alpha_s: {value: 1.0, latex: '\alpha_s'} +alpha_p: {value: 1.0, latex: '\alpha_p'} +alpha_dT: {value: -0.6, latex: '\alpha_{\mathrm{dust},T}'} +alpha_dE: {value: -0.4, latex: '\alpha_{\mathrm{dust},E}'} diff --git a/soliket/presets/defaults/systematics.yaml b/soliket/presets/defaults/systematics.yaml new file mode 100644 index 00000000..f04a6703 --- /dev/null +++ b/soliket/presets/defaults/systematics.yaml @@ -0,0 +1,17 @@ +# SOLikeT Fiducial map — systematics group. +bandint_shift_LAT_93: {prior: {dist: norm, loc: 0.0, scale: 1.0}, ref: 0.0, proposal: 0.1, latex: '\Delta_{\rm band}^{93}'} +bandint_shift_LAT_145: {prior: {dist: norm, loc: 0.0, scale: 1.0}, ref: 0.0, proposal: 0.1, latex: '\Delta_{\rm band}^{145}'} +bandint_shift_LAT_225: {prior: {dist: norm, loc: 0.0, scale: 1.0}, ref: 0.0, proposal: 0.1, latex: '\Delta_{\rm band}^{225}'} +cal_LAT_93: {prior: {dist: norm, loc: 1.0, scale: 0.01}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}^{93}'} +cal_LAT_145: {prior: {dist: norm, loc: 1.0, scale: 0.01}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}^{145}'} +cal_LAT_225: {prior: {dist: norm, loc: 1.0, scale: 0.01}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}^{225}'} +calT_LAT_93: {value: 1, latex: '\mathrm{Cal}_{\rm T}^{93}'} +calE_LAT_93: {prior: {min: 0.9, max: 1.1}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}_{\rm E}^{93}'} +calT_LAT_145: {value: 1, latex: '\mathrm{Cal}_{\rm T}^{145}'} +calE_LAT_145: {prior: {min: 0.9, max: 1.1}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}_{\rm E}^{145}'} +calT_LAT_225: {value: 1, latex: '\mathrm{Cal}_{\rm T}^{225}'} +calE_LAT_225: {prior: {min: 0.9, max: 1.1}, ref: 1, proposal: 0.001, latex: '\mathrm{Cal}_{\rm E}^{225}'} +calG_all: {value: 1, latex: '\mathrm{Cal}_{\rm G}^{\rm All}'} +alpha_LAT_93: {value: 0, latex: '\alpha^{93}'} +alpha_LAT_145: {value: 0, latex: '\alpha^{145}'} +alpha_LAT_225: {value: 0, latex: '\alpha^{225}'} diff --git a/soliket/presets/defaults/theory.yaml b/soliket/presets/defaults/theory.yaml new file mode 100644 index 00000000..50e623b5 --- /dev/null +++ b/soliket/presets/defaults/theory.yaml @@ -0,0 +1,23 @@ +# SOLikeT Fiducial map — theory group (the neutrino sector). +# Split by KIND from the param groups: this file carries the `theory` fragment +# (Boltzmann `extra_args`) of the neutrino setup, keyed by code. It composes with +# cosmo.yaml's `mnu` at runtime — together they are the "neutrino group". +# build_info overlays the selected code's `extra_args` onto the preset skeleton's +# theory block (recursive_update; the skeleton owns the precision extra_args). +# Folder-overridable like the param groups: an analysis (e.g. ISO normal +# hierarchy) ships its own theory.yaml that REPLACES this file wholesale, so the +# single-massive keys below never collide with a different neutrino setup. +# +# Single massive neutrino, Neff = 3.044 (cobaya `one_heavy_planck`). +camb: + extra_args: + num_massive_neutrinos: 1 + nnu: 3.044 +classy: + # classy's neutrino mass param is `m_ncdm`, not camb's `mnu`; carried here + # (with the `mnu` alias) because it is a code-specific spelling of the same mass. + extra_args: + N_ncdm: 1 + N_ur: 2.0328 + params: + m_ncdm: {value: 0.06, renames: mnu} diff --git a/soliket/presets/templates/lensing.yaml b/soliket/presets/templates/lensing.yaml new file mode 100644 index 00000000..851bb0bd --- /dev/null +++ b/soliket/presets/templates/lensing.yaml @@ -0,0 +1,12 @@ +# Cobaya info template for the `lensing` preset (SOLikeT CMB lensing). +# Params are supplied separately by the loader (cosmo group only). +likelihood: + soliket.LensingLikelihood: + theory_lmax: 5000 +theory: + camb: + extra_args: + kmax: 0.9 + stop_at_error: true +sampler: + evaluate: diff --git a/soliket/presets/templates/mflike.yaml b/soliket/presets/templates/mflike.yaml new file mode 100644 index 00000000..d6ab0563 --- /dev/null +++ b/soliket/presets/templates/mflike.yaml @@ -0,0 +1,18 @@ +# Cobaya info template for the `mflike` preset (MFLike TT/TE/EE). +# Params are supplied separately by the loader from the Fiducial map. +likelihood: + mflike.TTTEEE: + input_file: LAT_simu_sacc_00044.fits + cov_Bbl_file: data_sacc_w_covar_and_Bbl.fits +theory: + camb: + extra_args: + lens_potential_accuracy: 1 + WantTransfer: true + Transfer.high_precision: true + Transfer.kmax: 1.2 + stop_at_error: true + mflike.BandpowerForeground: + stop_at_error: true +sampler: + evaluate: diff --git a/soliket/presets/templates/multigaussian.yaml b/soliket/presets/templates/multigaussian.yaml new file mode 100644 index 00000000..10b76b4f --- /dev/null +++ b/soliket/presets/templates/multigaussian.yaml @@ -0,0 +1,18 @@ +# Cobaya info template for the `multigaussian` preset (MFLike + lensing combined). +# Only joint-level concerns live here. Each component's options and precision come +# from its member preset template (mflike.yaml / lensing.yaml), composed by +# build_info -- so they have a single source of truth and a folder override on a +# member (templates/.yaml) reaches this preset too. The composed `options` +# list is emitted in `components` order. The `theory` block below is the joint-level +# override, applied last over the composed member theory (last wins). +likelihood: + soliket.gaussian.MultiGaussianLikelihood: + components: + - mflike.TTTEEE + - soliket.LensingLikelihood + stop_at_error: true +theory: + camb: + stop_at_error: false # a single camb failure shouldn't halt the joint run +sampler: + evaluate: diff --git a/soliket/sacc_tools.py b/soliket/sacc_tools.py new file mode 100644 index 00000000..80398cd9 --- /dev/null +++ b/soliket/sacc_tools.py @@ -0,0 +1,218 @@ +"""Dataset-creation utilities distilled from the simulation notebooks. + +General, likelihood-agnostic helpers for building SACC datasets: top-hat +bandpower windows and the Knox (Gaussian) covariance for an arbitrary set of +tracers. The per-tracer ``add_tracer``/``add_ell_cl`` calls stay in the notebooks +-- they are thin SACC-API calls that differ by tracer type and gain nothing from +wrapping. +""" + +import numpy as np + + +def top_hat_windows(ell_max, n_bins): + """Normalised top-hat bandpower windows partitioning ``[0, ell_max]``. + + Returns ``(ells, window)`` where ``ells`` are the bin-centre multipoles and + ``window`` is a :class:`sacc.BandpowerWindow` whose per-bin weights **sum to + one**, so binning a spectrum through it gives the bin *mean*. The bins partition + every multipole in ``0..ell_max`` inclusive with no gaps; when ``ell_max + 1`` is + not divisible by ``n_bins`` the bin widths differ by at most one multipole rather + than leaving the high-ell tail unbinned. + + The normalisation matters: a likelihood bins theory as ``w_bins @ cl`` (a plain + contraction, no implicit averaging), so the data stored against this window must + be in the same units the window produces. Mean-normalised weights let a dataset + store ``C_ell`` at the bin centres -- which is what the simulation notebooks + compute and plot against unbinned theory. Real datasets whose windows sum to + ``delta_ell`` instead store bandpower *sums*; both are self-consistent, and the + likelihood is agnostic as long as ``data == w_bins @ cl_true``. + """ + import sacc + + ells_win = np.arange(ell_max + 1) + segments = np.array_split(ells_win, n_bins) + ells = np.array([seg.mean() for seg in segments]) + weights = np.zeros((n_bins, len(ells_win))) + for i, seg in enumerate(segments): + weights[i, seg] = 1.0 / len(seg) + return ells, sacc.BandpowerWindow(ells_win, weights.T) + + +def gaussian_covariance(cls, ells, delta_ell, fsky): + """Knox (Gaussian) covariance of the auto/cross bandpowers of N maps. + + Parameters + ---------- + cls : ndarray, shape (n_maps, n_maps, n_ell) + Auto- and cross-spectra (including any noise on the autos), symmetric in + the first two axes. + ells : ndarray, shape (n_ell,) + Bin-centre multipoles. + delta_ell : float or ndarray, shape (n_ell,) + Bin width, broadcast against ``ells``. Pass a per-bin array when the bins + are not uniform -- Knox variance goes as ``1 / delta_ell``, so a single bin + one multipole wider than the rest is a real (few-percent) error, not a wash. + Reading the widths off the bandpower window keeps them honest: + ``(window.weight.T != 0).sum(axis=1)``. + fsky : float + Sky fraction. + + Returns + ------- + ndarray, shape (n_cross * n_ell, n_cross * n_ell) + Joint covariance over the ``n_cross = n_maps (n_maps + 1) / 2`` unique + spectra, ordered as ``(0,0), (0,1), ..., (1,1), ...``. + """ + cls = np.asarray(cls) + n_maps = cls.shape[0] + n_ell = len(ells) + pairs = [(i, j) for i in range(n_maps) for j in range(i, n_maps)] + n_cross = len(pairs) + + covar = np.zeros((n_cross, n_ell, n_cross, n_ell)) + knox_norm = delta_ell * fsky * (2 * np.asarray(ells) + 1) + for id_i, (i1, i2) in enumerate(pairs): + for id_j, (j1, j2) in enumerate(pairs): + cov = (cls[i1, j1] * cls[i2, j2] + cls[i1, j2] * cls[i2, j1]) / knox_norm + covar[id_i, :, id_j, :] = np.diag(cov) + return covar.reshape(n_cross * n_ell, n_cross * n_ell) + + +def smooth_twin_sacc(src, data_type, tracer1, tracer2, theory, *, out_path=None): + """Smooth (theory) twin of a single-spectrum SACC. + + Reuses the tracers, bandpower windows and covariance of ``src`` for the + ``(data_type, tracer1, tracer2)`` spectrum but replaces the measured bandpowers + with ``theory``. Evaluating a likelihood on the result at the cosmology + ``theory`` was computed for gives chi^2 = 0 -- the standard way to make a + noiseless twin of a real dataset (e.g. a CMB-lensing reconstruction). If + ``out_path`` is given the SACC is saved there. Returns the :class:`sacc.Sacc`. + """ + import sacc + + ell, _, cov, ind = src.get_ell_cl( + data_type, tracer1, tracer2, return_cov=True, return_ind=True + ) + windows = src.get_bandpower_windows(ind) + + out = sacc.Sacc() + for name in dict.fromkeys((tracer1, tracer2)): # de-dup, keep order + out.add_tracer_object(src.tracers[name]) + out.add_ell_cl(data_type, tracer1, tracer2, ell, np.asarray(theory), window=windows) + out.add_covariance(cov) + + if out_path is not None: + out.save_fits(str(out_path), overwrite=True) + return out + + +# Multipole/spin spelling MFLike uses in SACC: T is a spin-0 (s0) map, E/B are +# spin-2 (s2); the data_type code spells each field as 0 (T), e (E), b (B). +_MAP_TYPE = {"T": "0", "E": "e", "B": "b"} +_POLS = ("T", "E", "B") + + +def _bin_modified_theory(mflike, dls, fg_totals, params): + """Bin the CMB+foreground+systematics theory through MFLike's bandpower windows. + + Returns ``(ps_dic, ps_vec)``: ``ps_dic[f"{t1}x{t2}"][pol]`` holds the binned + bandpowers per frequency pair and polarisation (``"tt"``/``"te"``/``"ee"``) + plus ``"lbin"``; ``ps_vec`` is the flat data vector in MFLike's own ordering. + """ + dls_cut = {s: dls[s][mflike.l_bpws] for s in mflike.lcuts} + obs = mflike.get_modified_theory(dls_cut, fg_totals, **params) + + ps_vec = np.zeros_like(mflike.data_vec) + ps_dic = {} + for m in mflike.spec_meta: + pol, ids, window = m["pol"], m["ids"], m["bpw"] + key = f"{m['t1']}x{m['t2']}" + ps_dic.setdefault(key, {"lbin": m["leff"]}) + t1, t2 = (m["t2"], m["t1"]) if m["hasYX_xsp"] else (m["t1"], m["t2"]) + spec = obs[pol, t1, t2] + for i, nonzero, weights in zip(ids, window.nonzeros, window.sliced_weights): + ps_vec[i] = weights @ spec[nonzero] + ps_dic[key][pol] = ps_vec[ids] + return ps_dic, ps_vec + + +def smooth_mflike_sacc(mflike, dls, fg_totals, params, *, out_path=None, beam_lmax=10000): + """Build a smooth (theory) MFLike data SACC and return it. + + Bins the CMB + foreground + systematics theory through MFLike's own bandpower + windows and writes one ``NuMap`` tracer per ``(frequency, spin)`` channel plus + an ``add_ell_cl`` per cross-spectrum -- the ``input_file`` format MFLike reads. + Evaluating the likelihood on it at the same fiducial gives chi^2 = 0. + + The intricate per-frequency plumbing is MFLike-specific, so this stays a helper + rather than living in the notebook; it takes the concrete handles (not a + ``Session``) so the caller keeps the model in view: + + Parameters + ---------- + mflike : an evaluated MFLike likelihood (e.g. ``resolve_aliases(model).mflike``), + exposing ``spec_meta``, ``bands``, ``l_bpws``, ``lcuts``, + ``get_modified_theory`` and ``data_vec``. + dls : the lensed CMB :math:`D_\\ell` dict, ``model.provider.get_Cl(ell_factor=True)``. + fg_totals : the foreground bandpowers, the foreground theory's ``get_fg_totals()``. + params : flat ``{name: value}`` of the cosmo + foreground + systematics fiducial, + forwarded to ``get_modified_theory`` for calibrations and bandpass shifts. + out_path : if given, save the SACC there as FITS. + beam_lmax : length of the unit beam attached to each tracer. + + Notes + ----- + The covariance and bandpower-window (Bbl) matrices are *not* written here; + MFLike reads them separately via ``cov_Bbl_file``, so reuse the shipped one. + Cross-frequency ``ET`` reuses the pair's ``TE`` and any B-mode spectrum is + written as zeros; MFLike selects only the requested TT/TE/ET/EE within its + scale cuts, so these extras are ignored. + """ + import sacc + + ps_dic, _ = _bin_modified_theory(mflike, dls, fg_totals, params) + freqs = sorted( + {t for key in ps_dic for t in key.split("x")}, + key=lambda name: int(name.split("_")[1]), + ) + + s = sacc.Sacc() + beam = {"ell": np.arange(beam_lmax), "beam": np.ones(beam_lmax)} + for freq in freqs: + for spin, quantity in (("s0", "cmb_temperature"), ("s2", "cmb_polarization")): + band = mflike.bands[f"{freq}_{spin}"] + s.add_tracer( + "NuMap", + f"{freq}_{spin}", + quantity=quantity, + spin=0 if spin == "s0" else 2, + nu=band["nu"], + bandpass=band["bandpass"], + **beam, + ) + + for ia, fa in enumerate(freqs): + for ib, fb in enumerate(freqs): + if ia > ib: + continue + for ipa, pa in enumerate(_POLS): + for pb in _POLS[ipa:] if fa == fb else _POLS: + ta = f"{fa}_s0" if pa == "T" else f"{fa}_s2" + tb = f"{fb}_s0" if pb == "T" else f"{fb}_s2" + cl_type = "cl_" + ( + _MAP_TYPE[pb] + _MAP_TYPE[pa] + if pb == "T" + else _MAP_TYPE[pa] + _MAP_TYPE[pb] + ) + pair = ps_dic[f"{fa}x{fb}"] + lbin = pair["lbin"] + values = pair.get( + (pa + pb).lower(), + pair.get((pb + pa).lower(), np.zeros(len(lbin))), + ) + s.add_ell_cl(cl_type, ta, tb, lbin, values) + + if out_path is not None: + s.save_fits(str(out_path), overwrite=True) + return s diff --git a/tests/likelihood_refs.yaml b/tests/likelihood_refs.yaml index 35cce91f..9a8c1673 100644 --- a/tests/likelihood_refs.yaml +++ b/tests/likelihood_refs.yaml @@ -68,7 +68,7 @@ shearkappa_ia_nla_noevo: rtol: *rtol atol: *atol shearkappa_ia_nla: - value: -114145.55021412153 + value: -111630.58132304 rtol: *rtol atol: *atol shearkappa_ia_perbin: diff --git a/tests/test_ccl_tracers_like.py b/tests/test_ccl_tracers_like.py index 540a43a5..34db8330 100644 --- a/tests/test_ccl_tracers_like.py +++ b/tests/test_ccl_tracers_like.py @@ -470,6 +470,33 @@ def test_get_ia_bias_variants(): assert isinstance(res3, tuple) +def test_check_buildable_tracers_rejects_unbuildable_allowed_quantity(): + """A subclass allowing a quantity _get_tracer cannot build fails at init-time + validation, regardless of the data — not silently at first evaluation.""" + import logging + + from cobaya.log import LoggedError + + from soliket.ccl_tracers import ShearKappaLikelihood + + lk = ShearKappaLikelihood.__new__(ShearKappaLikelihood) + lk.log = logging.getLogger("test_buildable") + lk._allowable_tracers = ["cmb_convergence", "not_a_real_quantity"] + with pytest.raises(LoggedError): + lk._check_buildable_tracers() + + +def test_check_buildable_tracers_accepts_supported_quantities(): + import logging + + from soliket.ccl_tracers import ShearKappaLikelihood + + lk = ShearKappaLikelihood.__new__(ShearKappaLikelihood) + lk.log = logging.getLogger("test_buildable") + lk._allowable_tracers = ["cmb_convergence", "galaxy_shear"] + lk._check_buildable_tracers() # must not raise + + # --- merged small cross-correlation unit tests (previously in separate files) --- def _make_fake_sacc_for_merging(): class Tracer: @@ -490,6 +517,9 @@ def __init__(self): def get_tracer_combinations(self): return [("g1", "k"), ("k", "g1")] + def get_data_types(self, tracers=None): + return ["cl_0e"] # one spectrum per pair, as get_binning requires + def indices(self, tracers=None): return [0] @@ -574,3 +604,52 @@ def test_get_tracer_both_types_merged(): t_shear = lk._get_tracer(ccl, cosmo, "g1", {}) assert t_shear is not None assert hasattr(t_shear, "dndz") or isinstance(t_shear, str) + + +def test_get_binning_rejects_multi_data_type_tracer_pair(): + """A tracer pair carrying several data types must be rejected in get_binning. + + The window is looked up by tracers alone (shear cross-spectra are cl_0e/cl_e0, + not cl_00), so a multi-data-type pair would yield a window spanning all of them. + The guard belongs in the shared lookup: _get_unbinned_theory reads the ell + support from here too, and would otherwise run Limber on the doubled grid + before _get_theory's shape check noticed. + """ + import sacc + + from soliket.ccl_tracers import ShearKappaLikelihood + + s = sacc.Sacc() + s.add_tracer("Misc", "gs_x", quantity="galaxy_shear", spin=2) + s.add_tracer("Misc", "ck_y", quantity="cmb_convergence", spin=0) + support = np.arange(2, 8) + bpw = sacc.BandpowerWindow(support, np.ones((len(support), 2)) / len(support)) + ell = np.array([3.0, 6.0]) + for dtype in ("cl_0e", "cl_00"): # same tracer pair, two data types + s.add_ell_cl(dtype, "gs_x", "ck_y", ell, np.zeros(2), window=bpw) + + lk = ShearKappaLikelihood.__new__(ShearKappaLikelihood) + lk.sacc_data = s + + with pytest.raises(ValueError, match="carry data types"): + lk.get_binning(("gs_x", "ck_y")) + + +def test_get_binning_accepts_single_data_type_pair(): + import sacc + + from soliket.ccl_tracers import ShearKappaLikelihood + + s = sacc.Sacc() + s.add_tracer("Misc", "gs_x", quantity="galaxy_shear", spin=2) + s.add_tracer("Misc", "ck_y", quantity="cmb_convergence", spin=0) + support = np.arange(2, 8) + bpw = sacc.BandpowerWindow(support, np.ones((len(support), 2)) / len(support)) + s.add_ell_cl("cl_0e", "gs_x", "ck_y", np.array([3.0, 6.0]), np.zeros(2), window=bpw) + + lk = ShearKappaLikelihood.__new__(ShearKappaLikelihood) + lk.sacc_data = s + + ells_theory, w_bins = lk.get_binning(("gs_x", "ck_y")) + np.testing.assert_array_equal(ells_theory, support) + assert w_bins.shape == (2, len(support)) diff --git a/tests/test_cross_covariance.py b/tests/test_cross_covariance.py new file mode 100644 index 00000000..baf0b755 --- /dev/null +++ b/tests/test_cross_covariance.py @@ -0,0 +1,620 @@ +"""Tests for soliket.cross_covariance kernels. + +The kernels are pure: they take the CAMB lensing derivative and fiducial spectra +as arrays and return covariance blocks. These structural tests pin shape, +fsky-scaling and spectrum-scaling without a (slow) CAMB recompute; an end-to-end +regression against the committed reference products lives separately and is +gated on data availability. +""" + +from types import SimpleNamespace + +import numpy as np + +from soliket.cross_covariance import ( + camb_lensing_derivatives, + cmb_combs_from_spec_meta, + cmb_lensing_block, + lensing_induced_block, + lensing_induced_cov, + n1_crosscov_block, + shear_kappa_block, +) + + +def _tiny_mflike_sacc(): + """A minimal MFLike-like SACC: one TT spectrum with bandpower windows.""" + import sacc + + s = sacc.Sacc() + s.add_tracer("Misc", "LAT_93_s0", quantity="cmb_temperature", spin=0) + support = np.arange(2, 30) + n_bins = 3 + weight = np.zeros((len(support), n_bins)) + centers = [] + for b, idx in enumerate(np.array_split(np.arange(len(support)), n_bins)): + weight[idx, b] = 1.0 / len(idx) + centers.append(support[idx].mean()) + window = sacc.BandpowerWindow(support, weight) + s.add_ell_cl( + "cl_00", + "LAT_93_s0", + "LAT_93_s0", + np.array(centers), + np.zeros(n_bins), + window=window, + ) + s.metadata["f_sky_LAT"] = 0.4 + s.metadata["cosmo_params"] = repr( + { + "cosmomc_theta": 0.0104, + "logA": 3.05, + "ombh2": 0.0224, + "omch2": 0.1202, + "ns": 0.9649, + "Alens": 1.0, + "tau": 0.0544, + } + ) + s.metadata["accuracy_params"] = repr({}) + s.metadata["lmax"] = 60 + return s + + +def test_cmb_lensing_crosscov_runs_end_to_end_on_tiny_data(): + """The full glue (extract -> CAMB derivative -> kernel) at small lmax. + + Confirms the code path works without the full-accuracy (multi-GB) derivative; + exact reproduction of the published reference is a separate, heavy check. + """ + from soliket.cross_covariance import cmb_combs_from_spec_meta, cmb_lensing_crosscov + + lmax_kk = 25 + binning = np.zeros((2, lmax_kk)) + binning[0, 2:12] = 0.1 + binning[1, 12:lmax_kk] = 0.1 + lensing = SimpleNamespace( + binning_matrix=binning, + provider=SimpleNamespace( + get_Cl=lambda ell_factor=True: {"pp": np.ones(lmax_kk) * 1e-8} + ), + ) + + sacc_data = _tiny_mflike_sacc() + combs = cmb_combs_from_spec_meta(_tiny_spec_meta(sacc_data)) + block = cmb_lensing_crosscov(sacc_data, lensing, combs) + + assert block.shape == (3, 2) # 3 CMB bins x 2 kappa bins + assert np.all(np.isfinite(block)) + + +def test_shared_derivatives_match_per_block_computation(): + """A precomputed derivative bundle must reproduce the per-block CAMB run. + + The joint covariance shares one ``camb_lensing_derivatives`` run across blocks + via ``derivatives=``; passing the bundle must give bit-identical results to + letting each builder compute its own (the redundant-CAMB path). + """ + from soliket.cross_covariance import ( + camb_lensing_derivatives_from_sacc, + cmb_combs_from_spec_meta, + cmb_lensing_crosscov, + lensing_induced_cov, + ) + + lmax_kk = 25 + binning = np.zeros((2, lmax_kk)) + binning[0, 2:12] = 0.1 + binning[1, 12:lmax_kk] = 0.1 + lensing = SimpleNamespace( + binning_matrix=binning, + provider=SimpleNamespace( + get_Cl=lambda ell_factor=True: {"pp": np.ones(lmax_kk) * 1e-8} + ), + ) + + sacc_data = _tiny_mflike_sacc() + combs = cmb_combs_from_spec_meta(_tiny_spec_meta(sacc_data)) + derivs = camb_lensing_derivatives_from_sacc(sacc_data) + + np.testing.assert_array_equal( + cmb_lensing_crosscov(sacc_data, lensing, combs, derivatives=derivs), + cmb_lensing_crosscov(sacc_data, lensing, combs), + ) + np.testing.assert_array_equal( + lensing_induced_cov(sacc_data, combs, derivatives=derivs), + lensing_induced_cov(sacc_data, combs), + ) + + +def _tiny_spec_meta(sacc_data): + """A one-TT-spectrum ``spec_meta`` for ``_tiny_mflike_sacc``, in mflike's shape + (``pol``, ``hasYX_xsp``, ``t1``, ``t2``, ``bpw``, ``leff``, ``ids``).""" + ell, _, ind = sacc_data.get_ell_cl("cl_00", "LAT_93_s0", "LAT_93_s0", return_ind=True) + bpw = sacc_data.get_bandpower_windows(ind) + return [ + { + "pol": "tt", + "hasYX_xsp": False, + "t1": "LAT_93", + "t2": "LAT_93", + "bpw": bpw, + "leff": ell, + "ids": np.arange(len(ell)), + } + ] + + +def _mflike_ids(spec_meta): + """The per-row bandpower identities for a ``spec_meta``, matching + ``gaussian.bandpower_ids`` and ``from_cmb_lensing``'s row labels.""" + return [ + (m["pol"], bool(m["hasYX_xsp"]), (m["t1"], m["t2"]), float(leff)) + for m in spec_meta + for leff in np.asarray(m["leff"]) + ] + + +def test_from_cmb_lensing_roundtrips_into_multigaussian(tmp_path): + """``CrossCov.from_cmb_lensing`` must produce a file that loads back usably. + + The cross block is keyed by the components' real ``GaussianData`` names (else + ``MultiGaussianData`` silently drops it), and the component auto-covariances are + carried alongside it so the saved file is a self-contained, non-singular joint + covariance. Every block carries bandpower ids, so it realigns to each component's + data by identity on load. + """ + from soliket.gaussian.gaussian_data import ( + CrossCov, + GaussianData, + MultiGaussianData, + ) + + sacc_data = _tiny_mflike_sacc() + sacc_data.save_fits(str(tmp_path / "mflike_cov.fits"), overwrite=True) + spec_meta = _tiny_spec_meta(sacc_data) + + n_cmb, n_kk = 3, 2 + le_ids = [("pp", ("kappa", "kappa"), float(i)) for i in range(n_kk)] + # the assembled components carry the same ids the saved block is labelled with + mf_data = GaussianData( + "mflike", + np.arange(n_cmb), + np.zeros(n_cmb), + np.eye(n_cmb), + ids=_mflike_ids(spec_meta), + ) + le_data = GaussianData( + "CMB Lensing", + np.arange(n_kk), + np.zeros(n_kk), + np.eye(n_kk) * 1e-15, + ids=le_ids, + ) + + lmax_kk = 25 + binning = np.zeros((n_kk, lmax_kk)) + binning[0, 2:12] = 0.1 + binning[1, 12:lmax_kk] = 0.1 + lensing = SimpleNamespace( + binning_matrix=binning, + provider=SimpleNamespace( + get_Cl=lambda ell_factor=True: {"pp": np.ones(lmax_kk) * 1e-8} + ), + _get_gauss_data=lambda: le_data, + ) + mflike = SimpleNamespace( + data_folder=str(tmp_path), + cov_Bbl_file="mflike_cov.fits", + input_file="mflike_cov.fits", + spec_meta=spec_meta, + _get_gauss_data=lambda: mf_data, + ) + xcov = CrossCov.from_cmb_lensing(mflike, lensing) + + # the off-diagonal block plus both auto-covariances, keyed by the real names + assert ("mflike", "CMB Lensing") in xcov + assert xcov[("mflike", "CMB Lensing")].shape == (n_cmb, n_kk) + np.testing.assert_array_equal(xcov[("mflike", "mflike")], mf_data.cov) + np.testing.assert_array_equal(xcov[("CMB Lensing", "CMB Lensing")], le_data.cov) + + # round-trips through SACC and into a non-singular joint covariance + out = tmp_path / "xcov.fits" + xcov.save(str(out)) + multi = MultiGaussianData([mf_data, le_data], CrossCov.load(str(out))) + assert np.all(np.isfinite(multi.inv_cov)) + + +def test_from_cmb_lensing_trims_lensing_axis_to_used_bandpowers(tmp_path): + """The cross block is computed on the full kappa range, but the lensing + likelihood may keep only the bins surviving its scale cuts. ``from_cmb_lensing`` + trims the columns to those kept bins (via the lensing data's ``indices``) and + labels them with the kept ids, so the cross block and the lensing auto share one + column order and the joint covariance assembles cleanly. + """ + from soliket.gaussian.gaussian_data import ( + CrossCov, + GaussianData, + MultiGaussianData, + ) + + sacc_data = _tiny_mflike_sacc() # 3 CMB bandpowers + sacc_data.save_fits(str(tmp_path / "mflike_cov.fits"), overwrite=True) + spec_meta = _tiny_spec_meta(sacc_data) + n_cmb = 3 + + # Lensing has 2 full kappa bins but keeps only 1 (a scale cut): ids span the + # full range, indices is the kept mask, and the data vector is the kept bin. + n_kk_full, n_kept = 2, 1 + kk_ids = [("pp", ("kappa", "kappa"), float(i)) for i in range(n_kk_full)] + kept_mask = np.array([True, False]) + mf_data = GaussianData( + "mflike", + np.arange(n_cmb), + np.zeros(n_cmb), + np.eye(n_cmb), + ids=_mflike_ids(spec_meta), + ) + le_data = GaussianData( + "CMB Lensing", + np.arange(n_kept), + np.zeros(n_kept), + np.eye(n_kept) * 1e-15, + indices=kept_mask, + ids=kk_ids, + ) + + lmax_kk = 25 + binning = np.zeros((n_kk_full, lmax_kk)) + binning[0, 2:12] = 0.1 + binning[1, 12:lmax_kk] = 0.1 + lensing = SimpleNamespace( + binning_matrix=binning, + provider=SimpleNamespace( + get_Cl=lambda ell_factor=True: {"pp": np.ones(lmax_kk) * 1e-8} + ), + _get_gauss_data=lambda: le_data, + ) + mflike = SimpleNamespace( + data_folder=str(tmp_path), + cov_Bbl_file="mflike_cov.fits", + input_file="mflike_cov.fits", + spec_meta=spec_meta, + _get_gauss_data=lambda: mf_data, + ) + xcov = CrossCov.from_cmb_lensing(mflike, lensing) + + # the stored block is trimmed to the kept kappa bin, matching the lensing auto + assert xcov[("mflike", "CMB Lensing")].shape == (n_cmb, n_kept) + + out = tmp_path / "xcov.fits" + xcov.save(str(out)) # must not raise a broadcast error + multi = MultiGaussianData([mf_data, le_data], CrossCov.load(str(out))) + assert np.all(np.isfinite(multi.inv_cov)) + # the assembled joint covariance keeps the cross block at the kept granularity + assert multi.cov[:n_cmb, n_cmb:].shape == (n_cmb, n_kept) + + +def test_from_cmb_lensing_aligns_reordered_block_in_joint_cov(tmp_path): + # The whole point of labelling: when the data is in a DIFFERENT order than the + # block was built in, assembly must realign the cross block by identity (not + # position). Here the mflike data carries the bandpower ids in reversed order, + # so the assembled cross block rows must come out reversed relative to the block + # as built. With the old positional cross-cov this either mis-orders silently or + # raises (data has ids, block does not). + from soliket.gaussian.gaussian_data import CrossCov, GaussianData, MultiGaussianData + + sacc_data = _tiny_mflike_sacc() + sacc_data.save_fits(str(tmp_path / "mflike_cov.fits"), overwrite=True) + spec_meta = _tiny_spec_meta(sacc_data) + n_cmb, n_kk = 3, 2 + + le_ids = [("pp", ("kappa", "kappa"), float(i)) for i in range(n_kk)] + # mflike data ids REVERSED relative to the block's spec_meta build order + rev_ids = _mflike_ids(spec_meta)[::-1] + mf_data = GaussianData( + "mflike", np.arange(n_cmb), np.zeros(n_cmb), np.eye(n_cmb), ids=rev_ids + ) + le_data = GaussianData( + "CMB Lensing", + np.arange(n_kk), + np.zeros(n_kk), + np.eye(n_kk) * 1e-15, + ids=le_ids, + ) + + lmax_kk = 25 + binning = np.zeros((n_kk, lmax_kk)) + binning[0, 2:12] = 0.1 + binning[1, 12:lmax_kk] = 0.1 + lensing = SimpleNamespace( + binning_matrix=binning, + provider=SimpleNamespace( + get_Cl=lambda ell_factor=True: {"pp": np.ones(lmax_kk) * 1e-8} + ), + _get_gauss_data=lambda: le_data, + ) + mflike = SimpleNamespace( + data_folder=str(tmp_path), + cov_Bbl_file="mflike_cov.fits", + input_file="mflike_cov.fits", + spec_meta=spec_meta, + _get_gauss_data=lambda: mf_data, + ) + xcov = CrossCov.from_cmb_lensing(mflike, lensing) + block_as_built = np.array(xcov[("mflike", "CMB Lensing")]) + + multi = MultiGaussianData([mf_data, le_data], xcov) + cross = multi.cov[:n_cmb, n_cmb:] + # data is reversed vs the build order, so the assembled rows are reversed + np.testing.assert_allclose(cross, block_as_built[::-1]) + + +def test_cmb_combs_from_spec_meta_follows_spec_meta_order_and_pol(): + # The block builder driven by spec_meta yields one triple per spectrum, in + # spec_meta (= auto-cov) order, mapping pol -> CAMB row and passing the window + # through unchanged. This is what aligns the cross-cov with the auto-cov. + import sacc + + support = np.arange(2, 8) + weight = np.ones((len(support), 2)) / len(support) + bpw = sacc.BandpowerWindow(support, weight) + # deliberately TE before TT (an order no get_tracer_combinations() would invent) + spec_meta = [ + {"pol": "te", "bpw": bpw, "leff": np.array([3.0, 6.0]), "ids": np.array([0, 1])}, + {"pol": "tt", "bpw": bpw, "leff": np.array([3.0, 6.0]), "ids": np.array([2, 3])}, + ] + + combs = cmb_combs_from_spec_meta(spec_meta) + + assert [ind_camb for ind_camb, _, _ in combs] == [3, 0] # te->3, tt->0, IN order + for (_, support_out, weight_out), m in zip(combs, spec_meta): + np.testing.assert_array_equal(support_out, m["bpw"].values) + assert weight_out.shape == (m["bpw"].weight.shape[1], m["bpw"].weight.shape[0]) + + +def test_from_cmb_lensing_labels_block_by_bandpower_identity(tmp_path): + # The cross block must carry per-row/col bandpower identities so CrossCov can + # realign it to the data by identity (not by build order). Rows use mflike's + # spec_meta vocabulary (pol, hasYX_xsp, (t1, t2), leff) -- exactly what + # gaussian.bandpower_ids reconstructs -- and columns reuse the lensing data's + # own ids. Without these labels the block is silently positional. + from soliket.gaussian.gaussian_data import CrossCov, GaussianData + + sacc_data = _tiny_mflike_sacc() + sacc_data.save_fits(str(tmp_path / "mflike_cov.fits"), overwrite=True) + ell, _, ind = sacc_data.get_ell_cl("cl_00", "LAT_93_s0", "LAT_93_s0", return_ind=True) + bpw = sacc_data.get_bandpower_windows(ind) + spec_meta = [ + { + "pol": "tt", + "hasYX_xsp": False, + "t1": "LAT_93", + "t2": "LAT_93", + "bpw": bpw, + "leff": ell, + "ids": np.arange(len(ell)), + } + ] + + n_cmb, n_kk = len(ell), 2 + le_ids = [("pp", ("kappa", "kappa"), float(i)) for i in range(n_kk)] + mf_data = GaussianData("mflike", ell, np.zeros(n_cmb), np.eye(n_cmb)) + le_data = GaussianData( + "CMB Lensing", + np.arange(n_kk), + np.zeros(n_kk), + np.eye(n_kk) * 1e-15, + ids=le_ids, + ) + lmax_kk = 25 + binning = np.zeros((n_kk, lmax_kk)) + binning[0, 2:12] = 0.1 + binning[1, 12:lmax_kk] = 0.1 + lensing = SimpleNamespace( + binning_matrix=binning, + provider=SimpleNamespace( + get_Cl=lambda ell_factor=True: {"pp": np.ones(lmax_kk) * 1e-8} + ), + _get_gauss_data=lambda: le_data, + ) + mflike = SimpleNamespace( + data_folder=str(tmp_path), + cov_Bbl_file="mflike_cov.fits", + input_file="mflike_cov.fits", + spec_meta=spec_meta, + _get_gauss_data=lambda: mf_data, + ) + + xcov = CrossCov.from_cmb_lensing(mflike, lensing) + + block = xcov[("mflike", "CMB Lensing")] + assert block.shape == (n_cmb, n_kk) # rows from spec_meta, cols full kappa range + assert np.all(np.isfinite(block)) + + row_ids, col_ids = xcov._block_ids_map[("mflike", "CMB Lensing")] + expected_rows = [("tt", False, ("LAT_93", "LAT_93"), float(leff)) for leff in ell] + assert row_ids == expected_rows + assert col_ids == le_ids + + +def test_camb_lensing_derivatives_returns_consistent_shapes(): + cosmo = { + "H0": 67.7, + "ombh2": 0.0224, + "omch2": 0.1202, + "ns": 0.9649, + "tau": 0.0544, + "As": 2.1e-9, + } + lmax = 150 + + cls, clp, dCllens = camb_lensing_derivatives(cosmo, accuracy={}, lmax=lmax) + + assert cls.shape[0] == lmax + 1 + assert clp.shape[0] == lmax + 1 + assert dCllens.shape[0] == 4 # TT, EE, BB, TE + assert dCllens.shape[1] == lmax + 1 + + +def _toy_cmb_combs(): + # one CMB tracer combination (TT), 1 bandpower bin over ell support {2,3,4} + return [(0, np.array([2, 3, 4]), np.ones((1, 3)))] + + +def test_cmb_lensing_block_shape(): + L, lmax_kk = 8, 5 + rng = np.random.default_rng(0) + dCllens = rng.random((4, L, L)) + clp = rng.random(L) + 1.0 + cl_kk = rng.random(lmax_kk) + 1.0 + binning = np.zeros((2, lmax_kk)) + binning[0, 2] = 1.0 + binning[1, 3] = 1.0 + + block = cmb_lensing_block(dCllens, clp, cl_kk, 0.5, _toy_cmb_combs(), binning) + + assert block.shape == (1, 2) # (n_cmb_data, n_kk_bins) + + +def test_cmb_lensing_block_scales_inversely_with_fsky(): + L, lmax_kk = 8, 5 + rng = np.random.default_rng(1) + dCllens = rng.random((4, L, L)) + clp = rng.random(L) + 1.0 + cl_kk = rng.random(lmax_kk) + 1.0 + binning = np.zeros((2, lmax_kk)) + binning[0, 2] = 1.0 + binning[1, 3] = 1.0 + combs = _toy_cmb_combs() + + half = cmb_lensing_block(dCllens, clp, cl_kk, 0.5, combs, binning) + quarter = cmb_lensing_block(dCllens, clp, cl_kk, 0.25, combs, binning) + + np.testing.assert_allclose(quarter, 2 * half) + + +def test_cmb_lensing_block_scales_with_cl_kk_squared(): + L, lmax_kk = 8, 5 + rng = np.random.default_rng(2) + dCllens = rng.random((4, L, L)) + clp = rng.random(L) + 1.0 + cl_kk = rng.random(lmax_kk) + 1.0 + binning = np.zeros((2, lmax_kk)) + binning[0, 2] = 1.0 + binning[1, 3] = 1.0 + combs = _toy_cmb_combs() + + base = cmb_lensing_block(dCllens, clp, cl_kk, 0.5, combs, binning) + doubled = cmb_lensing_block(dCllens, clp, 2 * cl_kk, 0.5, combs, binning) + + np.testing.assert_allclose(doubled, 4 * base) + + +def _two_cmb_combs(): + return [ + (0, np.array([2, 3, 4]), np.ones((2, 3))), # TT, 2 bins + (1, np.array([3, 4, 5]), np.ones((2, 3))), # EE, 2 bins + ] + + +def test_lensing_induced_block_shape_and_symmetry(): + L = 8 + dCllens = np.random.default_rng(3).random((4, L, L)) + + cov = lensing_induced_block(dCllens, 0.5, _two_cmb_combs()) + + assert cov.shape == (4, 4) # 2 combs x 2 bins each + np.testing.assert_allclose(cov, cov.T) + + +def test_lensing_induced_block_scales_inversely_with_fsky(): + L = 8 + dCllens = np.random.default_rng(4).random((4, L, L)) + combs = _two_cmb_combs() + + half = lensing_induced_block(dCllens, 0.5, combs) + quarter = lensing_induced_block(dCllens, 0.25, combs) + + np.testing.assert_allclose(quarter, 2 * half) + + +def test_n1_crosscov_block_shape_and_fsky_scaling(): + L, n_ell, lmax_kk = 8, 6, 5 + rng = np.random.default_rng(7) + dCllens = rng.random((4, L, L)) + clp = rng.random(L) + 1.0 + n1_mat = rng.random((lmax_kk, n_ell)) + binning = np.zeros((2, lmax_kk)) + binning[0, 2], binning[1, 3] = 1.0, 1.0 + combs = [(0, np.array([2, 3, 4]), np.ones((1, 3)))] + + half = n1_crosscov_block(dCllens, clp, n1_mat, 0.5, combs, binning) + quarter = n1_crosscov_block(dCllens, clp, n1_mat, 0.25, combs, binning) + + assert half.shape == (1, 2) + np.testing.assert_allclose(quarter, 2 * half) + + +def test_n1_crosscov_block_pins_ell_weighting(): + # With uniform dCllens/clp, an N1 matrix nonzero at a single multipole j + # isolates factor[j] = 2/(2j+1)/fsky * 2pi/(j(j+1))**2, so the ratio between + # two columns pins the bespoke ell weighting exactly. + L, n_ell, lmax_kk = 10, 8, 4 + dCllens = np.ones((4, L, L)) + clp = np.ones(L) + binning = np.zeros((1, lmax_kk)) + binning[0, 1] = 1.0 + combs = [(0, np.array([2]), np.ones((1, 1)))] + + def block_at_column(j): + n1 = np.zeros((lmax_kk, n_ell)) + n1[1, j] = 1.0 + return n1_crosscov_block(dCllens, clp, n1, 0.5, combs, binning)[0, 0] + + ratio = block_at_column(2) / block_at_column(3) + expected = (2 / 5 * 2 * np.pi / (2 * 3) ** 2) / (2 / 7 * 2 * np.pi / (3 * 4) ** 2) + np.testing.assert_allclose(ratio, expected) + + +def test_shear_kappa_block_concatenates_per_tracer_cmb_lensing_blocks(): + L, lmax_lss = 8, 5 + rng = np.random.default_rng(5) + dCllens = rng.random((4, L, L)) + clp = rng.random(L) + 1.0 + cl1 = rng.random(lmax_lss) + 1.0 + cl2 = rng.random(lmax_lss) + 1.0 + bin1 = np.zeros((2, lmax_lss)) + bin1[0, 2], bin1[1, 3] = 1.0, 1.0 + bin2 = np.zeros((3, lmax_lss)) + bin2[0, 2], bin2[1, 3], bin2[2, 4] = 1.0, 1.0, 1.0 + combs = [(0, np.array([2, 3, 4]), np.ones((1, 3)))] + + block = shear_kappa_block(dCllens, clp, [cl1, cl2], [bin1, bin2], 0.5, combs) + + # one CMB datum, columns = tracer1 bins (2) + tracer2 bins (3) + assert block.shape == (1, 5) + expected = np.hstack( + [ + cmb_lensing_block(dCllens, clp, cl1, 0.5, combs, bin1), + cmb_lensing_block(dCllens, clp, cl2, 0.5, combs, bin2), + ] + ) + np.testing.assert_allclose(block, expected) + + +def test_lensing_induced_cov_honours_explicit_combs(): + # The combs= override must drive the block, so a caller can pass + # cmb_combs_from_spec_meta to align it with the MFLike auto-covariance order + # instead of the SACC's tracer-combination order (the from_cmb_lensing fix, + # applied to the other builders too). + sacc_data = _tiny_mflike_sacc() # provides f_sky_LAT metadata + fsky = float(sacc_data.metadata["f_sky_LAT"]) + L = 8 + dCllens = np.random.default_rng(0).random((4, L, L)) + combs = _two_cmb_combs() # support max 5 < L + + out = lensing_induced_cov(sacc_data, derivatives=(None, None, dCllens), combs=combs) + + np.testing.assert_allclose(out, lensing_induced_block(dCllens, fsky, combs)) + assert out.shape == (4, 4) # 2 combs x 2 bins, from the given combs not the SACC diff --git a/tests/test_crosscov.py b/tests/test_crosscov.py index 817b1012..b04191be 100644 --- a/tests/test_crosscov.py +++ b/tests/test_crosscov.py @@ -848,9 +848,7 @@ def test_mflike_style_component_assembles_to_dcov_untrimmed(): ) # Simulate mflike's mismatched, LONGER kept-mask (len nM+2 != len(ids_M)). d_M.indices = np.array([True] * nM + [False, True]) - d_L = GaussianData( - "CMBk", np.arange(nL, dtype=float), np.zeros(nL), cov_L, ids=ids_L - ) + d_L = GaussianData("CMBk", np.arange(nL, dtype=float), np.zeros(nL), cov_L, ids=ids_L) # _kept_order must use the data-vector ids, not zip-truncate the mask. assert MultiGaussianData._kept_order(d_M) == ids_M @@ -900,9 +898,7 @@ def test_save_reconciles_conflicting_same_set_orders(): cc.add_component("B", cov_BB, ids=[b0, b1]) # Cross block stored with A's axis PERMUTED -> [a2, a0, a1] (same set). perm = [2, 0, 1] - cc.add_cross_covariance( - "A", "B", cross_AB[perm, :], ids1=[a2, a0, a1], ids2=[b0, b1] - ) + cc.add_cross_covariance("A", "B", cross_AB[perm, :], ids1=[a2, a0, a1], ids2=[b0, b1]) path = os.path.join(gettempdir(), "reconcile_cross_cov.sacc.fits") cc.save(path) # must NOT raise @@ -932,9 +928,7 @@ def test_save_raises_on_genuine_set_mismatch(): cc = CrossCov() cc.add_component("A", full[:3, :3], ids=[a0, a1, a2]) cc.add_component("B", full[3:, 3:], ids=[b0, b1]) - cc.add_cross_covariance( - "A", "B", full[:3, 3:], ids1=[a0, a1, aX], ids2=[b0, b1] - ) + cc.add_cross_covariance("A", "B", full[:3, 3:], ids1=[a0, a1, aX], ids2=[b0, b1]) path = os.path.join(gettempdir(), "mismatch_cross_cov.sacc.fits") with pytest.raises(ValueError, match="different|bandpower|set"): @@ -1015,8 +1009,9 @@ def test_save_keeps_cross_only_components_when_mixed_with_add_component(): # B is cross-only: assembling with a live B falls back to B's own covariance, # and the cross block is placed by identity. cov_B = np.diag([10.0, 20.0]) - d_A = GaussianData("A", np.arange(3.0), np.zeros(3), np.diag([1.0, 2.0, 3.0]), - ids=ids_A) + d_A = GaussianData( + "A", np.arange(3.0), np.zeros(3), np.diag([1.0, 2.0, 3.0]), ids=ids_A + ) d_B = GaussianData("B", np.arange(2.0), np.zeros(2), cov_B, ids=ids_B) cov = MultiGaussianData([d_A, d_B], loaded).cov assert np.allclose(cov[3:, 3:], cov_B) # B auto from the live likelihood diff --git a/tests/test_nb.py b/tests/test_nb.py new file mode 100644 index 00000000..241ab243 --- /dev/null +++ b/tests/test_nb.py @@ -0,0 +1,52 @@ +"""Tests for the repo-only notebook helpers in notebooks/_nb.py. + +The module is not part of the shipped package, so it is loaded from its file +path rather than imported. +""" + +import importlib.util +import pathlib +from types import SimpleNamespace + +import matplotlib +import numpy as np + +matplotlib.use("Agg") + +_NB_PATH = pathlib.Path(__file__).parent.parent / "notebooks" / "_nb.py" + + +def _load_nb(): + spec = importlib.util.spec_from_file_location("_nb", _NB_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_theory_dls_reads_from_the_model_provider(): + sentinel = {"ell": [2, 3], "tt": [1.0, 2.0]} + session = SimpleNamespace( + model=SimpleNamespace( + provider=SimpleNamespace(get_Cl=lambda ell_factor=True: sentinel) + ) + ) + + assert _load_nb().theory_dls(session) is sentinel + + +def test_foreground_totals_reads_from_the_foreground_role(): + sentinel = object() + session = SimpleNamespace(foreground=SimpleNamespace(get_fg_totals=lambda: sentinel)) + + assert _load_nb().foreground_totals(session) is sentinel + + +def test_plot_dls_draws_one_line_per_requested_spectrum(): + nb = _load_nb() + ell = np.arange(2, 100) + dls = {"ell": ell, "tt": ell * 1.0, "te": ell * 0.5, "ee": ell * 0.1} + + ax = nb.plot_dls(dls, spectra=("tt", "ee")) + + assert len(ax.get_lines()) == 2 + assert ax.get_xscale() == "log" diff --git a/tests/test_presets.py b/tests/test_presets.py new file mode 100644 index 00000000..58cbe774 --- /dev/null +++ b/tests/test_presets.py @@ -0,0 +1,501 @@ +"""Tests for soliket.presets — the notebook-onboarding layer. + +The loader turns the single-source Fiducial map (a grouped spec) into a flat +Cobaya ``params`` dict. A *dual parameter* (one carrying a ``prior``) is fixed to +its central value unless it appears in the explicit ``sample`` list. +""" + +import os +from importlib import resources +from types import SimpleNamespace + +import numpy as np +import pytest +import yaml +from cobaya.theories.camb.camb import CAMB +from cobaya.tools import resolve_packages_path + +from soliket.gaussian import MultiGaussianLikelihood +from soliket.lensing import LensingLikelihood +from soliket.presets import ( + Session, + build_info, + build_params, + load_fiducial_map, + load_params, + quickstart, + resolve_aliases, +) + + +def _mflike_data_available(): + path = resolve_packages_path() + return bool(path) and os.path.isfile( + os.path.join(path, "data", "MFLike", "v0.8", "LAT_simu_sacc_00044.fits") + ) + + +def _bare(cls): + """A real-typed instance without running the heavy __init__.""" + return cls.__new__(cls) + + +def test_build_info_mflike_wires_likelihood_theory_and_params(): + info = build_info("mflike") + + assert "mflike.TTTEEE" in info["likelihood"] + assert "camb" in info["theory"] + assert "mflike.BandpowerForeground" in info["theory"] + # params come from the Fiducial map, all fixed by default + assert info["params"]["tau"]["value"] == 0.0544 + assert info["params"]["a_tSZ"]["value"] == 3.30 + + +def test_build_info_lensing_includes_only_cosmo_params(): + info = build_info("lensing") + + assert "soliket.LensingLikelihood" in info["likelihood"] + assert info["params"]["ns"]["value"] == 0.9649 + assert "a_tSZ" not in info["params"] # foreground not wired for lensing-only + + +def test_build_info_threads_sample_into_params(): + info = build_info("mflike", sample=["tau"]) + + assert "prior" in info["params"]["tau"] + assert info["params"]["ns"]["value"] == 0.9649 # unlisted stays fixed + + +def test_build_info_defaults_to_camb_theory(): + assert "camb" in build_info("mflike")["theory"] + + +def test_build_info_camb_uses_single_massive_neutrino(): + info = build_info("mflike") + ea = info["theory"]["camb"]["extra_args"] + assert ea["num_massive_neutrinos"] == 1 + assert ea["nnu"] == 3.044 + # mnu is now an ordinary cosmo param (lives in cosmo.yaml), not a bare float + assert info["params"]["mnu"]["value"] == 0.06 + # the camb precision extra_args survive the overlay + assert ea["lens_potential_accuracy"] == 1 + # the old normal-hierarchy keys are gone from the bundled preset + assert "nu_mass_eigenstates" not in ea + + +def test_build_info_neutrino_default_does_not_clobber_override(tmp_path): + # An override that pins mnu (the ISO normal-hierarchy case) must win over + # the injected single-massive default of 0.06. + (tmp_path / "cosmo.yaml").write_text( + "ns: {value: 0.9649, latex: 'n_s'}\n" + "mnu: {value: 0.12, latex: '\\\\sum m_\\\\nu'}\n" + ) + info = build_info("lensing", defaults_dir=str(tmp_path)) + assert info["params"]["mnu"]["value"] == 0.12 + + +def test_build_info_rejects_unknown_theory(): + with pytest.raises(ValueError, match="unknown theory"): + build_info("mflike", theory="cosmomc") + + +def test_build_info_classy_uses_classy_neutrino_subblock(): + info = build_info("mflike", theory="classy") + + assert "classy" in info["theory"] + assert "camb" not in info["theory"] + assert "mflike.BandpowerForeground" in info["theory"] # non-Boltzmann preserved + ea = info["theory"]["classy"]["extra_args"] + assert ea["N_ncdm"] == 1 + assert ea["N_ur"] == 2.0328 + # classy-native neutrino param, with the camb-name alias + assert info["params"]["m_ncdm"]["value"] == 0.06 + assert info["params"]["m_ncdm"]["renames"] == "mnu" + assert "mnu" not in info["params"] # mnu is camb-only; not injected under classy + + +def test_build_info_honors_defaults_dir_override(tmp_path): + (tmp_path / "cosmo.yaml").write_text("ns: {value: 0.5, latex: 'n_s'}\n") + + info = build_info("lensing", defaults_dir=str(tmp_path)) + + assert info["params"]["ns"]["value"] == 0.5 + + +def test_build_info_rejects_unknown_preset(): + with pytest.raises(ValueError, match="unknown preset"): + build_info("nope") + + +def test_session_exposes_roles_fiducial_and_loglike(check_skip_mflike): + from mflike import TTTEEE + + cosmo, mflike = CAMB.__new__(CAMB), TTTEEE.__new__(TTTEEE) + fake_model = SimpleNamespace( + components=[cosmo, mflike], + loglikes=lambda point=None: (np.array([-1.5]), {}), + ) + info = {"params": {"ns": {"value": 0.9649}}} + + s = Session(info, fake_model) + + assert s.model is fake_model + assert s.info is info + assert s.fiducial == {"ns": {"value": 0.9649}} + assert s.mflike is mflike + assert s.cosmo is cosmo + assert s.lensing is None + assert s.loglike() == -1.5 + + +@pytest.mark.skipif(not _mflike_data_available(), reason="MFLike data not installed") +@pytest.mark.parametrize( + "preset, expect_mflike, expect_lensing", + [ + ("mflike", True, False), + ("lensing", False, True), + ("multigaussian", True, True), + ], +) +def test_quickstart_builds_runnable_session( + preset, expect_mflike, expect_lensing, check_skip_mflike +): + s = quickstart(preset) + + assert s.cosmo is not None + assert (s.mflike is not None) is expect_mflike + assert (s.lensing is not None) is expect_lensing + assert np.isfinite(s.loglike()) + + +def test_resolve_aliases_finds_roles_by_class_regardless_of_order(check_skip_mflike): + from mflike import TTTEEE, BandpowerForeground + + cosmo, fg, mflike = _bare(CAMB), _bare(BandpowerForeground), _bare(TTTEEE) + # deliberately scrambled order + model = SimpleNamespace(components=[fg, mflike, cosmo]) + + roles = resolve_aliases(model) + + assert roles.mflike is mflike + assert roles.foreground is fg + assert roles.cosmo is cosmo + assert roles.lensing is None + + +def test_resolve_aliases_recurses_into_multigaussian(check_skip_mflike): + from mflike import TTTEEE + + cosmo = _bare(CAMB) + multi = _bare(MultiGaussianLikelihood) + multi.likelihoods = [_bare(TTTEEE), _bare(LensingLikelihood)] + model = SimpleNamespace(components=[cosmo, multi]) + + roles = resolve_aliases(model) + + assert roles.mflike is multi.likelihoods[0] + assert roles.lensing is multi.likelihoods[1] + assert roles.cosmo is cosmo + + +def test_load_params_all_fixed_by_default(): + params = load_params() + + # Dual params collapse to their fiducial central value... + assert params["tau"]["value"] == 0.0544 + assert params["ns"]["value"] == 0.9649 + assert params["a_tSZ"]["value"] == 3.30 + # ...always-fixed and derived params survive untouched. + assert params["T_d"]["value"] == 9.7 + assert params["omegam"]["derived"] is True + + +def test_load_params_attaches_cobaya_theory_renames(): + # Renames are sourced from Cobaya's own camb/classy tables so cosmo params + # are theory-portable: e.g. ombh2 -> omega_b under CLASS via the omegabh2 alias. + params = load_params() + + assert "omegabh2" in params["ombh2"]["renames"] + assert "omegach2" in params["omch2"]["renames"] + assert "omegal" in params["omega_de"]["renames"] + + +def test_load_params_can_restrict_to_groups(): + cosmo_only = load_params(groups=["cosmo"]) + + assert "ns" in cosmo_only + assert "a_tSZ" not in cosmo_only # foreground group excluded + assert "cal_LAT_93" not in cosmo_only # systematics group excluded + + +def test_load_params_keeps_prior_for_sampled_params(): + params = load_params(sample=["tau", "a_tSZ"]) + + assert "prior" in params["tau"] + assert "value" not in params["tau"] + assert "prior" in params["a_tSZ"] + # Unlisted dual params stay fixed. + assert params["ns"]["value"] == 0.9649 + + +def test_load_params_rejects_unsampleable_names(): + # A name that matches nothing is a typo, and silently returning a fully-fixed + # info would send an MCMC off sampling zero parameters. + with pytest.raises(ValueError, match="cannot sample"): + load_params(sample=["taau"]) + + # A real dual param, but from a group this call does not load. + with pytest.raises(ValueError, match="cannot sample"): + load_params(sample=["a_tSZ"], groups=["cosmo"]) + + # A param that exists in the group but carries no prior, so cannot be varied. + fixed = [n for n, p in load_fiducial_map()["cosmo"].items() if "prior" not in p] + if fixed: + with pytest.raises(ValueError, match="cannot sample"): + load_params(sample=[fixed[0]], groups=["cosmo"]) + + +def test_load_fiducial_map_reads_all_groups(): + from soliket.presets import load_fiducial_map + + spec = load_fiducial_map() + assert set(spec) == {"cosmo", "foreground", "systematics"} + assert spec["cosmo"]["ns"]["ref"]["loc"] == 0.9649 + + +def test_load_params_override_dir_replaces_one_group(tmp_path): + # Drop in only cosmo.yaml; foreground/systematics must still come from the package. + (tmp_path / "cosmo.yaml").write_text("ns: {value: 0.5, latex: 'n_s'}\n") + + params = load_params(defaults_dir=str(tmp_path)) + + assert params["ns"]["value"] == 0.5 # overridden file used + assert params["a_tSZ"]["value"] == 3.30 # foreground fell back to bundled + assert params["cal_LAT_93"]["value"] == 1 # systematics fell back to bundled + + +def test_load_params_rejects_non_mapping_override(tmp_path): + # A malformed hand-edited override (here: a YAML list) must fail with a clear + # error naming the file, not a cryptic AttributeError deep in build_params. + bad = tmp_path / "cosmo.yaml" + bad.write_text("- not\n- a\n- mapping\n") + + with pytest.raises(ValueError, match="cosmo.yaml"): + load_params(defaults_dir=str(tmp_path)) + + +def test_build_params_errors_on_missing_group(): + # A preset requesting a group with no params/.yaml must fail clearly, + # naming the missing group, not raise a bare KeyError. + with pytest.raises(ValueError, match="systematics"): + build_params({"cosmo": {}}, groups=["cosmo", "systematics"]) + + +def _mflike_fg_defaults(): + """Merge mflike's shipped foreground param defaults into one flat dict. + + The ``fg_*.yaml`` files are flat param dicts; ``Foreground.yaml`` carries a + top-level ``params:`` block with the always-used foreground params. + """ + merged = {} + for spec in ("TT", "TE", "EE"): + txt = resources.files("mflike").joinpath(f"fg_{spec}.yaml").read_text() + merged.update(yaml.safe_load(txt) or {}) + fg = yaml.safe_load(resources.files("mflike").joinpath("Foreground.yaml").read_text()) + merged.update((fg or {}).get("params") or {}) + return merged + + +def test_presets_foreground_matches_mflike_defaults(check_skip_mflike): + """Drift tripwire: presets foreground priors must match mflike's defaults. + + ``soliket/presets/defaults/foreground.yaml`` DUPLICATES the ``prior``, + ``proposal`` and ``latex`` values from mflike's shipped foreground defaults + (``fg_TT.yaml``, ``fg_TE.yaml``, ``fg_EE.yaml`` and the ``params:`` block of + ``Foreground.yaml``), adding an SO-specific ``ref`` that mflike does not ship. + We deliberately keep foreground explicit rather than auto-pulling from mflike, + so this test exists purely as a tripwire: if an mflike version bump changes a + default we copied, this fails loudly instead of silently shifting our priors. + + When it fails: review mflike's change and deliberately re-pin + ``soliket/presets/defaults/foreground.yaml`` to match (or, if the deviation is + an intentional SO choice, add a documented exclusion here). Do NOT just + force it green. + + Only the INTERSECTION of param names is compared (SO-specific params absent + from mflike, and mflike params absent from presets, are ignored), and only + the ``prior``/``proposal``/``latex`` fields — the presets-only ``ref`` is + ignored. + """ + from soliket.presets import load_fiducial_map + + presets_fg = load_fiducial_map()["foreground"] + mflike_fg = _mflike_fg_defaults() + + fields = ("prior", "proposal", "latex") + mismatches = [] + for name in sorted(set(presets_fg) & set(mflike_fg)): + p, m = presets_fg[name], mflike_fg[name] + for field in fields: + # Treat absence consistently: a key missing on either side reads as + # None, so "present here / absent there" surfaces as a mismatch. + pv, mv = p.get(field), m.get(field) + if pv != mv: + mismatches.append((name, field, pv, mv)) + + assert not mismatches, ( + "Presets foreground priors have DRIFTED from mflike's shipped defaults. " + "This tripwire guards against an mflike bump silently changing the " + "foreground priors we duplicated in " + "soliket/presets/defaults/foreground.yaml. Review mflike's change and " + "deliberately re-pin that file (or add a documented exclusion). " + "Mismatches (name, field, presets_value, mflike_value): " + + "; ".join( + f"{name}.{field}: presets={pv!r} mflike={mv!r}" + for name, field, pv, mv in mismatches + ) + ) + + +def test_build_info_calls_are_independent(): + # The neutrino sector now comes from theory.yaml, loaded fresh per call. + # Mutating one returned info must not leak into the next call's defaults. + info = build_info("mflike", theory="classy") + info["params"]["m_ncdm"]["value"] = 999 + info["theory"]["classy"]["extra_args"]["N_ncdm"] = 999 + + fresh = build_info("mflike", theory="classy") + assert fresh["params"]["m_ncdm"]["value"] == 0.06 + assert fresh["theory"]["classy"]["extra_args"]["N_ncdm"] == 1 + + +def test_build_info_theory_dir_override_replaces_neutrino_extra_args(tmp_path): + # ISO normal-hierarchy case: a defaults folder carrying its own theory.yaml + # REPLACES the packaged single-massive neutrino extra_args wholesale (per-file + # fallback), so the conflicting baseline keys never appear -- no merge, no + # deletion needed. + (tmp_path / "theory.yaml").write_text( + "camb:\n" + " extra_args:\n" + " num_nu_massive: 2\n" + " nu_mass_eigenstates: 2\n" + " share_delta_neff: true\n" + ) + + info = build_info("lensing", defaults_dir=str(tmp_path)) + ea = info["theory"]["camb"]["extra_args"] + + # the NH override is present... + assert ea["num_nu_massive"] == 2 + assert ea["nu_mass_eigenstates"] == 2 + # ...the packaged single-massive keys are gone (wholesale replacement)... + assert "num_massive_neutrinos" not in ea + assert "nnu" not in ea + # ...and the preset-skeleton accuracy survives the overlay. + assert ea["kmax"] == 0.9 + + +def test_build_info_theory_falls_back_to_bundled_when_dir_lacks_it(tmp_path): + # A defaults folder that overrides only cosmo must still get the packaged + # neutrino theory.yaml (per-file fallback, mirroring the param groups). + (tmp_path / "cosmo.yaml").write_text("ns: {value: 0.9649, latex: 'n_s'}\n") + + info = build_info("lensing", defaults_dir=str(tmp_path)) + ea = info["theory"]["camb"]["extra_args"] + + assert ea["num_massive_neutrinos"] == 1 + assert ea["nnu"] == 3.044 + + +def test_build_info_template_dir_override_patches_likelihood_option(tmp_path): + # A defaults folder carrying templates/.yaml overlays the packaged + # skeleton (recursive_update): the named option changes... + templates = tmp_path / "templates" + templates.mkdir() + (templates / "lensing.yaml").write_text( + "likelihood:\n soliket.LensingLikelihood:\n theory_lmax: 3000\n" + ) + + info = build_info("lensing", defaults_dir=str(tmp_path)) + like = info["likelihood"]["soliket.LensingLikelihood"] + + # the override wins... + assert like["theory_lmax"] == 3000 + # ...and the rest of the packaged skeleton is inherited (overlay, not replace). + assert info["theory"]["camb"]["extra_args"]["kmax"] == 0.9 + assert "evaluate" in info["sampler"] + + +def test_build_info_template_falls_back_to_bundled_when_dir_lacks_it(tmp_path): + # A defaults folder overriding only params (no templates/) leaves the packaged + # likelihood skeleton untouched. + (tmp_path / "cosmo.yaml").write_text("ns: {value: 0.9649, latex: 'n_s'}\n") + + info = build_info("lensing", defaults_dir=str(tmp_path)) + assert info["likelihood"]["soliket.LensingLikelihood"]["theory_lmax"] == 5000 + + +def _mgl_options(info): + mgl = info["likelihood"]["soliket.gaussian.MultiGaussianLikelihood"] + return mgl["components"], mgl["options"] + + +def test_multigaussian_composes_options_from_members_in_component_order(): + # The joint skeleton declares only `components`; build_info fills `options` + # positionally from the member presets (mflike.yaml / lensing.yaml). + info = build_info("multigaussian") + components, options = _mgl_options(info) + + assert components == ["mflike.TTTEEE", "soliket.LensingLikelihood"] + assert options[0]["input_file"] == "LAT_simu_sacc_00044.fits" # from mflike.yaml + assert options[1]["theory_lmax"] == 5000 # from lensing.yaml + + +def test_multigaussian_theory_unions_member_precision(): + # Regression: the joint camb extra_args must carry BOTH members' precision + # (mflike's Transfer.* and lensing's kmax), not silently drop either. + ea = build_info("multigaussian")["theory"]["camb"]["extra_args"] + + assert ea["kmax"] == 0.9 # from lensing.yaml + assert ea["Transfer.kmax"] == 1.2 # from mflike.yaml + assert ea["WantTransfer"] is True # from mflike.yaml + # the joint-level skeleton override is applied last (last wins) + assert build_info("multigaussian")["theory"]["camb"]["stop_at_error"] is False + # the foreground theory component rides in from the mflike member + assert "mflike.BandpowerForeground" in build_info("multigaussian")["theory"] + + +def test_member_template_override_flows_to_standalone_and_joint(tmp_path): + # A single folder override on a member template reaches BOTH the standalone + # member preset and the joint preset that composes it -- so an imprint built + # from `lensing` and a fit built from `multigaussian` stay consistent. + templates = tmp_path / "templates" + templates.mkdir() + (templates / "lensing.yaml").write_text( + "likelihood:\n soliket.LensingLikelihood:\n theory_lmax: 3000\n" + ) + + solo = build_info("lensing", defaults_dir=str(tmp_path)) + _, joint_options = _mgl_options( + build_info("multigaussian", defaults_dir=str(tmp_path)) + ) + + assert solo["likelihood"]["soliket.LensingLikelihood"]["theory_lmax"] == 3000 + assert joint_options[1]["theory_lmax"] == 3000 + + +def test_dual_param_fixed_to_central_when_not_sampled(): + spec = { + "cosmo": { + "tau": { + "prior": {"dist": "norm", "loc": 0.0544, "scale": 0.0073}, + "ref": {"dist": "norm", "loc": 0.0544, "scale": 0.0073}, + "proposal": 0.0073, + "latex": r"\tau", + } + } + } + + params = build_params(spec, sample=[]) + + assert params["tau"] == {"value": 0.0544, "latex": r"\tau"} diff --git a/tests/test_sacc_tools.py b/tests/test_sacc_tools.py new file mode 100644 index 00000000..de95d8e7 --- /dev/null +++ b/tests/test_sacc_tools.py @@ -0,0 +1,176 @@ +"""Tests for soliket.sacc_tools dataset-creation utilities.""" + +import os + +import numpy as np +import pytest +import sacc +from cobaya.tools import resolve_packages_path + +from soliket.sacc_tools import ( + gaussian_covariance, + smooth_mflike_sacc, + smooth_twin_sacc, + top_hat_windows, +) + + +def _mflike_data_available(): + path = resolve_packages_path() + return bool(path) and os.path.isfile( + os.path.join(path, "data", "MFLike", "v0.8", "LAT_simu_sacc_00044.fits") + ) + + +def test_top_hat_windows_shapes_and_partition(): + ells, window = top_hat_windows(ell_max=600, n_bins=20) + + assert ells.shape == (20,) + weights = window.weight # (n_support, n_bins) + assert weights.shape[1] == 20 + assert weights.shape[0] == 601 # every multipole 0..ell_max has a support slot + # top-hat partition: every multipole, including ell_max, lands in exactly one bin + np.testing.assert_array_equal((weights != 0).sum(axis=1), np.ones(601)) + + +def test_top_hat_windows_covers_tail_when_not_divisible(): + # ell_max + 1 = 101 is not divisible by 3; the old floor-division binning left + # the high-ell tail (and ell_max itself) unbinned. The partition must be total. + ells, window = top_hat_windows(ell_max=100, n_bins=3) + + assert ells.shape == (3,) + np.testing.assert_array_equal((window.weight != 0).sum(axis=1), np.ones(101)) + + +def test_top_hat_windows_bin_a_constant_to_itself(): + # Pins the *normalisation*, which the partition assertions above cannot see: a + # likelihood bins theory as `w_bins @ cl` with no implicit averaging, so a + # window whose per-bin weights sum to one must return a constant spectrum + # unchanged. Unit-weight (sum) windows would return delta_ell * const instead, + # silently mismatching data stored at the bin centres by a factor ~30. + for ell_max, n_bins in [(600, 20), (100, 3)]: # divisible and not + _, window = top_hat_windows(ell_max=ell_max, n_bins=n_bins) + w_bins = window.weight.T + np.testing.assert_allclose(w_bins.sum(axis=1), np.ones(n_bins)) + cl = np.full(ell_max + 1, 3.7) + np.testing.assert_allclose(w_bins @ cl, np.full(n_bins, 3.7)) + + +def test_gaussian_covariance_is_symmetric_positive_diagonal(): + n_maps, n_ell = 2, 5 + ells = (np.arange(n_ell) + 0.5) * 30 + rng = np.random.default_rng(0) + cls = rng.random((n_maps, n_maps, n_ell)) + 1.0 + cls = (cls + cls.transpose(1, 0, 2)) / 2 # symmetric in the map indices + + cov = gaussian_covariance(cls, ells, delta_ell=30, fsky=0.4) + + n_cross = n_maps * (n_maps + 1) // 2 + assert cov.shape == (n_cross * n_ell, n_cross * n_ell) + np.testing.assert_allclose(cov, cov.T) + assert np.all(np.diag(cov) > 0) + + +def test_gaussian_covariance_scales_inversely_with_fsky(): + n_maps, n_ell = 2, 4 + ells = (np.arange(n_ell) + 0.5) * 30 + rng = np.random.default_rng(1) + cls = rng.random((n_maps, n_maps, n_ell)) + 1.0 + cls = (cls + cls.transpose(1, 0, 2)) / 2 + + half = gaussian_covariance(cls, ells, 30, fsky=0.4) + quarter = gaussian_covariance(cls, ells, 30, fsky=0.2) + + np.testing.assert_allclose(quarter, 2 * half) + + +def _tiny_lensing_sacc(values): + """A one-spectrum (cl_00, ck x ck) SACC with windows and a covariance.""" + n = len(values) + s = sacc.Sacc() + s.add_tracer( + "Map", + "ck", + quantity="cmb_convergence", + spin=0, + ell=np.arange(100), + beam=np.ones(100), + ) + support = np.arange(2, 2 + 3 * n) + weight = np.zeros((len(support), n)) + for b, idx in enumerate(np.array_split(np.arange(len(support)), n)): + weight[idx, b] = 1.0 / len(idx) + s.add_ell_cl( + "cl_00", + "ck", + "ck", + np.arange(n), + values, + window=sacc.BandpowerWindow(support, weight), + ) + s.add_covariance(np.diag(np.full(n, 4.0))) + return s + + +def test_smooth_twin_sacc_replaces_mean_and_reuses_windows_and_cov(tmp_path): + # A smooth twin keeps the source tracers/windows/covariance but swaps the + # measured bandpowers for the theory we pass in. + src = _tiny_lensing_sacc(np.array([1.0, 2.0, 3.0, 4.0])) + theory = np.array([10.0, 20.0, 30.0, 40.0]) + out = tmp_path / "twin.fits" + + twin = smooth_twin_sacc(src, "cl_00", "ck", "ck", theory, out_path=out) + + np.testing.assert_array_equal(twin.mean, theory) # data == theory + np.testing.assert_array_equal(twin.covariance.covmat, src.covariance.covmat) + assert "ck" in twin.tracers # tracer carried over + reloaded = sacc.Sacc.load_fits(str(out)) + np.testing.assert_array_equal(reloaded.mean, theory) # survives round-trip + assert reloaded.get_bandpower_windows(np.arange(len(theory))) is not None + + +def _numeric_fiducial(info): + """Flat {name: value} of the numeric fixed fiducial params (skip lambdas).""" + return { + name: spec["value"] + for name, spec in info["params"].items() + if isinstance(spec, dict) + and "value" in spec + and not isinstance(spec["value"], str) + } + + +@pytest.mark.skipif(not _mflike_data_available(), reason="MFLike data not installed") +def test_smooth_mflike_sacc_gives_zero_chi2_round_trip(tmp_path, check_skip_mflike): + # A smooth MFLike dataset is theory binned through MFLike's own windows; the + # MFLike likelihood evaluated on it at the same fiducial must give chi^2 = 0. + from cobaya.model import get_model + + from soliket.presets import build_info, resolve_aliases + + info = build_info("mflike") + info["packages_path"] = resolve_packages_path() + model = get_model(info) + roles = resolve_aliases(model) + params = _numeric_fiducial(info) + model.loglikes(params) # populate the provider at the fiducial point + + dls = model.provider.get_Cl(ell_factor=True) + fg_totals = roles.foreground.get_fg_totals() + out = tmp_path / "data_sacc_smooth.fits" + + sacc_obj = smooth_mflike_sacc(roles.mflike, dls, fg_totals, params, out_path=out) + + assert out.is_file() + assert len(sacc_obj.mean) > 0 + + # Rebuild MFLike on the smooth data (absolute input_file; reuse shipped cov/Bbl). + info2 = build_info("mflike") + info2["packages_path"] = resolve_packages_path() + info2["likelihood"]["mflike.TTTEEE"]["input_file"] = str(out) + model2 = get_model(info2) + mflike2 = model2.likelihood["mflike.TTTEEE"] + loglike = float(model2.loglikes(params)[0].sum()) + chi2 = -2 * (loglike - mflike2.logp_const) + + assert abs(chi2) < 1e-6