diff --git a/.agents/skills/isolate-accuracy-fix/SKILL.md b/.agents/skills/isolate-accuracy-fix/SKILL.md new file mode 100644 index 00000000..0051865b --- /dev/null +++ b/.agents/skills/isolate-accuracy-fix/SKILL.md @@ -0,0 +1,98 @@ +--- +name: isolate-accuracy-fix +description: "Isolate minimal efficient code changes to improve numerical accuracy or fix regression mismatches in CAMB. Use for run_tests.py failures, check_accuracy instability, step-size or switch sensitivity, low-k or recombination-era regressions, and when accuracy fixes must preserve runtime." +argument-hint: "Describe the failing test, ini file, output mismatch, or accuracy-sensitive regime to isolate." +user-invocable: true +--- + +# Isolate Minimal Accuracy Fixes + +Use this skill when CAMB has a numerical mismatch, stability failure, or accuracy regression and the goal is to find the smallest code change that fixes it without paying unnecessary runtime cost. + +This workflow is for targeted isolation, not broad tuning. Prefer a local fix to a global increase in tolerances, sampling, or step control. + +## When to Use + +- `fortran/tests/run_tests.py` fails on one or a few parameter files. +- `camb/check_accuracy.py` shows unstable outputs or larger-than-expected boosted-reference differences. +- A change in step size, switch timing, approximation handoff, or tolerance appears to affect results. +- A proposed fix improves accuracy but may slow unrelated runs. +- You need to identify which switch, redshift range, `k` range, or output family is actually sensitive. + +## Main Goal + +Find the narrowest code path and the narrowest phase-space region that controls the failing behavior, then implement the smallest performant fix that removes the mismatch. + +## Core Rules + +1. Start from a concrete failing anchor. + Use a failing ini file, output stem, test command, or changed routine. + +2. Reproduce in a clean comparison environment. + If the workspace is dirty, use a disposable copy under `/tmp` so A/B tests do not disturb user edits. + +3. Isolate the list of physically distinct failing outputs and address them one at a time. + E.g. low-l EE, mpk, and high-L TT failures are likely to be different issues. Focus separately on each in turn. + +4. Do not infer causality from a command option just because it is present. + Vary or remove the suspected trigger and check whether the same physical output still fails. E.g. a command with high lensing accuracy can still expose an unrelated low-l EE issue. + +5. a) Probe broadly first. Sweep AccuracyBoost / lAccuracyBoost / lSampleBoost + to confirm the issue is accuracy-controlled, not a bug. + The camb/check_accuracy.py script command can do this for stability under changes in accuracy paremeters, as well as providing info on which other less broad boost parameters affect the result. You may need to wait many minutes while it runs. + b) Then change one lever at a time when narrowing the cause, more locally that just a boost parameter. + +6. Prefer local overrides over global defaults. + If a change is only needed in a specific regime (a redshift window, k-range, or output type), test a localized change before touching global defaults. A broad change is acceptable when the precision gain is broad and the runtime cost is negligible; narrow the target when the fix is expensive. + +7. Treat runtime as a first-class constraint. + A fix is incomplete if it passes but slows transfer or matter-power runs for no precision benefit. + +8. Your changes should make physical sense. + Target changes to relevant physical time/k/other scales, rather than be ad-hoc patches that treat symptom rather than accuracy of general underlying calculation. Give physical explanation for each physically distinct change that you make, and if it makes no sense, investigate further. + +9. When changing a sampling grid, identify all consumers of that grid. + CMB source grids, transfer grids, and lensing grids can be shared with matter-power or source-window paths. Run an adjacent shared-path check before accepting the change. + +10. Treat index-based regression tests as grid-fragile. + If a test checks `array[i]`, decide whether `i` represents a physical point or just the old sampling. For grid changes, validate by interpolating to fixed physical coordinates before updating expected values. + +11. Separate "more accurate" from "different grid". + A passing boosted comparison is not enough if unrelated outputs move. Check a like-for-like physical comparison or a higher-accuracy reference before updating tests. + +12. Prefer direct control variables over proxy boosts. + If a broad boost works, identify which concrete derived quantity changed + (e.g. `dlnk`, `dk`, `lognum`, sample number, switch location, integration limit). Prefer changing that quantity directly rather than keeping a boost that also + changes unrelated ranges. + +## Acceptance Matrix + +For each proposed local fix, test: + +- The original failing command, suppressing unrelated output families only when needed to keep focus. +- The same physical output with the suspected trigger removed or varied. +- An adjacent path that shares the changed grid, switch, or tolerance. +- The default unit/regression test covering that shared path. +- Runtime before and after for the target run. +- For any accepted boost-derived fix, document the concrete internal quantity + being changed and confirm unrelated quantities controlled by the same boost + are not part of the final patch. +- Never use a local scaled boost fix where a more specific/physically-targeted fix to specific k/time/output/etc might work available. +- No temporary or unjustified code changes remain in the working tree + +## Deliverable + +Leave the final, minimal fix applied to the working tree as unstaged changes so the user can review the diff directly. + +Clean up any A/B scaffolding before finishing where not likely to be useful again: + +- Remove disposable `/tmp` copies used for comparison. +- Revert any exploratory edits, debug prints, or temporary parameter bumps that are not part of the final fix. + +Report should include (for each separate physical issue): + +- The minimal failing reproducer (ini + command). +- The localized change(s) (file + lines) that resolves it +- Before/after numbers for the failing diagnostic. +- Runtime impact on a representative non-failing run (cpu times). +- Physical justification for why the change targets the right scale/domain. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..60b8025c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,50 @@ +ARG VARIANT="3.14-bookworm" +FROM mcr.microsoft.com/devcontainers/python:${VARIANT} + +ENV CAMB_DEVCONTAINER_VENV=/home/vscode/.local/share/camb-devcontainer/.venv \ + PRE_COMMIT_HOME=/home/vscode/.cache/pre-commit \ + UV_CACHE_DIR=/home/vscode/.cache/uv \ + UV_LINK_MODE=copy \ + UV_PROJECT_ENVIRONMENT=/home/vscode/.local/share/camb-devcontainer/.venv + +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + g++ \ + gfortran \ + git \ + libgsl-dev \ + make \ + pkg-config \ + tar \ + unzip \ + xz-utils \ + && curl -LsSf https://astral.sh/uv/install.sh -o /tmp/install-uv.sh \ + && env UV_INSTALL_DIR=/usr/local/bin sh /tmp/install-uv.sh \ + && rm -f /tmp/install-uv.sh \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p \ + /home/vscode/.local/share/camb-devcontainer \ + "${PRE_COMMIT_HOME}" \ + "${UV_CACHE_DIR}" \ + && chown -R vscode:vscode /home/vscode/.local /home/vscode/.cache \ + && su vscode -c 'uv venv /home/vscode/.local/share/camb-devcontainer/.venv --python /usr/local/bin/python3' \ + && su vscode -c 'uv pip install \ + --upgrade \ + --python /home/vscode/.local/share/camb-devcontainer/.venv/bin/python3 \ + pip \ + setuptools \ + wheel \ + findent \ + fortls \ + numpy \ + scipy \ + sympy \ + packaging \ + ruff \ + pre-commit' diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..13cc0afa --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,64 @@ +# CAMB devcontainer + +This devcontainer uses Python 3.14 and caps the container and default build runtime to 8 cores. + +The Python environment is created inside the container filesystem, not in the workspace checkout, so it does not fight with host-mounted file permissions or filesystem performance. + +The repository-level VS Code settings stay host-friendly and only help VS Code discover a workspace `./.venv`. +The devcontainer sets its own interpreter path separately, so a Windows host virtual environment and the Linux +container virtual environment can coexist cleanly. + +The devcontainer is designed to open either the main CAMB checkout or a linked Git worktree. It mounts the parent +directory of the opened folder so the container can always see the repository `.git` metadata, and the startup script +normalizes linked-worktree metadata to portable relative paths so the same checkout works from both Windows and the +Linux container. + +Git inside the container is also told to ignore executable-bit changes for the mounted workspace. On Windows-backed +bind mounts, Linux often reports regular files as executable even when the Git index records them as non-executable, +which would otherwise produce spurious `100644 => 100755` diffs. + +When the container is created it: + +- installs the CAMB build toolchain, including `gfortran`, `g++`, `make`, and `libgsl-dev` +- installs `uv` for locked, repeatable Python environment syncs +- creates the container-local Python virtual environment and pre-installs the small Python dependency/tool set +- downloads CosmoRec into `external/CosmoRec` +- clones HYREC-2 into `external/HYREC-2` +- patches the CosmoRec makefile to add `-fPIC`, which CAMB's Python wrapper requires +- rebuilds `camb` with `recfast`, `cosmorec`, and `hyrec` enabled, and adds the workspace to the environment with a `.pth` file +- configures `core.hooksPath` to use the tracked `.githooks/pre-commit` wrapper, which can use `pre-commit` from the container venv or from a host `.venv` +- pre-installs the `pre-commit` hook environments into a container-local cache so later hook runs can work offline + +The devcontainer also installs `findent` and `fortls` into the container-local virtual environment so the Modern Fortran +extension can format and index code without depending on host-side tools. + +The image and bootstrap intentionally avoid `pip install -e .`: + +- the Dockerfile uses `uv pip install ...` to install the small runtime and dev tool set directly into the container-local virtual environment +- `post-create.sh` runs `python setup.py make` to rebuild the linked Fortran library with the optional recombination backends enabled +- `post-create.sh` writes a small `.pth` file in the container-local environment so the workspace checkout is importable immediately without waiting on slow editable metadata generation + +Clean-tree CAMB builds automatically do the first Fortran sub-build with `-j1` until compiler-generated `.d` files +exist, after which `make -j` and `python setup.py make` can safely respect any `MAKEFLAGS` you set. + +The first container bootstrap still needs network access to fetch the hook repositories listed in +`.pre-commit-config.yaml`, but once bootstrap succeeds the tracked Git hook can run later without internet access. + +The `external/` directory stays in the workspace, so the downloaded sources are visible from Windows as normal files. + +Useful commands inside the container: + +```bash +uv pip install --python /home/vscode/.local/share/camb-devcontainer/.venv/bin/python3 numpy scipy sympy packaging ruff pre-commit +uv run python setup.py make +uv run python -m unittest camb.tests.camb_test +``` + +On the host, keep using a normal workspace `.venv` if you want. The tracked Git hook resolves the right +`pre-commit` executable at runtime instead of baking in one interpreter path during installation. + +Because the workspace may still contain a host-side `.venv` that is not usable inside Linux, tools that do not honor the +devcontainer's selected interpreter may need to be pointed explicitly at +`/home/vscode/.local/share/camb-devcontainer/.venv/bin/python3`. + +If you explicitly want a standards-based editable install after the container is up, you can still run `python -m pip install --no-build-isolation --no-deps -e .`, but that setuptools metadata phase is the part that remains slow on this bind-mounted checkout. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..db5a5c0e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,69 @@ +{ + "name": "camb-py314", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + "VARIANT": "3.14-bookworm" + } + }, + "runArgs": [ + "--cpus=8" + ], + "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces/camb-parent,type=bind", + "workspaceFolder": "/workspaces/camb-parent/${localWorkspaceFolderBasename}", + "mounts": [ + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.gitconfig,target=/tmp/devcontainer-host.gitconfig,type=bind,readonly", + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.codex,target=/tmp/devcontainer-host-codex,type=bind,readonly", + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.claude,target=/tmp/devcontainer-host-claude,type=bind,readonly", + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.claude.json,target=/tmp/devcontainer-host.claude.json,type=bind,readonly" + ], + "initializeCommand": "git submodule update --init --recursive forutils", + "containerEnv": { + "COSMOREC_PATH": "${containerWorkspaceFolder}/external/CosmoRec/", + "FORUTILSPATH": "${containerWorkspaceFolder}/forutils", + "GIT_CONFIG_COUNT": "2", + "GIT_CONFIG_KEY_0": "submodule.forutils.ignore", + "GIT_CONFIG_VALUE_0": "all", + "GIT_CONFIG_KEY_1": "core.fileMode", + "GIT_CONFIG_VALUE_1": "false", + "HYREC_PATH": "${containerWorkspaceFolder}/external/HYREC-2/", + "OMP_NUM_THREADS": "8", + "OPENBLAS_NUM_THREADS": "8", + "RECOMBINATION_FILES": "recfast cosmorec hyrec", + "CAMB_DEVCONTAINER_VENV": "/home/vscode/.local/share/camb-devcontainer/.venv", + "PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit", + "UV_CACHE_DIR": "/home/vscode/.cache/uv", + "UV_LINK_MODE": "copy", + "UV_PROJECT_ENVIRONMENT": "/home/vscode/.local/share/camb-devcontainer/.venv" + }, + "remoteEnv": { + "PATH": "/home/vscode/.local/share/camb-devcontainer/.venv/bin:${containerEnv:PATH}", + "VIRTUAL_ENV": "/home/vscode/.local/share/camb-devcontainer/.venv" + }, + "postCreateCommand": "bash .devcontainer/scripts/post-create.sh", + "postStartCommand": "bash .devcontainer/scripts/post-start.sh", + "customizations": { + "vscode": { + "settings": { + "fortran.formatting.formatter": "findent", + "fortran.formatting.path": "/home/vscode/.local/share/camb-devcontainer/.venv/bin", + "python.defaultInterpreterPath": "/home/vscode/.local/share/camb-devcontainer/.venv/bin/python3", + "python-envs.workspaceSearchPaths": [ + "/home/vscode/.local/share/camb-devcontainer" + ], + "python.venvPath": "/home/vscode/.local/share/camb-devcontainer", + "terminal.integrated.defaultProfile.linux": "bash", + "python-envs.terminal.autoActivationType": "off" + }, + "extensions": [ + "charliermarsh.ruff", + "fortran-lang.linter-gfortran", + "ms-python.debugpy", + "ms-python.python", + "openai.chatgpt", + "anthropic.claude-code" + ] + } + } +} diff --git a/.devcontainer/scripts/install-recombination-deps.sh b/.devcontainer/scripts/install-recombination-deps.sh new file mode 100644 index 00000000..a953e623 --- /dev/null +++ b/.devcontainer/scripts/install-recombination-deps.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +workspace_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +external_dir="${workspace_dir}/external" +cosmorec_dir="${COSMOREC_PATH:-${external_dir}/CosmoRec}" +hyrec_dir="${HYREC_PATH:-${external_dir}/HYREC-2}" +cosmorec_url="${COSMOREC_URL:-https://www.cita.utoronto.ca/~jchluba/Recombination/_Downloads_/CosmoRec.v2.0.3b.tar.gz}" +hyrec_repo="${HYREC_REPO:-https://github.com/nanoomlee/HYREC-2.git}" + +safe_git() { + env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE git "$@" +} + +cosmorec_dir="${cosmorec_dir%/}" +hyrec_dir="${hyrec_dir%/}" +patched_cosmorec_flags=0 + +mkdir -p "${external_dir}" + +if [[ ! -f "${cosmorec_dir}/Makefile" ]]; then + archive_path="${external_dir}/CosmoRec.v2.0.3b.tar.gz" + temp_dir="$(mktemp -d)" + trap 'rm -rf "${temp_dir}"' EXIT + + if [[ ! -f "${archive_path}" ]]; then + curl --fail --show-error --location --retry 3 --retry-delay 2 "${cosmorec_url}" -o "${archive_path}" + fi + + mkdir -p "${temp_dir}/extract" + tar --no-same-owner --no-same-permissions -xzf "${archive_path}" -C "${temp_dir}/extract" + + extracted_root="$(find "${temp_dir}/extract" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + if [[ -z "${extracted_root}" ]]; then + echo "Failed to locate extracted CosmoRec sources" >&2 + exit 1 + fi + + rm -rf "${cosmorec_dir}" + mv "${extracted_root}" "${cosmorec_dir}" +fi + +chmod -R u+rwX "${cosmorec_dir}" >/dev/null 2>&1 || true + +cosmorec_makefile_in="${cosmorec_dir}/Makefile.in" +if [[ -f "${cosmorec_makefile_in}" ]] && ! grep -Eq '^[[:space:]]*CXXFLAGS.*-fPIC' "${cosmorec_makefile_in}"; then + sed -i -E '/^[[:space:]]*CXXFLAGS[[:space:]]*=/ s/$/ -fPIC/' "${cosmorec_makefile_in}" + patched_cosmorec_flags=1 +fi + +if [[ -f "${cosmorec_makefile_in}" ]]; then + if grep -Eq '^[[:space:]]*CXXFLAGSLOC[[:space:]]*=.*RECFASTPPPATH' "${cosmorec_makefile_in}"; then + sed -i -E '/^[[:space:]]*CXXFLAGSLOC[[:space:]]*=/ s|[[:space:]]+-D RECFASTPPPATH=.*$||' "${cosmorec_makefile_in}" + patched_cosmorec_flags=1 + fi + + if ! grep -Eq '^[[:space:]]+-D RECFASTPPPATH=' "${cosmorec_makefile_in}"; then + sed -i '/-D COSMORECPATH=/i\ -D RECFASTPPPATH=\\"$(PWD)/$(DEV_DIR)/Cosmology/Recfast++/\\" \\' "${cosmorec_makefile_in}" + patched_cosmorec_flags=1 + fi +fi + +cosmorec_makefile="${cosmorec_dir}/Makefile" +if [[ -f "${cosmorec_makefile}" ]] && ! grep -Eq '^[[:space:]]*CXXFLAGS.*-fPIC' "${cosmorec_makefile}"; then + printf '\nCXXFLAGS += -fPIC\n' >> "${cosmorec_makefile}" + patched_cosmorec_flags=1 +fi + +if [[ "${patched_cosmorec_flags}" == "1" ]]; then + make -C "${cosmorec_dir}" tidy >/dev/null 2>&1 || true +fi + +if [[ ! -d "${hyrec_dir}/.git" ]]; then + rm -rf "${hyrec_dir}" + safe_git clone --depth 1 --filter=blob:none "${hyrec_repo}" "${hyrec_dir}" +fi + +chmod -R u+rwX "${hyrec_dir}" >/dev/null 2>&1 || true + +if [[ ! -f "${hyrec_dir}/Makefile" ]]; then + echo "HYREC-2 checkout at ${hyrec_dir} does not contain a Makefile" >&2 + exit 1 +fi diff --git a/.devcontainer/scripts/post-create.sh b/.devcontainer/scripts/post-create.sh new file mode 100644 index 00000000..7028eaab --- /dev/null +++ b/.devcontainer/scripts/post-create.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +set -euo pipefail + +workspace_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +venv_dir="${CAMB_DEVCONTAINER_VENV:-${UV_PROJECT_ENVIRONMENT:-/home/vscode/.local/share/camb-devcontainer/.venv}}" +venv_python="${venv_dir}/bin/python3" +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +has_git_worktree=0 +workspace_git_dir="${GIT_DIR:-}" +workspace_git_work_tree="${GIT_WORK_TREE:-}" + +workspace_git() { + local -a git_args + + git_args=() + if [[ -n "${workspace_git_dir}" ]]; then + git_args+=("--git-dir=${workspace_git_dir}") + fi + if [[ -n "${workspace_git_work_tree}" ]]; then + git_args+=("--work-tree=${workspace_git_work_tree}") + fi + + env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE git "${git_args[@]}" "$@" +} + +repair_writable_path() { + local path="$1" + + if [[ ! -e "${path}" || -w "${path}" ]]; then + return 0 + fi + + if ! command -v sudo >/dev/null 2>&1; then + return 0 + fi + + sudo chown -R "$(id -u):$(id -g)" "${path}" >/dev/null 2>&1 || true + sudo chmod -R u+rwX "${path}" >/dev/null 2>&1 || true +} + +if ! command -v uv >/dev/null 2>&1; then + echo "uv is required for the CAMB devcontainer bootstrap." >&2 + exit 1 +fi + +cd "${workspace_dir}" + +bash "${script_dir}/post-start.sh" + +if workspace_git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + has_git_worktree=1 +fi + +unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE + +if [[ ! -f "${FORUTILSPATH:-${workspace_dir}/forutils}/Makefile" ]] && [[ "${has_git_worktree}" == "1" ]]; then + workspace_git submodule update --init --recursive forutils +fi + +repair_writable_path "${workspace_dir}/external" +repair_writable_path "${workspace_dir}/fortran/Releaselib" +repair_writable_path "${workspace_dir}/fortran/Debuglib" +repair_writable_path "${workspace_dir}/forutils/Releaselib" +repair_writable_path "${workspace_dir}/forutils/Debuglib" + +bash .devcontainer/scripts/install-recombination-deps.sh + +mkdir -p "$(dirname "${venv_dir}")" +export UV_PROJECT_ENVIRONMENT="${venv_dir}" + +if [[ ! -x "${venv_python}" ]]; then + uv venv "${venv_dir}" --python /usr/local/bin/python3 +fi + +mkdir -p "${PRE_COMMIT_HOME:-/home/vscode/.cache/pre-commit}" + + +site_packages_dir="$("${venv_python}" - <<'PY' +import site + +print(site.getsitepackages()[0]) +PY +)" + +# Keep any caller-provided MAKEFLAGS; CAMB falls back to a one-time serial sub-build +# automatically when compiler-generated dependency files do not exist yet. +"${venv_python}" setup.py make + +# Modern editable installs spend a long time in setuptools metadata generation on this bind mount. +# Add the workspace directly to site-packages so imports resolve immediately in the devcontainer. +printf '%s\n' "${workspace_dir}" > "${site_packages_dir}/camb-devcontainer.pth" + +if [[ "${has_git_worktree}" == "1" ]]; then + if ! workspace_git config core.hooksPath .githooks; then + echo "Failed to configure repository-local Git hooks." >&2 + fi + + if [[ -f .githooks/pre-commit ]]; then + chmod +x .githooks/pre-commit >/dev/null 2>&1 || true + fi + + if ! "${venv_python}" -m pre_commit install-hooks; then + echo "pre-commit hook environment installation failed; offline hook runs may not work until you rerun '${venv_python} -m pre_commit install-hooks' with network access." >&2 + fi +fi + +echo "CAMB devcontainer bootstrap finished." diff --git a/.devcontainer/scripts/post-start.sh b/.devcontainer/scripts/post-start.sh new file mode 100644 index 00000000..d1ccdc62 --- /dev/null +++ b/.devcontainer/scripts/post-start.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash + +set -euo pipefail + +workspace_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +host_gitconfig="/tmp/devcontainer-host.gitconfig" +host_codex_dir="/tmp/devcontainer-host-codex" +host_claude_dir="/tmp/devcontainer-host-claude" +host_claude_json="/tmp/devcontainer-host.claude.json" + +safe_git() { + env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE git "$@" +} + +normalize_worktree_metadata() { + local workspace_name git_file admin_dir gitdir_file + + git_file="${workspace_dir}/.git" + if [[ ! -f "${git_file}" ]] || [[ -d "${git_file}" ]]; then + return 0 + fi + + workspace_name="$(basename "${workspace_dir}")" + admin_dir="$(cd "${workspace_dir}/.." && pwd)/.git/worktrees/${workspace_name}" + gitdir_file="${admin_dir}/gitdir" + + if [[ ! -d "${admin_dir}" ]] || [[ ! -f "${gitdir_file}" ]]; then + return 0 + fi + + printf 'gitdir: ../.git/worktrees/%s\n' "${workspace_name}" > "${git_file}" + printf '../../%s/.git\n' "${workspace_name}" > "${gitdir_file}" +} + +normalize_worktree_metadata + +ensure_safe_directory() { + local repo_dir="$1" + local existing_dir + + if [[ ! -e "${repo_dir}/.git" ]]; then + return 0 + fi + + while IFS= read -r existing_dir; do + if [[ "${existing_dir}" == "${repo_dir}" ]]; then + return 0 + fi + done < <(safe_git config --global --get-all safe.directory || true) + + safe_git config --global --add safe.directory "${repo_dir}" +} + +ensure_safe_directory "${workspace_dir}" +ensure_safe_directory "${workspace_dir}/forutils" + +setup_codex_dir() { + local codex_dir source_path target_path + + if [[ ! -d "${host_codex_dir}" ]]; then + return 0 + fi + + codex_dir="${HOME}/.codex" + if [[ -e "${codex_dir}" && ! -w "${codex_dir}" ]] && command -v sudo >/dev/null 2>&1; then + sudo chown -R "$(id -u):$(id -g)" "${codex_dir}" >/dev/null 2>&1 || true + fi + + mkdir -p "${codex_dir}" + chmod 700 "${codex_dir}" >/dev/null 2>&1 || true + + for name in auth.json config.toml; do + source_path="${host_codex_dir}/${name}" + target_path="${codex_dir}/${name}" + if [[ -f "${source_path}" && ! -e "${target_path}" ]]; then + cp -p "${source_path}" "${target_path}" >/dev/null 2>&1 || cp "${source_path}" "${target_path}" + chmod 600 "${target_path}" >/dev/null 2>&1 || true + fi + done + + for name in skills plugins rules; do + source_path="${host_codex_dir}/${name}" + target_path="${codex_dir}/${name}" + if [[ -d "${source_path}" ]]; then + mkdir -p "${target_path}" + cp -a "${source_path}/." "${target_path}/" >/dev/null 2>&1 || true + fi + done + + mkdir -p \ + "${codex_dir}/cache" \ + "${codex_dir}/sessions" \ + "${codex_dir}/shell_snapshots" \ + "${codex_dir}/sqlite" \ + "${codex_dir}/tmp" + chmod -R u+rwX \ + "${codex_dir}/cache" \ + "${codex_dir}/sessions" \ + "${codex_dir}/shell_snapshots" \ + "${codex_dir}/sqlite" \ + "${codex_dir}/tmp" \ + >/dev/null 2>&1 || true +} + +setup_codex_dir + +setup_claude_dir() { + local claude_dir source_path target_path + + claude_dir="${HOME}/.claude" + if [[ -e "${claude_dir}" && ! -w "${claude_dir}" ]] && command -v sudo >/dev/null 2>&1; then + sudo chown -R "$(id -u):$(id -g)" "${claude_dir}" >/dev/null 2>&1 || true + fi + + mkdir -p "${claude_dir}" + chmod 700 "${claude_dir}" >/dev/null 2>&1 || true + + if [[ -f "${host_claude_json}" && ! -e "${HOME}/.claude.json" ]]; then + cp -p "${host_claude_json}" "${HOME}/.claude.json" >/dev/null 2>&1 || cp "${host_claude_json}" "${HOME}/.claude.json" + chmod 600 "${HOME}/.claude.json" >/dev/null 2>&1 || true + fi + + if [[ ! -d "${host_claude_dir}" ]]; then + return 0 + fi + + for name in config.json settings.json policy-limits.json; do + source_path="${host_claude_dir}/${name}" + target_path="${claude_dir}/${name}" + if [[ -f "${source_path}" && ! -e "${target_path}" ]]; then + cp -p "${source_path}" "${target_path}" >/dev/null 2>&1 || cp "${source_path}" "${target_path}" + chmod 600 "${target_path}" >/dev/null 2>&1 || true + fi + done + + for name in plugins; do + source_path="${host_claude_dir}/${name}" + target_path="${claude_dir}/${name}" + if [[ -d "${source_path}" ]]; then + mkdir -p "${target_path}" + cp -a "${source_path}/." "${target_path}/" >/dev/null 2>&1 || true + fi + done + + mkdir -p \ + "${claude_dir}/cache" \ + "${claude_dir}/downloads" \ + "${claude_dir}/file-history" \ + "${claude_dir}/ide" \ + "${claude_dir}/projects" \ + "${claude_dir}/sessions" \ + "${claude_dir}/telemetry" + chmod -R u+rwX \ + "${claude_dir}/cache" \ + "${claude_dir}/downloads" \ + "${claude_dir}/file-history" \ + "${claude_dir}/ide" \ + "${claude_dir}/projects" \ + "${claude_dir}/sessions" \ + "${claude_dir}/telemetry" \ + >/dev/null 2>&1 || true +} + +setup_claude_dir + +setup_workspace_claude_link() { + local claude_link="${workspace_dir}/.claude" + local agents_dir="${workspace_dir}/.agents" + + if [[ ! -d "${agents_dir}" ]]; then + return 0 + fi + + if [[ -L "${claude_link}" || -e "${claude_link}" ]]; then + return 0 + fi + + ln -s ".agents" "${claude_link}" +} + +setup_workspace_claude_link + +if [[ ! -f "${host_gitconfig}" ]]; then + exit 0 +fi + +cd "${HOME}" + +host_user_name="$(safe_git config --file "${host_gitconfig}" --get user.name || true)" +host_user_email="$(safe_git config --file "${host_gitconfig}" --get user.email || true)" + +if [[ -n "${host_user_name}" ]] && [[ -z "$(safe_git config --global --get user.name || true)" ]]; then + safe_git config --global user.name "${host_user_name}" +fi + +if [[ -n "${host_user_email}" ]] && [[ -z "$(safe_git config --global --get user.email || true)" ]]; then + safe_git config --global user.email "${host_user_email}" +fi diff --git a/.gitattributes b/.gitattributes index 5e11a078..7628e755 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,11 @@ +* text=auto eol=lf *.txt text *.f90 text *.F90 text -Makefile* text +Makefile* text *.m text *.dat text *.ini text *.sh text +Dockerfile* text +.devcontainer/** text diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 00000000..caaf345c --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +config_file="${repo_root}/.pre-commit-config.yaml" +hook_dir="${repo_root}/.githooks" +git_common_dir="$(git rev-parse --git-common-dir)" +shared_checkout_root="" +platform="$(uname -s)" + +if [[ ! -f "${config_file}" ]]; then + exit 0 +fi + +if [[ "${git_common_dir}" != /* ]] && [[ ! "${git_common_dir}" =~ ^[A-Za-z]:[/\\] ]]; then + git_common_dir="$(cd "${repo_root}" && cd "${git_common_dir}" && pwd)" +fi + +if [[ "$(basename "${git_common_dir}")" == ".git" ]]; then + shared_checkout_root="$(cd "${git_common_dir}/.." && pwd)" +fi + +python_candidates=() + +add_python_candidate() { + local python_bin="$1" + + for existing_bin in "${python_candidates[@]:-}"; do + if [[ "${existing_bin}" == "${python_bin}" ]]; then + return 0 + fi + done + + python_candidates+=("${python_bin}") +} + +if [[ -n "${CAMB_DEVCONTAINER_VENV:-}" ]]; then + add_python_candidate "${CAMB_DEVCONTAINER_VENV}/bin/python" + add_python_candidate "${CAMB_DEVCONTAINER_VENV}/bin/python3" +fi + +add_python_candidate "${repo_root}/.venv/bin/python" + +if [[ -n "${shared_checkout_root}" ]] && [[ "${shared_checkout_root}" != "${repo_root}" ]]; then + add_python_candidate "${shared_checkout_root}/.venv/bin/python" +fi + +case "${platform}" in + CYGWIN*|MINGW*|MSYS*) + add_python_candidate "${repo_root}/.venv/Scripts/python.exe" + + if [[ -n "${shared_checkout_root}" ]] && [[ "${shared_checkout_root}" != "${repo_root}" ]]; then + add_python_candidate "${shared_checkout_root}/.venv/Scripts/python.exe" + fi + ;; +esac + +for python_bin in "${python_candidates[@]}"; do + if [[ -x "${python_bin}" ]]; then + exec "${python_bin}" -m pre_commit hook-impl \ + --config "${config_file}" \ + --hook-type pre-commit \ + --hook-dir "${hook_dir}" \ + --skip-on-missing-config \ + -- "$@" + fi +done + +if command -v python >/dev/null 2>&1 && python -c "import pre_commit" >/dev/null 2>&1; then + exec python -m pre_commit hook-impl \ + --config "${config_file}" \ + --hook-type pre-commit \ + --hook-dir "${hook_dir}" \ + --skip-on-missing-config \ + -- "$@" +fi + +if command -v pre-commit >/dev/null 2>&1; then + exec pre-commit hook-impl \ + --config "${config_file}" \ + --hook-type pre-commit \ + --hook-dir "${hook_dir}" \ + --skip-on-missing-config \ + -- "$@" +fi + +echo "pre-commit is not installed for this checkout. Install CAMB's dev dependencies or use the devcontainer bootstrap." >&2 +exit 1 diff --git a/.github/workflows/test_fortran.yml b/.github/workflows/test_fortran.yml index 1b868c2b..ba13e756 100644 --- a/.github/workflows/test_fortran.yml +++ b/.github/workflows/test_fortran.yml @@ -9,7 +9,8 @@ on: jobs: fortran-test: runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'cmbant/camb') + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name != 'cmbant/camb') steps: - uses: actions/checkout@v5 @@ -29,7 +30,7 @@ jobs: python -m pip install -e . - name: Run Build and Test Script - run: python fortran/tests/run_tests.py + run: python fortran/tests/run_tests.py --verbose_diff_output - name: Upload Test Files on Failure if: failure() diff --git a/.gitignore b/.gitignore index e84f6796..0f7028d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,13 @@ *.o *.so *.sm +*.json *~* .metadata .project .pydevproject x64/ +_build/ Debug/ Debuglib/ Release/ @@ -54,12 +56,12 @@ testfile* .vs *.mp4 *.exe +*_tmp* # Ignore all .dat and .ini files except those already tracked in git *.dat *.ini *compatibility.txt -camb_test.py temp_test.py *.layout lensing_cgrads.f90 @@ -84,3 +86,7 @@ scripts uv.lock .venv __pycache__ +external/ +!.devcontainer/ +!.devcontainer/** +.claude diff --git a/.vscode/settings.json b/.vscode/settings.json index efcf0467..af6af27d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,10 @@ "editor.formatOnSave": true, "files.trimTrailingWhitespace": true, "files.trimFinalNewlines": true, + "files.eol": "\n", + "python-envs.workspaceSearchPaths": [ + "./.venv" + ], // Python test settings "python.testing.pytestEnabled": false, "python.testing.unittestEnabled": true, @@ -114,6 +118,4 @@ "**/*.vfproj": true, "**/*.u2d": true, }, - "python-envs.defaultEnvManager": "ms-python.python:venv", - "python-envs.defaultPackageManager": "ms-python.python:pip", } diff --git a/AGENTS.md b/AGENTS.md index 7acc7f8a..8f5ded25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,20 @@ -# Project instructions -- This is the CAMB python cosmology code source, wrapping fortran 2003 for numerics -- Follow the rules in this file. -- If `AGENTS.local.md` exists, read it after this file and let it override local environment details. - -# Project rules and guidance -- Python uses python 3.10+ with modern type hint syntax -- Use double-quotes for strings; line length 120 -- Code is auto-formatted by ruff, including import reordering. -- Fortran uses modern Fortran 2003, compiled with gfortran or ifort -- Install and run pre-commit hooks as needed for checking -- /forutils is a git submodule with shared fortran utilities and classes -- For fortran/wrapped fortran code modifications see /docs/modifying_code.rst -- When updating version, update both python __version__ and version defined near the top of fortran/config.f90 -- Use pip -e for editable first install. After that use "python setup.py make" to quickly rebuild fortran for use with python. -- Default test: python -m unittest camb.tests.camb_test (do not run hmcode_test unless specifically relevant) -- Installation/clone/modification/contributing/pre-commit/vscode config: see /CONTRIBUTING.md -- When plotting fractional differences for cross-spectra, use e.g. Delta TE/sqrt(TT*EE) -- High accuracy lensing needs lens_potential_accuracy = 8 or so (mainly increases kmax) +# Project instructions +- This is the CAMB python cosmology code source, wrapping fortran 2003 for numerics +- Follow the rules in this file. +- If `AGENTS.local.md` exists, read it after this file and let it override local environment details. + +# Project rules and guidance +- Python uses python 3.10+ with modern type hint syntax +- Use double-quotes for strings; line length 120 +- Code is auto-formatted by ruff, including import reordering. +- Fortran uses modern Fortran 2003, compiled with gfortran or ifort +- Install and run pre-commit hooks as needed for checking +- /forutils is a git submodule with shared fortran utilities and classes +- For fortran/wrapped fortran code modifications see /docs/modifying_code.rst +- When updating version, update both python __version__ and version defined near the top of fortran/config.f90 +- Use pip -e for editable first install. After that use "python setup.py make" to quickly rebuild fortran for use with python. +- Default test: python -m unittest camb.tests.camb_test (do not run hmcode_test unless specifically relevant) +- Long test against precomputed results: fortran/tests/run_tests.py which calls CAMB_test_files.py (only if asked) +- Installation/clone/modification/contributing/pre-commit/vscode config: see /CONTRIBUTING.md +- When plotting fractional differences for cross-spectra, use e.g. Delta TE/sqrt(TT*EE) +- High accuracy lensing needs lens_potential_accuracy = 8 or so (mainly increases kmax) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76afb4e0..8e60cbe6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,10 +15,13 @@ pip install -e .[dev] ### 2. Install Pre-commit Hooks ```bash -pre-commit install +git config core.hooksPath .githooks ``` -This will automatically format your code and check for issues before each commit. +The repository ships a portable `.githooks/pre-commit` wrapper that looks for `pre-commit` in the CAMB devcontainer +environment first, then in the current checkout's `.venv`, then in the main checkout's `.venv` when you are committing +from a linked worktree, then on your `PATH`. This keeps the same checkout working on a Linux devcontainer and a Windows +host without rewriting the hook each time you switch environments. ### 3. Code Formatting Standards @@ -86,6 +89,9 @@ The following extensions will be suggested when you open the project: - `charliermarsh.ruff` - Ruff formatter and linter - `fortran-lang.linter-gfortran` - Fortran support +The committed workspace settings only help VS Code discover a local `./.venv`; the devcontainer sets its +container-local interpreter separately, so host and container environments do not fight each other. + ### Optional Fortran Tools For enhanced Fortran development, you may want to install: diff --git a/MANIFEST.in b/MANIFEST.in index 39af2bcd..0f56fe5e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,15 +1,39 @@ -include *.rst *.txt +# Start from an explicit whitelist so nested worktrees and scratch clones do +# not leak into the source distribution just because they live under the repo +# root. +prune * +exclude * + +# Root metadata and packaging files. +include LICENCE.txt +include MANIFEST.in +include README.rst +include pyproject.toml +include requirements.txt +include setup.py + +# Package sources and bundled data. recursive-include camb *.py recursive-include fortran Makefile* *.dat *.?90 recursive-include forutils Makefile* *.?90 -exclude camb/cambdll.dll -exclude camb/camblib.so + include camb/PArthENoPE_880.2_marcucci.dat include camb/PArthENoPE_880.2_standard.dat include camb/PRIMAT_Yp_DH_Error.dat include camb/PRIMAT_Yp_DH_ErrorMC_2021.dat include camb/PRIMAT_Yp_DH_ErrorMC_2024.dat + +# PyPI long description. +include docs/README_pypi.rst + +# Built artifacts and optional/generated files are shipped separately or +# rebuilt locally. +exclude .gitattributes +exclude .gitignore +exclude .gitmodules +exclude .readthedocs.yml +exclude camb/cambdll.dll +exclude camb/camblib.so exclude camb/HighLExtrapTemplate_lenspotentialCls.dat exclude fortran/sigma8.f90 exclude fortran/writefits.f90 -include docs/README_pypi.rst diff --git a/README.rst b/README.rst index 84c60002..ecba9604 100644 --- a/README.rst +++ b/README.rst @@ -42,7 +42,7 @@ Then install using:: For development, install with dev dependencies and setup pre-commit hooks:: pip install -e ./CAMB[dev] [--user] - pre-commit install + git config core.hooksPath .githooks See `CONTRIBUTING.md `_ for full development setup instructions. diff --git a/camb/__init__.py b/camb/__init__.py index 138edc6c..139f6196 100644 --- a/camb/__init__.py +++ b/camb/__init__.py @@ -1,48 +1,48 @@ -""" - -CAMB, Code for Anisotropies in the Microwave Background (https://camb.info) -Computational modules are wrapped Fortran 2003, but can be used entirely from Python. - -""" - -__author__ = "Antony Lewis" -__contact__ = "antony at cosmologist dot info" -__url__ = "https://camb.readthedocs.io" -__version__ = "1.6.7" - -from . import baseconfig - -baseconfig.check_fortran_version(__version__) -from . import dark_energy, initialpower, model, nonlinear, reionization -from ._config import config -from .baseconfig import ( - Array1D, - CAMBError, - CAMBFortranError, - CAMBParamRangeError, - CAMBUnknownArgumentError, - CAMBValueError, -) -from .camb import ( - free_global_memory, - get_age, - get_background, - get_matter_power_interpolator, - get_results, - get_transfer_functions, - get_valid_numerical_params, - get_zre_from_tau, - read_ini, - run_ini, - set_feedback_level, - set_params, - set_params_cosmomc, - write_ini, -) -from .dark_energy import DarkEnergyFluid, DarkEnergyPPF -from .initialpower import InitialPowerLaw, SplinedInitialPower -from .mathutils import threej -from .model import CAMBparams, TransferParams -from .nonlinear import Halofit -from .reionization import ExpReionization, TanhReionization -from .results import CAMBdata, ClTransferData, MatterTransferData +""" + +CAMB, Code for Anisotropies in the Microwave Background (https://camb.info) +Computational modules are wrapped Fortran 2003, but can be used entirely from Python. + +""" + +__author__ = "Antony Lewis" +__contact__ = "antony at cosmologist dot info" +__url__ = "https://camb.readthedocs.io" +__version__ = "1.6.7" + +from . import baseconfig + +baseconfig.check_fortran_version(__version__) +from . import dark_energy, initialpower, model, nonlinear, reionization +from ._config import config +from .baseconfig import ( + Array1D, + CAMBError, + CAMBFortranError, + CAMBParamRangeError, + CAMBUnknownArgumentError, + CAMBValueError, +) +from .camb import ( + free_global_memory, + get_age, + get_background, + get_matter_power_interpolator, + get_results, + get_transfer_functions, + get_valid_numerical_params, + get_zre_from_tau, + read_ini, + run_ini, + set_feedback_level, + set_params, + set_params_cosmomc, + write_ini, +) +from .dark_energy import DarkEnergyFluid, DarkEnergyPPF +from .initialpower import InitialPowerLaw, SplinedInitialPower +from .mathutils import threej +from .model import CAMBparams, TransferParams +from .nonlinear import Halofit +from .reionization import ExpReionization, TanhReionization +from .results import CAMBdata, ClTransferData, MatterTransferData diff --git a/camb/_command_line.py b/camb/_command_line.py index e3fc72e4..bb805fd7 100644 --- a/camb/_command_line.py +++ b/camb/_command_line.py @@ -4,24 +4,47 @@ import sys from argparse import RawTextHelpFormatter -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -import camb -from camb.baseconfig import filepath_to_fortran, lib_import +sys.path.insert(0, os.path.dirname(__file__)) + +from baseconfig import filepath_to_fortran, lib_import + + +def _get_version() -> str: + init_path = os.path.join(os.path.dirname(__file__), "__init__.py") + with open(init_path, encoding="utf-8") as handle: + for line in handle: + if line.startswith("__version__"): + _, value = line.split("=", 1) + return value.strip().strip("\"'") + raise RuntimeError(f"Could not determine CAMB version from {init_path}") + + +def _run_check_accuracy(argv: list[str]) -> int: + try: + from . import check_accuracy + except ImportError: + import check_accuracy + + return check_accuracy.main(argv, prog="camb check_accuracy") def run_command_line(): + if len(sys.argv) > 1 and sys.argv[1] == "check_accuracy": + raise SystemExit(_run_check_accuracy(sys.argv[2:])) + parser = argparse.ArgumentParser( formatter_class=RawTextHelpFormatter, description="Python command line CAMB reading parameters from a .ini file." + "\n\nSample .ini files are provided in the source distribution, " "e.g. see inifiles/planck_2018.ini at " - "https://github.com/cmbant/CAMB/tree/master/inifiles", + "https://github.com/cmbant/CAMB/tree/master/inifiles" + + "\n\nUse 'camb check_accuracy INI_FILE ...' to compare results against a boosted-accuracy run.", ) parser.add_argument("ini_file", help="text .ini file with parameter settings") parser.add_argument( "--validate", action="store_true", help="Just validate the .ini file, don't actually run anything" ) - parser.add_argument("-V", "--version", action="version", version=camb.__version__) + parser.add_argument("-V", "--version", action="version", version=_get_version()) args = parser.parse_args() if not os.path.exists(args.ini_file): diff --git a/camb/_ini.py b/camb/_ini.py index 2134acd1..5af6b82a 100644 --- a/camb/_ini.py +++ b/camb/_ini.py @@ -1,314 +1,317 @@ -from __future__ import annotations - -import os - -from . import constants, dark_energy, initialpower, model, nonlinear, recombination, reionization, sources -from .baseconfig import CAMBValueError -from .inifile import IniFile - -_initial_condition_names = [ - "initial_vector", - "initial_adiabatic", - "initial_iso_CDM", - "initial_iso_baryon", - "initial_iso_neutrino", - "initial_iso_neutrino_vel", -] -_massive_nu_method_names = ["Nu_int", "Nu_trunc", "Nu_approx", "Nu_best"] -_recfast_hswitch_offset = 1.14 - (1.105 + 0.02) - - -class CambIniFile(IniFile): - pass - - -def format_value(value) -> str: - if isinstance(value, bool): - return "T" if value else "F" - if isinstance(value, str): - return value - if isinstance(value, int): - return str(value) - if isinstance(value, float): - return f"{value:.17g}" - return str(value) - - -def set_param(state: CambIniFile, key: str, value) -> None: - if key not in state.params: - state.readOrder.append(key) - state.params[key] = format_value(value) - - -def _set_ini_sequence(state: CambIniFile, key: str, values) -> None: - set_param(state, key, " ".join(format_value(value) for value in values)) - - -def _nonlinear_mode_value(mode_name: str) -> int: - return model.NonLinear_names.index(mode_name) - - -def _massive_nu_method_value(mode_name: str) -> int: - return _massive_nu_method_names.index(mode_name) - - -def _scalar_initial_condition_value(condition_name: str) -> int: - return _initial_condition_names.index(condition_name) - - -def _update_dark_energy_ini(params: model.CAMBparams, state: CambIniFile) -> None: - dark_energy_model = params.DarkEnergy - if isinstance(dark_energy_model, dark_energy.DarkEnergyFluid): - set_param(state, "dark_energy_model", "fluid") - if dark_energy_model.use_tabulated_w: - raise CAMBValueError("write_ini cannot serialize tabulated dark energy") - else: - set_param(state, "w", dark_energy_model.w) - set_param(state, "wa", dark_energy_model.wa) - set_param(state, "cs2_lam", dark_energy_model.cs2) - return - - if isinstance(dark_energy_model, dark_energy.DarkEnergyPPF): - set_param(state, "dark_energy_model", "ppf") - if dark_energy_model.use_tabulated_w: - raise CAMBValueError("write_ini cannot serialize tabulated dark energy") - else: - set_param(state, "w", dark_energy_model.w) - set_param(state, "wa", dark_energy_model.wa) - set_param(state, "cs2_lam", dark_energy_model.cs2) - return - - if isinstance(dark_energy_model, dark_energy.AxionEffectiveFluid): - set_param(state, "dark_energy_model", "AxionEffectiveFluid") - set_param(state, "AxionEffectiveFluid_w_n", dark_energy_model.w_n) - set_param(state, "AxionEffectiveFluid_fde_zc", dark_energy_model.fde_zc) - set_param(state, "AxionEffectiveFluid_zc", dark_energy_model.zc) - set_param(state, "AxionEffectiveFluid_theta_i", dark_energy_model.theta_i) - return - - if isinstance(dark_energy_model, dark_energy.EarlyQuintessence): - raise CAMBValueError("write_ini cannot serialize EarlyQuintessence") - - raise CAMBValueError(f"write_ini does not support dark energy class {dark_energy_model.__class__.__name__}") - - -def _update_reionization_ini(params: model.CAMBparams, state: CambIniFile) -> None: - reion_model = params.Reion - set_param(state, "reionization", reion_model.Reionization) - if isinstance(reion_model, reionization.BaseTauWithHeReionization): - set_param(state, "re_use_optical_depth", reion_model.use_optical_depth) - if reion_model.use_optical_depth: - set_param(state, "re_optical_depth", reion_model.optical_depth) - else: - set_param(state, "re_redshift", reion_model.redshift) - set_param(state, "re_ionization_frac", reion_model.fraction) - set_param(state, "include_helium_fullreion", reion_model.include_helium_fullreion) - set_param(state, "re_helium_redshift", reion_model.helium_redshift) - set_param(state, "re_helium_delta_redshift", reion_model.helium_delta_redshift) - set_param(state, "re_helium_redshiftstart", reion_model.helium_redshiftstart) - set_param(state, "max_zrei", reion_model.max_redshift) - if isinstance(reion_model, reionization.TanhReionization): - set_param(state, "re_delta_redshift", reion_model.delta_redshift) - elif isinstance(reion_model, reionization.ExpReionization): - set_param(state, "reion_redshift_complete", reion_model.reion_redshift_complete) - set_param(state, "reion_exp_power", reion_model.reion_exp_power) - set_param(state, "reion_exp_smooth_width", reion_model.reion_exp_smooth_width) - - -def _update_initial_power_ini(params: model.CAMBparams, state: CambIniFile) -> None: - initial_power_model = params.InitPower - if isinstance(initial_power_model, initialpower.InitialPowerLaw): - set_param(state, "pivot_scalar", initial_power_model.pivot_scalar) - set_param(state, "pivot_tensor", initial_power_model.pivot_tensor) - set_param(state, "initial_power_num", 1) - set_param(state, "scalar_amp(1)", initial_power_model.As) - set_param(state, "scalar_spectral_index(1)", initial_power_model.ns) - set_param(state, "scalar_nrun(1)", initial_power_model.nrun) - set_param(state, "scalar_nrunrun(1)", initial_power_model.nrunrun) - set_param( - state, - "tensor_parameterization", - initialpower.tensor_parameterization_names.index(initial_power_model.tensor_parameterization) + 1, - ) - set_param(state, "tensor_spectral_index(1)", initial_power_model.nt) - set_param(state, "tensor_nrun(1)", initial_power_model.ntrun) - if initial_power_model.tensor_parameterization == "tensor_param_AT": - set_param(state, "tensor_amp(1)", initial_power_model.At) - else: - set_param(state, "initial_ratio(1)", initial_power_model.r) - return - - raise CAMBValueError(f"write_ini does not support initial power class {initial_power_model.__class__.__name__}") - - -def _update_recombination_ini(params: model.CAMBparams, state: CambIniFile) -> None: - recomb_model = params.Recomb - if isinstance(recomb_model, recombination.Recfast): - set_param(state, "recombination_model", "Recfast") - recfast_fudge = recomb_model.RECFAST_fudge - if recomb_model.RECFAST_Hswitch: - recfast_fudge += _recfast_hswitch_offset - set_param(state, "RECFAST_fudge", recfast_fudge) - set_param(state, "RECFAST_fudge_He", recomb_model.RECFAST_fudge_He) - set_param(state, "RECFAST_Heswitch", recomb_model.RECFAST_Heswitch) - set_param(state, "RECFAST_Hswitch", recomb_model.RECFAST_Hswitch) - set_param(state, "AGauss1", recomb_model.AGauss1) - set_param(state, "AGauss2", recomb_model.AGauss2) - set_param(state, "zGauss1", recomb_model.zGauss1) - set_param(state, "zGauss2", recomb_model.zGauss2) - set_param(state, "wGauss1", recomb_model.wGauss1) - set_param(state, "wGauss2", recomb_model.wGauss2) - set_param(state, "RECFAST_nz", recomb_model.Nz) - elif isinstance(recomb_model, recombination.CosmoRec): - set_param(state, "recombination_model", "CosmoRec") - set_param(state, "cosmorec_runmode", recomb_model.runmode) - set_param(state, "cosmorec_accuracy", recomb_model.accuracy) - set_param(state, "cosmorec_fdm", recomb_model.fdm) - elif isinstance(recomb_model, recombination.HyRec): - set_param(state, "recombination_model", "HyRec") - - -def _update_source_terms_ini(params: model.CAMBparams, state: CambIniFile) -> None: - set_param(state, "limber_windows", params.SourceTerms.limber_windows) - set_param(state, "limber_phiphi", params.SourceTerms.limber_phi_lmin) - set_param(state, "Do21cm", params.Do21cm) - set_param(state, "counts_density", params.SourceTerms.counts_density) - set_param(state, "counts_redshift", params.SourceTerms.counts_redshift) - set_param(state, "DoRedshiftLensing", params.SourceTerms.counts_lensing) - set_param(state, "counts_velocity", params.SourceTerms.counts_velocity) - set_param(state, "counts_radial", params.SourceTerms.counts_radial) - set_param(state, "counts_timedelay", params.SourceTerms.counts_timedelay) - set_param(state, "counts_ISW", params.SourceTerms.counts_ISW) - set_param(state, "counts_potential", params.SourceTerms.counts_potential) - set_param(state, "counts_evolve", params.SourceTerms.counts_evolve) - set_param(state, "line_basic", params.SourceTerms.line_basic) - set_param(state, "line_distortions", params.SourceTerms.line_distortions) - set_param(state, "line_extra", params.SourceTerms.line_extra) - set_param(state, "line_phot_dipole", params.SourceTerms.line_phot_dipole) - set_param(state, "line_phot_quadrupole", params.SourceTerms.line_phot_quadrupole) - set_param(state, "line_reionization", params.SourceTerms.line_reionization) - set_param(state, "use_mK", params.SourceTerms.use_21cm_mK) - set_param(state, "Kmax_Boost", params.Accuracy.KmaxBoost) - - if not params.SourceWindows: - set_param(state, "num_redshiftwindows", 0) - return - - if any(not isinstance(window, sources.GaussianSourceWindow) for window in params.SourceWindows): - raise CAMBValueError("write_ini only supports GaussianSourceWindow") - - set_param(state, "num_redshiftwindows", len(params.SourceWindows)) - for index, window in enumerate(params.SourceWindows, start=1): - set_param(state, f"redshift({index})", window.redshift) - set_param(state, f"redshift_kind({index})", window.source_type) - if window.source_type == "21cm": - set_param(state, f"redshift_sigma_Mhz({index})", window.sigma * constants.f_21cm / 1e6) - else: - set_param(state, f"redshift_sigma({index})", window.sigma) - if window.source_type == "counts": - set_param(state, f"redshift_bias({index})", window.bias) - set_param(state, f"redshift_dlog10Ndm({index})", window.dlog10Ndm) - - -def _update_ini_state_from_params(params: model.CAMBparams, state: CambIniFile) -> None: - set_param(state, "get_scalar_cls", params.WantScalars) - set_param(state, "get_vector_cls", params.WantVectors) - set_param(state, "get_tensor_cls", params.WantTensors) - set_param(state, "want_CMB", params.Want_CMB) - set_param(state, "want_CMB_lensing", params.Want_CMB_lensing) - set_param(state, "get_transfer", params.WantTransfer) - set_param(state, "do_nonlinear", _nonlinear_mode_value(params.NonLinear)) - set_param(state, "evolve_baryon_cs", params.Evolve_baryon_cs) - set_param(state, "evolve_delta_xe", params.Evolve_delta_xe) - set_param(state, "evolve_delta_ts", params.Evolve_delta_Ts) - set_param(state, "l_min", params.min_l) - set_param(state, "use_physical", True) - set_param(state, "hubble", params.H0) - set_param(state, "ombh2", params.ombh2) - set_param(state, "omch2", params.omch2) - set_param(state, "omnuh2", params.omnuh2) - set_param(state, "omk", params.omk) - set_param(state, "temp_cmb", params.TCMB) - set_param(state, "helium_fraction", params.YHe) - set_param(state, "massless_neutrinos", params.num_nu_massless) - set_param(state, "nu_mass_eigenstates", params.nu_mass_eigenstates) - if params.nu_mass_eigenstates: - _set_ini_sequence(state, "massive_neutrinos", params.nu_mass_numbers[: params.nu_mass_eigenstates]) - else: - set_param(state, "massive_neutrinos", 0) - set_param(state, "share_delta_neff", params.share_delta_neff) - if params.num_nu_massive > 0: - if not params.share_delta_neff: - _set_ini_sequence(state, "nu_mass_degeneracies", params.nu_mass_degeneracies[: params.nu_mass_eigenstates]) - _set_ini_sequence(state, "nu_mass_fractions", params.nu_mass_fractions[: params.nu_mass_eigenstates]) - set_param(state, "Alens", params.Alens) - set_param(state, "derived_parameters", params.WantDerivedParameters) - set_param(state, "accurate_polarization", params.Accuracy.AccuratePolarization) - set_param(state, "accurate_reionization", params.Accuracy.AccurateReionization) - set_param(state, "accurate_BB", params.Accuracy.AccurateBB) - set_param(state, "accuracy_boost", params.Accuracy.AccuracyBoost) - set_param(state, "l_accuracy_boost", params.Accuracy.lAccuracyBoost) - set_param(state, "l_sample_boost", params.Accuracy.lSampleBoost) - set_param(state, "do_late_rad_truncation", params.DoLateRadTruncation) - set_param(state, "massive_nu_approx", _massive_nu_method_value(params.MassiveNuMethod)) - - if params.WantCls and (params.WantScalars or params.WantVectors): - set_param(state, "l_max_scalar", params.max_l) - set_param(state, "k_eta_max_scalar", params.max_eta_k) - if params.WantScalars: - set_param(state, "do_lensing", params.DoLensing) - if params.WantCls and params.WantTensors: - set_param(state, "l_max_tensor", params.max_l_tensor) - set_param(state, "k_eta_max_tensor", params.max_eta_k_tensor) - - _update_dark_energy_ini(params, state) - _update_reionization_ini(params, state) - _update_initial_power_ini(params, state) - _update_recombination_ini(params, state) - _update_source_terms_ini(params, state) - - if params.WantTransfer: - set_param(state, "transfer_high_precision", params.Transfer.high_precision) - set_param(state, "accurate_massive_neutrino_transfers", params.Transfer.accurate_massive_neutrinos) - set_param(state, "transfer_kmax", params.Transfer.kmax / (params.H0 / 100.0)) - set_param(state, "transfer_k_per_logint", params.Transfer.k_per_logint) - set_param(state, "transfer_num_redshifts", params.Transfer.PK_num_redshifts) - for index, redshift in enumerate(params.Transfer.PK_redshifts[: params.Transfer.PK_num_redshifts], start=1): - set_param(state, f"transfer_redshift({index})", redshift) - set_param(state, "transfer_21cm_cl", params.transfer_21cm_cl) - - if isinstance(params.NonLinearModel, nonlinear.Halofit): - set_param(state, "halofit_version", nonlinear.halofit_version_names[params.NonLinearModel.halofit_version]) - set_param(state, "HMCode_A_baryon", params.NonLinearModel.HMCode_A_baryon) - set_param(state, "HMCode_eta_baryon", params.NonLinearModel.HMCode_eta_baryon) - set_param(state, "HMcode_logT_AGN", params.NonLinearModel.HMCode_logT_AGN) - elif params.NonLinear != model.NonLinear_none: - raise CAMBValueError( - f"write_ini does not support non-linear model class {params.NonLinearModel.__class__.__name__}" - ) - - set_param(state, "initial_condition", _scalar_initial_condition_value(params.scalar_initial_condition)) - if params.scalar_initial_condition == "initial_vector": - _set_ini_sequence(state, "initial_vector", params.InitialConditionVector) - set_param(state, "use_cl_spline_template", params.use_cl_spline_template) - - -def write_ini( - params: model.CAMBparams, - ini_filename, - *, - validate: bool = True, -) -> None: - if not isinstance(params, model.CAMBparams): - raise TypeError("params must be an instance of CAMBparams") - - ini_path = os.fspath(ini_filename) - state = CambIniFile() - _update_ini_state_from_params(params, state) - state.saveFile(ini_path) - if validate: - from .camb import read_ini - - reparsed = read_ini(ini_path) - if repr(reparsed) != repr(params): - raise CAMBValueError(f"Saved ini did not round-trip via read_ini ({ini_path})") +from __future__ import annotations + +import os + +from . import constants, dark_energy, initialpower, model, nonlinear, recombination, reionization, sources +from .baseconfig import CAMBValueError +from .inifile import IniFile + +_initial_condition_names = [ + "initial_vector", + "initial_adiabatic", + "initial_iso_CDM", + "initial_iso_baryon", + "initial_iso_neutrino", + "initial_iso_neutrino_vel", +] +_massive_nu_method_names = ["Nu_int", "Nu_trunc", "Nu_approx", "Nu_best"] +_recfast_hswitch_offset = 1.14 - (1.105 + 0.02) + + +class CambIniFile(IniFile): + pass + + +def format_value(value) -> str: + if isinstance(value, bool): + return "T" if value else "F" + if isinstance(value, str): + return value + if isinstance(value, int): + return str(value) + if isinstance(value, float): + return repr(value) + return str(value) + + +def set_param(state: CambIniFile, key: str, value) -> None: + if key not in state.params: + state.readOrder.append(key) + state.params[key] = format_value(value) + + +def _set_ini_sequence(state: CambIniFile, key: str, values) -> None: + set_param(state, key, " ".join(format_value(value) for value in values)) + + +def _nonlinear_mode_value(mode_name: str) -> int: + return model.NonLinear_names.index(mode_name) + + +def _massive_nu_method_value(mode_name: str) -> int: + return _massive_nu_method_names.index(mode_name) + + +def _scalar_initial_condition_value(condition_name: str) -> int: + return _initial_condition_names.index(condition_name) + + +def _update_dark_energy_ini(params: model.CAMBparams, state: CambIniFile) -> None: + dark_energy_model = params.DarkEnergy + if isinstance(dark_energy_model, dark_energy.DarkEnergyFluid): + set_param(state, "dark_energy_model", "fluid") + if dark_energy_model.use_tabulated_w: + raise CAMBValueError("write_ini cannot serialize tabulated dark energy") + else: + set_param(state, "w", dark_energy_model.w) + set_param(state, "wa", dark_energy_model.wa) + set_param(state, "cs2_lam", dark_energy_model.cs2) + return + + if isinstance(dark_energy_model, dark_energy.DarkEnergyPPF): + set_param(state, "dark_energy_model", "ppf") + if dark_energy_model.use_tabulated_w: + raise CAMBValueError("write_ini cannot serialize tabulated dark energy") + else: + set_param(state, "w", dark_energy_model.w) + set_param(state, "wa", dark_energy_model.wa) + set_param(state, "cs2_lam", dark_energy_model.cs2) + return + + if isinstance(dark_energy_model, dark_energy.AxionEffectiveFluid): + set_param(state, "dark_energy_model", "AxionEffectiveFluid") + set_param(state, "AxionEffectiveFluid_w_n", dark_energy_model.w_n) + set_param(state, "AxionEffectiveFluid_fde_zc", dark_energy_model.fde_zc) + set_param(state, "AxionEffectiveFluid_zc", dark_energy_model.zc) + set_param(state, "AxionEffectiveFluid_theta_i", dark_energy_model.theta_i) + return + + if isinstance(dark_energy_model, dark_energy.EarlyQuintessence): + raise CAMBValueError("write_ini cannot serialize EarlyQuintessence") + + raise CAMBValueError(f"write_ini does not support dark energy class {dark_energy_model.__class__.__name__}") + + +def _update_reionization_ini(params: model.CAMBparams, state: CambIniFile) -> None: + reion_model = params.Reion + set_param(state, "reionization", reion_model.Reionization) + if isinstance(reion_model, reionization.BaseTauWithHeReionization): + set_param(state, "re_use_optical_depth", reion_model.use_optical_depth) + if reion_model.use_optical_depth: + set_param(state, "re_optical_depth", reion_model.optical_depth) + else: + set_param(state, "re_redshift", reion_model.redshift) + set_param(state, "re_ionization_frac", reion_model.fraction) + set_param(state, "include_helium_fullreion", reion_model.include_helium_fullreion) + set_param(state, "re_helium_redshift", reion_model.helium_redshift) + set_param(state, "re_helium_delta_redshift", reion_model.helium_delta_redshift) + set_param(state, "re_helium_redshiftstart", reion_model.helium_redshiftstart) + set_param(state, "max_zrei", reion_model.max_redshift) + if isinstance(reion_model, reionization.TanhReionization): + set_param(state, "re_delta_redshift", reion_model.delta_redshift) + elif isinstance(reion_model, reionization.ExpReionization): + set_param(state, "reion_redshift_complete", reion_model.reion_redshift_complete) + set_param(state, "reion_exp_power", reion_model.reion_exp_power) + set_param(state, "reion_exp_smooth_width", reion_model.reion_exp_smooth_width) + + +def _update_initial_power_ini(params: model.CAMBparams, state: CambIniFile) -> None: + initial_power_model = params.InitPower + if isinstance(initial_power_model, initialpower.InitialPowerLaw): + set_param(state, "pivot_scalar", initial_power_model.pivot_scalar) + set_param(state, "pivot_tensor", initial_power_model.pivot_tensor) + set_param(state, "initial_power_num", 1) + set_param(state, "scalar_amp(1)", initial_power_model.As) + set_param(state, "scalar_spectral_index(1)", initial_power_model.ns) + set_param(state, "scalar_nrun(1)", initial_power_model.nrun) + set_param(state, "scalar_nrunrun(1)", initial_power_model.nrunrun) + set_param( + state, + "tensor_parameterization", + initialpower.tensor_parameterization_names.index(initial_power_model.tensor_parameterization) + 1, + ) + set_param(state, "tensor_spectral_index(1)", initial_power_model.nt) + set_param(state, "tensor_nrun(1)", initial_power_model.ntrun) + if initial_power_model.tensor_parameterization == "tensor_param_AT": + set_param(state, "tensor_amp(1)", initial_power_model.At) + else: + set_param(state, "initial_ratio(1)", initial_power_model.r) + return + + raise CAMBValueError(f"write_ini does not support initial power class {initial_power_model.__class__.__name__}") + + +def _update_recombination_ini(params: model.CAMBparams, state: CambIniFile) -> None: + recomb_model = params.Recomb + if isinstance(recomb_model, recombination.Recfast): + set_param(state, "recombination_model", "Recfast") + recfast_fudge = recomb_model.RECFAST_fudge + if recomb_model.RECFAST_Hswitch: + recfast_fudge += _recfast_hswitch_offset + set_param(state, "RECFAST_fudge", recfast_fudge) + set_param(state, "RECFAST_fudge_He", recomb_model.RECFAST_fudge_He) + set_param(state, "RECFAST_Heswitch", recomb_model.RECFAST_Heswitch) + set_param(state, "RECFAST_Hswitch", recomb_model.RECFAST_Hswitch) + set_param(state, "AGauss1", recomb_model.AGauss1) + set_param(state, "AGauss2", recomb_model.AGauss2) + set_param(state, "zGauss1", recomb_model.zGauss1) + set_param(state, "zGauss2", recomb_model.zGauss2) + set_param(state, "wGauss1", recomb_model.wGauss1) + set_param(state, "wGauss2", recomb_model.wGauss2) + set_param(state, "RECFAST_nz", recomb_model.Nz) + set_param(state, "RECFAST_use_rosenbrock", recomb_model.use_rosenbrock) + set_param(state, "RECFAST_rosenbrock_handoff_xH", recomb_model.rosenbrock_handoff_xH) + set_param(state, "RECFAST_rosenbrock_tol", recomb_model.rosenbrock_tol) + elif isinstance(recomb_model, recombination.CosmoRec): + set_param(state, "recombination_model", "CosmoRec") + set_param(state, "cosmorec_runmode", recomb_model.runmode) + set_param(state, "cosmorec_accuracy", recomb_model.accuracy) + set_param(state, "cosmorec_fdm", recomb_model.fdm) + elif isinstance(recomb_model, recombination.HyRec): + set_param(state, "recombination_model", "HyRec") + + +def _update_source_terms_ini(params: model.CAMBparams, state: CambIniFile) -> None: + set_param(state, "limber_windows", params.SourceTerms.limber_windows) + set_param(state, "limber_phiphi", params.SourceTerms.limber_phi_lmin) + set_param(state, "Do21cm", params.Do21cm) + set_param(state, "counts_density", params.SourceTerms.counts_density) + set_param(state, "counts_redshift", params.SourceTerms.counts_redshift) + set_param(state, "DoRedshiftLensing", params.SourceTerms.counts_lensing) + set_param(state, "counts_velocity", params.SourceTerms.counts_velocity) + set_param(state, "counts_radial", params.SourceTerms.counts_radial) + set_param(state, "counts_timedelay", params.SourceTerms.counts_timedelay) + set_param(state, "counts_ISW", params.SourceTerms.counts_ISW) + set_param(state, "counts_potential", params.SourceTerms.counts_potential) + set_param(state, "counts_evolve", params.SourceTerms.counts_evolve) + set_param(state, "line_basic", params.SourceTerms.line_basic) + set_param(state, "line_distortions", params.SourceTerms.line_distortions) + set_param(state, "line_extra", params.SourceTerms.line_extra) + set_param(state, "line_phot_dipole", params.SourceTerms.line_phot_dipole) + set_param(state, "line_phot_quadrupole", params.SourceTerms.line_phot_quadrupole) + set_param(state, "line_reionization", params.SourceTerms.line_reionization) + set_param(state, "use_mK", params.SourceTerms.use_21cm_mK) + set_param(state, "Kmax_Boost", params.Accuracy.KmaxBoost) + + if not params.SourceWindows: + set_param(state, "num_redshiftwindows", 0) + return + + if any(not isinstance(window, sources.GaussianSourceWindow) for window in params.SourceWindows): + raise CAMBValueError("write_ini only supports GaussianSourceWindow") + + set_param(state, "num_redshiftwindows", len(params.SourceWindows)) + for index, window in enumerate(params.SourceWindows, start=1): + set_param(state, f"redshift({index})", window.redshift) + set_param(state, f"redshift_kind({index})", window.source_type) + if window.source_type == "21cm": + set_param(state, f"redshift_sigma_Mhz({index})", window.sigma * constants.f_21cm / 1e6) + else: + set_param(state, f"redshift_sigma({index})", window.sigma) + if window.source_type == "counts": + set_param(state, f"redshift_bias({index})", window.bias) + set_param(state, f"redshift_dlog10Ndm({index})", window.dlog10Ndm) + + +def _update_ini_state_from_params(params: model.CAMBparams, state: CambIniFile) -> None: + set_param(state, "get_scalar_cls", params.WantScalars) + set_param(state, "get_vector_cls", params.WantVectors) + set_param(state, "get_tensor_cls", params.WantTensors) + set_param(state, "want_CMB", params.Want_CMB) + set_param(state, "want_CMB_lensing", params.Want_CMB_lensing) + set_param(state, "get_transfer", params.WantTransfer) + set_param(state, "do_nonlinear", _nonlinear_mode_value(params.NonLinear)) + set_param(state, "evolve_baryon_cs", params.Evolve_baryon_cs) + set_param(state, "evolve_delta_xe", params.Evolve_delta_xe) + set_param(state, "evolve_delta_ts", params.Evolve_delta_Ts) + set_param(state, "l_min", params.min_l) + set_param(state, "use_physical", True) + set_param(state, "hubble", params.H0) + set_param(state, "ombh2", params.ombh2) + set_param(state, "omch2", params.omch2) + set_param(state, "omnuh2", params.omnuh2) + set_param(state, "omk", params.omk) + set_param(state, "temp_cmb", params.TCMB) + set_param(state, "helium_fraction", params.YHe) + set_param(state, "massless_neutrinos", params.num_nu_massless) + set_param(state, "nu_mass_eigenstates", params.nu_mass_eigenstates) + if params.nu_mass_eigenstates: + _set_ini_sequence(state, "massive_neutrinos", params.nu_mass_numbers[: params.nu_mass_eigenstates]) + else: + set_param(state, "massive_neutrinos", 0) + set_param(state, "share_delta_neff", params.share_delta_neff) + if params.num_nu_massive > 0: + if not params.share_delta_neff: + _set_ini_sequence(state, "nu_mass_degeneracies", params.nu_mass_degeneracies[: params.nu_mass_eigenstates]) + _set_ini_sequence(state, "nu_mass_fractions", params.nu_mass_fractions[: params.nu_mass_eigenstates]) + set_param(state, "Alens", params.Alens) + set_param(state, "derived_parameters", params.WantDerivedParameters) + set_param(state, "accurate_polarization", params.Accuracy.AccuratePolarization) + set_param(state, "accurate_reionization", params.Accuracy.AccurateReionization) + set_param(state, "accurate_BB", params.Accuracy.AccurateBB) + set_param(state, "accuracy_boost", params.Accuracy.AccuracyBoost) + set_param(state, "l_accuracy_boost", params.Accuracy.lAccuracyBoost) + set_param(state, "l_sample_boost", params.Accuracy.lSampleBoost) + set_param(state, "do_late_rad_truncation", params.DoLateRadTruncation) + set_param(state, "massive_nu_approx", _massive_nu_method_value(params.MassiveNuMethod)) + + if params.WantCls and (params.WantScalars or params.WantVectors): + set_param(state, "l_max_scalar", params.max_l) + set_param(state, "k_eta_max_scalar", params.max_eta_k) + if params.WantScalars: + set_param(state, "do_lensing", params.DoLensing) + if params.WantCls and params.WantTensors: + set_param(state, "l_max_tensor", params.max_l_tensor) + set_param(state, "k_eta_max_tensor", params.max_eta_k_tensor) + + _update_dark_energy_ini(params, state) + _update_reionization_ini(params, state) + _update_initial_power_ini(params, state) + _update_recombination_ini(params, state) + _update_source_terms_ini(params, state) + + if params.WantTransfer: + set_param(state, "transfer_high_precision", params.Transfer.high_precision) + set_param(state, "accurate_massive_neutrino_transfers", params.Transfer.accurate_massive_neutrinos) + set_param(state, "transfer_kmax", params.Transfer.kmax / (params.H0 / 100.0)) + set_param(state, "transfer_k_per_logint", params.Transfer.k_per_logint) + set_param(state, "transfer_num_redshifts", params.Transfer.PK_num_redshifts) + for index, redshift in enumerate(params.Transfer.PK_redshifts[: params.Transfer.PK_num_redshifts], start=1): + set_param(state, f"transfer_redshift({index})", redshift) + set_param(state, "transfer_21cm_cl", params.transfer_21cm_cl) + + if isinstance(params.NonLinearModel, nonlinear.Halofit): + set_param(state, "halofit_version", nonlinear.halofit_version_names[params.NonLinearModel.halofit_version]) + set_param(state, "HMCode_A_baryon", params.NonLinearModel.HMCode_A_baryon) + set_param(state, "HMCode_eta_baryon", params.NonLinearModel.HMCode_eta_baryon) + set_param(state, "HMcode_logT_AGN", params.NonLinearModel.HMCode_logT_AGN) + elif params.NonLinear != model.NonLinear_none: + raise CAMBValueError( + f"write_ini does not support non-linear model class {params.NonLinearModel.__class__.__name__}" + ) + + set_param(state, "initial_condition", _scalar_initial_condition_value(params.scalar_initial_condition)) + if params.scalar_initial_condition == "initial_vector": + _set_ini_sequence(state, "initial_vector", params.InitialConditionVector) + set_param(state, "use_cl_spline_template", params.use_cl_spline_template) + + +def write_ini( + params: model.CAMBparams, + ini_filename, + *, + validate: bool = True, +) -> None: + if not isinstance(params, model.CAMBparams): + raise TypeError("params must be an instance of CAMBparams") + + ini_path = os.fspath(ini_filename) + state = CambIniFile() + _update_ini_state_from_params(params, state) + state.saveFile(ini_path) + if validate: + from .camb import read_ini + + reparsed = read_ini(ini_path) + if repr(reparsed) != repr(params): + raise CAMBValueError(f"Saved ini did not round-trip via read_ini ({ini_path})") diff --git a/camb/camb.py b/camb/camb.py index 3dc8db11..dedfece2 100644 --- a/camb/camb.py +++ b/camb/camb.py @@ -1,491 +1,491 @@ -import ctypes -import logging -import numbers -import os -from ctypes import POINTER, byref, c_bool, c_double -from inspect import FullArgSpec, getfullargspec - -from . import constants, model -from ._config import config -from .baseconfig import CAMBError as CAMBError -from .baseconfig import CAMBUnknownArgumentError, CAMBValueError, camblib, filepath_to_fortran, np -from .model import CAMBparams -from .results import CAMBdata -from .results import ClTransferData as ClTransferData -from .results import MatterTransferData as MatterTransferData - -logger = logging.getLogger(__name__) - -_debug_params = False -_setter_spec_cache: dict[object, FullArgSpec] = {} - - -def _get_setter_spec(setter) -> FullArgSpec: - func = getattr(setter, "__func__", setter) - try: - return _setter_spec_cache[func] - except KeyError: - spec = getfullargspec(func) - _setter_spec_cache[func] = spec - return spec - - -def set_feedback_level(level=1): - """ - Set the feedback level for internal CAMB calls - - :param level: zero for nothing, >1 for more - """ - config.FeedbackLevel = level - - -def get_results(params): - """ - Calculate results for specified parameters and return :class:`~.results.CAMBdata` instance for getting results. - - :param params: :class:`.model.CAMBparams` instance - :return: :class:`~.results.CAMBdata` instance - """ - if isinstance(params, dict): - params = set_params(**params) - res = CAMBdata() - if _debug_params: - print(params) - res.calc_power_spectra(params) - return res - - -def get_transfer_functions(params, only_time_sources=False): - """ - Calculate transfer functions for specified parameters and return :class:`~.results.CAMBdata` instance for - getting results and subsequently calculating power spectra. - - :param params: :class:`.model.CAMBparams` instance - :param only_time_sources: does not calculate the CMB l,k transfer functions and does not apply any non-linear - correction scaling. Results with only_time_sources=True can therefore be used with - different initial power spectra to get consistent non-linear lensed spectra. - :return: :class:`~.results.CAMBdata` instance - """ - - res = CAMBdata() - res.calc_transfers(params, only_transfers=True, only_time_sources=only_time_sources) - return res - - -def get_background(params, no_thermo=False): - """ - Calculate background cosmology for specified parameters and return :class:`~.results.CAMBdata`, ready to get derived - parameters and use background functions like :func:`~results.CAMBdata.angular_diameter_distance`. - - :param params: :class:`.model.CAMBparams` instance - :param no_thermo: set True if thermal and ionization history not required. - :return: :class:`~.results.CAMBdata` instance - """ - - res = CAMBdata() - if no_thermo: - res.calc_background_no_thermo(params) - else: - res.calc_background(params) - return res - - -def get_age(params): - """ - Get age of universe for given set of parameters - - :param params: :class:`.model.CAMBparams` instance - :return: age of universe in Julian gigayears - """ - return CAMB_GetAge(byref(params)) - - -def get_zre_from_tau(params, tau): - """ - Get reionization redshift given optical depth tau - - :param params: :class:`.model.CAMBparams` instance - :param tau: optical depth - :return: reionization redshift (or negative number if error) - """ - return params.Reion.get_zre(params, tau) - - -def set_params(cp=None, verbose=False, **params): - """ - - Set all CAMB parameters at once, including parameters which are part of the - CAMBparams structure, as well as global parameters. - - E.g.:: - - cp = camb.set_params( - ns=1, - H0=67, - ombh2=0.022, - omch2=0.1, - w=-0.95, - Alens=1.2, - lmax=2000, - WantTransfer=True, - dark_energy_model="DarkEnergyPPF", - ) - - This is equivalent to:: - - cp = model.CAMBparams() - cp.DarkEnergy = DarkEnergyPPF() - cp.DarkEnergy.set_params(w=-0.95) - cp.set_cosmology(H0=67, omch2=0.1, ombh2=0.022, Alens=1.2) - cp.set_for_lmax(lmax=2000) - cp.InitPower.set_params(ns=1) - cp.WantTransfer = True - - The wrapped functions are (in this order): - - * :meth:`.model.CAMBparams.set_accuracy` - * :meth:`.model.CAMBparams.set_classes` - * :meth:`.dark_energy.DarkEnergyEqnOfState.set_params` (or equivalent if a different dark energy model class used) - * :meth:`.reionization.TanhReionization.set_extra_params` (or equivalent if a different reionization class used) - * :meth:`.model.CAMBparams.set_cosmology` - * :meth:`.model.CAMBparams.set_matter_power` - * :meth:`.model.CAMBparams.set_for_lmax` - * :meth:`.initialpower.InitialPowerLaw.set_params` (or equivalent if a different initial power model class used) - * :meth:`.nonlinear.Halofit.set_params` - - :param params: the values of the parameters - :param cp: use this CAMBparams instead of creating a new one - :param verbose: print out the equivalent set of commands - :return: :class:`.model.CAMBparams` instance - - """ - - if "ALens" in params: - raise ValueError("Use Alens not ALens") - - if cp is None: - cp = model.CAMBparams() - else: - assert isinstance(cp, model.CAMBparams), "cp should be an instance of CAMBparams" - - used_params = set() - - def do_set(setter): - kwargs = {kk: params[kk] for kk in _get_setter_spec(setter).args[1:] if kk in params} - used_params.update(kwargs) - if kwargs: - if verbose: - logger.warning(f"Calling {setter.__name__}(**{kwargs})") - setter(**kwargs) - - # Note order is important: must call DarkEnergy.set_params before set_cosmology if setting theta rather than H0 - # set_classes allows redefinition of the classes used, so must be called before setting class parameters - do_set(cp.set_accuracy) - do_set(cp.set_classes) - do_set(cp.DarkEnergy.set_params) - do_set(cp.Reion.set_extra_params) - do_set(cp.set_cosmology) - do_set(cp.set_matter_power) - do_set(cp.set_for_lmax) - do_set(cp.InitPower.set_params) - do_set(cp.NonLinearModel.set_params) - - if cp.InitPower.has_tensors(): - cp.WantTensors = True - - if unused_params := set(params) - used_params: - if "share_delta_neff" in unused_params: - logger.warning( - "share_delta_neff is deprecated in python interface, " - "use delta_neff is only for backward compatibility with .ini files" - ) - - for k in unused_params: - obj = cp - if "." in k: - parts = k.split(".") - for p in parts[:-1]: - obj = getattr(obj, p) - par = parts[-1] - else: - par = k - if hasattr(obj, par): - setattr(obj, par, params[k]) - else: - raise CAMBUnknownArgumentError(f"Unrecognized parameter: {k}") - return cp - - -def get_valid_numerical_params(transfer_only=False, **class_names): - """ - Get numerical parameter names that are valid input to :func:`set_params` - - :param transfer_only: if True, exclude parameters that affect only initial power spectrum or non-linear model - :param class_names: class name parameters that will be used by :meth:`.model.CAMBparams.set_classes` - :return: set of valid input parameter names for :func:`set_params` - """ - cp = CAMBparams() - cp.set_classes(**class_names) - params = set() - - def extract_params(set_func): - pars = _get_setter_spec(set_func) - for arg in pars.args[1 : len(pars.args) - len(pars.defaults or [])]: - params.add(arg) - if pars.defaults: - for arg, v in zip(pars.args[len(pars.args) - len(pars.defaults) :], pars.defaults): - if (isinstance(v, numbers.Number) and not isinstance(v, bool) or v is None) and "version" not in arg: - params.add(arg) - - extract_params(cp.DarkEnergy.set_params) - extract_params(cp.Reion.set_extra_params) - extract_params(cp.set_cosmology) - if not transfer_only: - extract_params(cp.InitPower.set_params) - extract_params(cp.NonLinearModel.set_params) - # noinspection PyProtectedMember - for f, tp, *_ in cp._fields_: - if not f.startswith("_") and tp == ctypes.c_double: - params.add(f) - return params - { - "max_eta_k_tensor", - "max_eta_k", - "neutrino_hierarchy", - "standard_neutrino_neff", - "setter_H0", - "pivot_scalar", - "pivot_tensor", - "num_massive_neutrinos", - "num_nu_massless", - "bbn_predictor", - } - - -def set_params_cosmomc( - p, - num_massive_neutrinos=1, - neutrino_hierarchy="degenerate", - halofit_version="mead", - dark_energy_model="ppf", - lmax=2500, - lens_potential_accuracy=1, - inpars=None, -): - """ - get CAMBParams for dictionary of cosmomc-named parameters assuming Planck 2018 defaults - - :param p: dictionary of cosmomc parameters (e.g. from getdist.types.BestFit's getParamDict() function) - :param num_massive_neutrinos: usually 1 if fixed mnu=0.06 eV, three if mnu varying - :param neutrino_hierarchy: hierarchy - :param halofit_version: name of the specific Halofit model to use for non-linear modelling - :param dark_energy_model: ppf or fluid dark energy model - :param lmax: lmax for accuracy settings - :param lens_potential_accuracy: lensing accuracy parameter - :param inpars: optional input CAMBParams to set - :return: - """ - pars = inpars or model.CAMBparams() - if p.get("alpha1", 0) or p.get("Aphiphi", 1) != 1: - raise ValueError("Parameter not currently supported by set_params_cosmomc") - - pars.set_dark_energy(w=p.get("w", -1), wa=p.get("wa", 0), dark_energy_model=dark_energy_model) - pars.Reion.set_extra_params(deltazrei=p.get("deltazrei", None)) - pars.set_cosmology( - H0=p["H0"], - ombh2=p["omegabh2"], - omch2=p["omegach2"], - mnu=p.get("mnu", 0.06), - omk=p.get("omegak", 0), - tau=p["tau"], - nnu=p.get("nnu", constants.default_nnu), - Alens=p.get("Alens", 1.0), - YHe=p.get("yheused", None), - meffsterile=p.get("meffsterile", 0), - num_massive_neutrinos=num_massive_neutrinos, - neutrino_hierarchy=neutrino_hierarchy, - ) - pars.InitPower.set_params( - ns=p["ns"], r=p.get("r", 0), As=p["A"] * 1e-9, nrun=p.get("nrun", 0), nrunrun=p.get("nrunrun", 0) - ) - pars.set_for_lmax(lmax, lens_potential_accuracy=lens_potential_accuracy) - pars.NonLinearModel.set_params(halofit_version=halofit_version) - pars.WantTensors = pars.InitPower.has_tensors() - return pars - - -def validate_ini_file(filename): - # Check if fortran .ini file parameters are valid; catch error stop in separate process - import subprocess - import sys - - try: - err = "" - command = [ - sys.executable, - os.path.join(os.path.dirname(__file__), "_command_line.py"), - filename, - "--validate", - ] - subprocess.run(command, capture_output=True, text=True, check=True) - except subprocess.CalledProcessError as error: - err = "\n".join(filter(None, [error.stdout, error.stderr])).replace("ERROR STOP", "").strip() - if err: - raise CAMBValueError(err + f" ({filename})") - return True - - -def write_ini(params: CAMBparams, ini_filename, validate=True): - return params.write_ini(ini_filename, validate=validate) - - -def run_ini(ini_filename, no_validate=False): - """ - Run the command line camb from a .ini file (producing text files as with the command line program). - This does the same as the command line program, except global config parameters are not read and set (which does not - change results in almost all cases). - - :param ini_filename: .ini file to use - :param no_validate: do not pre-validate the ini file (faster, but may crash kernel if error) - """ - if not os.path.exists(ini_filename): - raise CAMBValueError(f"File not found: {ini_filename}") - if not no_validate: - validate_ini_file(ini_filename) - run_inifile = camblib.__camb_MOD_camb_runinifile - run_inifile.argtypes = [ctypes.c_char_p, POINTER(ctypes.c_long)] - run_inifile.restype = c_bool - s, path_len = filepath_to_fortran(ini_filename) - if not run_inifile(s, path_len): - config.check_global_error("run_ini") - - -def read_ini(ini_filename, no_validate=False): - """ - Get a :class:`.model.CAMBparams` instance using parameter specified in a .ini parameter file. - - :param ini_filename: path of the .ini file to read, or a full URL to download from - :param no_validate: do not pre-validate the ini file (faster, but may crash kernel if error) - :return: :class:`.model.CAMBparams` instance - """ - if ini_filename.startswith("http"): - import tempfile - - try: - import requests - except ImportError: - raise ImportError("install 'requests' package, required for reading ini files from URLs") - - data = requests.get(ini_filename) - with tempfile.NamedTemporaryFile(suffix=".ini", delete=False) as f: - ini_filename = f.name - with open(ini_filename, "wb") as file: - file.write(data.content) - else: - data = None - if not os.path.exists(ini_filename): - raise CAMBValueError(f"File not found: {ini_filename}") - try: - if not no_validate: - validate_ini_file(ini_filename) - cp = model.CAMBparams() - read_inifile = camblib.__camb_MOD_camb_readparamfile - read_inifile.argtypes = [POINTER(CAMBparams), ctypes.c_char_p, POINTER(ctypes.c_long)] - read_inifile.restype = ctypes.c_bool - s, path_len = filepath_to_fortran(ini_filename) - if not read_inifile(cp, s, path_len): - config.check_global_error("read_ini") - finally: - if data: - os.unlink(ini_filename) - return cp - - -def get_matter_power_interpolator( - params, - zmin=0.0, - zmax=10.0, - nz_step=100, - zs=None, - kmax=10.0, - nonlinear=True, - var1=None, - var2=None, - hubble_units=True, - k_hunit=True, - return_z_k=False, - k_per_logint=None, - log_interp=True, - extrap_kmax=None, -): - r""" - Return a 2D spline interpolation object to evaluate matter power spectrum as function of z and k/h, e.g. - - .. code-block:: python - - from camb import get_matter_power_interpolator - - PK = get_matter_power_interpolator(params) - print("Power spectrum at z=0.5, k/h=0.1/Mpc is %s (Mpc/h)^3 " % (PK.P(0.5, 0.1))) - - For a description of outputs for different var1, var2 see :ref:`transfer-variables`. - - This function re-calculates results from scratch with the given parameters. - If you already have a :class:`~.results.CAMBdata` result object, you should instead - use :meth:`~.results.CAMBdata.get_matter_power_interpolator` - (call :meth:`.model.CAMBparams.set_matter_power` as need to set up the required ranges for the matter power - before calling get_results). - - :param params: :class:`.model.CAMBparams` instance - :param zmin: minimum z (use 0 or smaller than you want for good interpolation) - :param zmax: maximum z (use larger than you want for good interpolation) - :param nz_step: number of steps to sample in z (default max allowed is 100) - :param zs: instead of zmin,zmax, nz_step, can specific explicit array of z values to spline from - :param kmax: maximum k - :param nonlinear: include non-linear correction from halo model - :param var1: variable i (index, or name of variable; default delta_tot) - :param var2: variable j (index, or name of variable; default delta_tot) - :param hubble_units: if true, output power spectrum in :math:`({\rm Mpc}/h)^{3}` units, - otherwise :math:`{\rm Mpc}^{3}` - :param k_hunit: if true, matter power is a function of k/h, if false, just k (both :math:`{\rm Mpc}^{-1}` units) - :param return_z_k: if true, return interpolator, z, k where z, k are the grid used - :param k_per_logint: specific uniform sampling over log k (if not set, uses optimized irregular sampling) - :param log_interp: if true, interpolate log of power spectrum (unless any values are negative in which case ignored) - :param extrap_kmax: if set, use power law extrapolation beyond kmax to extrap_kmax (useful for tails of integrals) - :return: An object PK based on :class:`~scipy:scipy.interpolate.RectBivariateSpline`, that can be called - with PK.P(z,kh) or PK(z,log(kh)) to get log matter power values. - If return_z_k=True, instead return interpolator, z, k where z, k are the grid used. - """ - - pars = params.copy() - - if zs is None: - zs = zmin + np.exp(np.log(zmax - zmin + 1) * np.linspace(0, 1, nz_step)) - 1 - pars.set_matter_power(redshifts=zs, kmax=kmax, k_per_logint=k_per_logint, silent=True) - pars.NonLinear = model.NonLinear_none - results = get_results(pars) - - return results.get_matter_power_interpolator( - nonlinear=nonlinear, - var1=var1, - var2=var2, - hubble_units=hubble_units, - k_hunit=k_hunit, - return_z_k=return_z_k, - log_interp=log_interp, - extrap_kmax=extrap_kmax, - ) - - -def free_global_memory(): - """ - Clean up all globally allocated Fortran memory. - - This function calls various Fortran cleanup routines to deallocate - global allocatable arrays, particularly useful for memory leak testing. - """ - camblib.__camb_MOD_camb_freeglobalmemory() - - -CAMB_GetAge = camblib.__camb_MOD_camb_getage -CAMB_GetAge.restype = c_double -CAMB_GetAge.argtypes = [POINTER(model.CAMBparams)] +import ctypes +import logging +import numbers +import os +from ctypes import POINTER, byref, c_bool, c_double +from inspect import FullArgSpec, getfullargspec + +from . import constants, model +from ._config import config +from .baseconfig import CAMBError as CAMBError +from .baseconfig import CAMBUnknownArgumentError, CAMBValueError, camblib, filepath_to_fortran, np +from .model import CAMBparams +from .results import CAMBdata +from .results import ClTransferData as ClTransferData +from .results import MatterTransferData as MatterTransferData + +logger = logging.getLogger(__name__) + +_debug_params = False +_setter_spec_cache: dict[object, FullArgSpec] = {} + + +def _get_setter_spec(setter) -> FullArgSpec: + func = getattr(setter, "__func__", setter) + try: + return _setter_spec_cache[func] + except KeyError: + spec = getfullargspec(func) + _setter_spec_cache[func] = spec + return spec + + +def set_feedback_level(level=1): + """ + Set the feedback level for internal CAMB calls + + :param level: zero for nothing, >1 for more + """ + config.FeedbackLevel = level + + +def get_results(params): + """ + Calculate results for specified parameters and return :class:`~.results.CAMBdata` instance for getting results. + + :param params: :class:`.model.CAMBparams` instance + :return: :class:`~.results.CAMBdata` instance + """ + if isinstance(params, dict): + params = set_params(**params) + res = CAMBdata() + if _debug_params: + print(params) + res.calc_power_spectra(params) + return res + + +def get_transfer_functions(params, only_time_sources=False): + """ + Calculate transfer functions for specified parameters and return :class:`~.results.CAMBdata` instance for + getting results and subsequently calculating power spectra. + + :param params: :class:`.model.CAMBparams` instance + :param only_time_sources: does not calculate the CMB l,k transfer functions and does not apply any non-linear + correction scaling. Results with only_time_sources=True can therefore be used with + different initial power spectra to get consistent non-linear lensed spectra. + :return: :class:`~.results.CAMBdata` instance + """ + + res = CAMBdata() + res.calc_transfers(params, only_transfers=True, only_time_sources=only_time_sources) + return res + + +def get_background(params, no_thermo=False): + """ + Calculate background cosmology for specified parameters and return :class:`~.results.CAMBdata`, ready to get derived + parameters and use background functions like :func:`~results.CAMBdata.angular_diameter_distance`. + + :param params: :class:`.model.CAMBparams` instance + :param no_thermo: set True if thermal and ionization history not required. + :return: :class:`~.results.CAMBdata` instance + """ + + res = CAMBdata() + if no_thermo: + res.calc_background_no_thermo(params) + else: + res.calc_background(params) + return res + + +def get_age(params): + """ + Get age of universe for given set of parameters + + :param params: :class:`.model.CAMBparams` instance + :return: age of universe in Julian gigayears + """ + return CAMB_GetAge(byref(params)) + + +def get_zre_from_tau(params, tau): + """ + Get reionization redshift given optical depth tau + + :param params: :class:`.model.CAMBparams` instance + :param tau: optical depth + :return: reionization redshift (or negative number if error) + """ + return params.Reion.get_zre(params, tau) + + +def set_params(cp=None, verbose=False, **params): + """ + + Set all CAMB parameters at once, including parameters which are part of the + CAMBparams structure, as well as global parameters. + + E.g.:: + + cp = camb.set_params( + ns=1, + H0=67, + ombh2=0.022, + omch2=0.1, + w=-0.95, + Alens=1.2, + lmax=2000, + WantTransfer=True, + dark_energy_model="DarkEnergyPPF", + ) + + This is equivalent to:: + + cp = model.CAMBparams() + cp.DarkEnergy = DarkEnergyPPF() + cp.DarkEnergy.set_params(w=-0.95) + cp.set_cosmology(H0=67, omch2=0.1, ombh2=0.022, Alens=1.2) + cp.set_for_lmax(lmax=2000) + cp.InitPower.set_params(ns=1) + cp.WantTransfer = True + + The wrapped functions are (in this order): + + * :meth:`.model.CAMBparams.set_accuracy` + * :meth:`.model.CAMBparams.set_classes` + * :meth:`.dark_energy.DarkEnergyEqnOfState.set_params` (or equivalent if a different dark energy model class used) + * :meth:`.reionization.TanhReionization.set_extra_params` (or equivalent if a different reionization class used) + * :meth:`.model.CAMBparams.set_cosmology` + * :meth:`.model.CAMBparams.set_matter_power` + * :meth:`.model.CAMBparams.set_for_lmax` + * :meth:`.initialpower.InitialPowerLaw.set_params` (or equivalent if a different initial power model class used) + * :meth:`.nonlinear.Halofit.set_params` + + :param params: the values of the parameters + :param cp: use this CAMBparams instead of creating a new one + :param verbose: print out the equivalent set of commands + :return: :class:`.model.CAMBparams` instance + + """ + + if "ALens" in params: + raise ValueError("Use Alens not ALens") + + if cp is None: + cp = model.CAMBparams() + else: + assert isinstance(cp, model.CAMBparams), "cp should be an instance of CAMBparams" + + used_params = set() + + def do_set(setter): + kwargs = {kk: params[kk] for kk in _get_setter_spec(setter).args[1:] if kk in params} + used_params.update(kwargs) + if kwargs: + if verbose: + logger.warning(f"Calling {setter.__name__}(**{kwargs})") + setter(**kwargs) + + # Note order is important: must call DarkEnergy.set_params before set_cosmology if setting theta rather than H0 + # set_classes allows redefinition of the classes used, so must be called before setting class parameters + do_set(cp.set_accuracy) + do_set(cp.set_classes) + do_set(cp.DarkEnergy.set_params) + do_set(cp.Reion.set_extra_params) + do_set(cp.set_cosmology) + do_set(cp.set_matter_power) + do_set(cp.set_for_lmax) + do_set(cp.InitPower.set_params) + do_set(cp.NonLinearModel.set_params) + + if cp.InitPower.has_tensors(): + cp.WantTensors = True + + if unused_params := set(params) - used_params: + if "share_delta_neff" in unused_params: + logger.warning( + "share_delta_neff is deprecated in python interface, " + "use delta_neff is only for backward compatibility with .ini files" + ) + + for k in unused_params: + obj = cp + if "." in k: + parts = k.split(".") + for p in parts[:-1]: + obj = getattr(obj, p) + par = parts[-1] + else: + par = k + if hasattr(obj, par): + setattr(obj, par, params[k]) + else: + raise CAMBUnknownArgumentError(f"Unrecognized parameter: {k}") + return cp + + +def get_valid_numerical_params(transfer_only=False, **class_names): + """ + Get numerical parameter names that are valid input to :func:`set_params` + + :param transfer_only: if True, exclude parameters that affect only initial power spectrum or non-linear model + :param class_names: class name parameters that will be used by :meth:`.model.CAMBparams.set_classes` + :return: set of valid input parameter names for :func:`set_params` + """ + cp = CAMBparams() + cp.set_classes(**class_names) + params = set() + + def extract_params(set_func): + pars = _get_setter_spec(set_func) + for arg in pars.args[1 : len(pars.args) - len(pars.defaults or [])]: + params.add(arg) + if pars.defaults: + for arg, v in zip(pars.args[len(pars.args) - len(pars.defaults) :], pars.defaults): + if (isinstance(v, numbers.Number) and not isinstance(v, bool) or v is None) and "version" not in arg: + params.add(arg) + + extract_params(cp.DarkEnergy.set_params) + extract_params(cp.Reion.set_extra_params) + extract_params(cp.set_cosmology) + if not transfer_only: + extract_params(cp.InitPower.set_params) + extract_params(cp.NonLinearModel.set_params) + # noinspection PyProtectedMember + for f, tp, *_ in cp._fields_: + if not f.startswith("_") and tp == ctypes.c_double: + params.add(f) + return params - { + "max_eta_k_tensor", + "max_eta_k", + "neutrino_hierarchy", + "standard_neutrino_neff", + "setter_H0", + "pivot_scalar", + "pivot_tensor", + "num_massive_neutrinos", + "num_nu_massless", + "bbn_predictor", + } + + +def set_params_cosmomc( + p, + num_massive_neutrinos=1, + neutrino_hierarchy="degenerate", + halofit_version="mead", + dark_energy_model="ppf", + lmax=2500, + lens_potential_accuracy=1, + inpars=None, +): + """ + get CAMBParams for dictionary of cosmomc-named parameters assuming Planck 2018 defaults + + :param p: dictionary of cosmomc parameters (e.g. from getdist.types.BestFit's getParamDict() function) + :param num_massive_neutrinos: usually 1 if fixed mnu=0.06 eV, three if mnu varying + :param neutrino_hierarchy: hierarchy + :param halofit_version: name of the specific Halofit model to use for non-linear modelling + :param dark_energy_model: ppf or fluid dark energy model + :param lmax: lmax for accuracy settings + :param lens_potential_accuracy: lensing accuracy parameter + :param inpars: optional input CAMBParams to set + :return: + """ + pars = inpars or model.CAMBparams() + if p.get("alpha1", 0) or p.get("Aphiphi", 1) != 1: + raise ValueError("Parameter not currently supported by set_params_cosmomc") + + pars.set_dark_energy(w=p.get("w", -1), wa=p.get("wa", 0), dark_energy_model=dark_energy_model) + pars.Reion.set_extra_params(deltazrei=p.get("deltazrei", None)) + pars.set_cosmology( + H0=p["H0"], + ombh2=p["omegabh2"], + omch2=p["omegach2"], + mnu=p.get("mnu", 0.06), + omk=p.get("omegak", 0), + tau=p["tau"], + nnu=p.get("nnu", constants.default_nnu), + Alens=p.get("Alens", 1.0), + YHe=p.get("yheused", None), + meffsterile=p.get("meffsterile", 0), + num_massive_neutrinos=num_massive_neutrinos, + neutrino_hierarchy=neutrino_hierarchy, + ) + pars.InitPower.set_params( + ns=p["ns"], r=p.get("r", 0), As=p["A"] * 1e-9, nrun=p.get("nrun", 0), nrunrun=p.get("nrunrun", 0) + ) + pars.set_for_lmax(lmax, lens_potential_accuracy=lens_potential_accuracy) + pars.NonLinearModel.set_params(halofit_version=halofit_version) + pars.WantTensors = pars.InitPower.has_tensors() + return pars + + +def validate_ini_file(filename): + # Check if fortran .ini file parameters are valid; catch error stop in separate process + import subprocess + import sys + + try: + err = "" + command = [ + sys.executable, + os.path.join(os.path.dirname(__file__), "_command_line.py"), + filename, + "--validate", + ] + subprocess.run(command, capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as error: + err = "\n".join(filter(None, [error.stdout, error.stderr])).replace("ERROR STOP", "").strip() + if err: + raise CAMBValueError(err + f" ({filename})") + return True + + +def write_ini(params: CAMBparams, ini_filename, validate=True): + return params.write_ini(ini_filename, validate=validate) + + +def run_ini(ini_filename, no_validate=False): + """ + Run the command line camb from a .ini file (producing text files as with the command line program). + This does the same as the command line program, except global config parameters are not read and set (which does not + change results in almost all cases). + + :param ini_filename: .ini file to use + :param no_validate: do not pre-validate the ini file (faster, but may crash kernel if error) + """ + if not os.path.exists(ini_filename): + raise CAMBValueError(f"File not found: {ini_filename}") + if not no_validate: + validate_ini_file(ini_filename) + run_inifile = camblib.__camb_MOD_camb_runinifile + run_inifile.argtypes = [ctypes.c_char_p, POINTER(ctypes.c_long)] + run_inifile.restype = c_bool + s, path_len = filepath_to_fortran(ini_filename) + if not run_inifile(s, path_len): + config.check_global_error("run_ini") + + +def read_ini(ini_filename, no_validate=False): + """ + Get a :class:`.model.CAMBparams` instance using parameter specified in a .ini parameter file. + + :param ini_filename: path of the .ini file to read, or a full URL to download from + :param no_validate: do not pre-validate the ini file (faster, but may crash kernel if error) + :return: :class:`.model.CAMBparams` instance + """ + if ini_filename.startswith("http"): + import tempfile + + try: + import requests + except ImportError: + raise ImportError("install 'requests' package, required for reading ini files from URLs") + + data = requests.get(ini_filename) + with tempfile.NamedTemporaryFile(suffix=".ini", delete=False) as f: + ini_filename = f.name + with open(ini_filename, "wb") as file: + file.write(data.content) + else: + data = None + if not os.path.exists(ini_filename): + raise CAMBValueError(f"File not found: {ini_filename}") + try: + if not no_validate: + validate_ini_file(ini_filename) + cp = model.CAMBparams() + read_inifile = camblib.__camb_MOD_camb_readparamfile + read_inifile.argtypes = [POINTER(CAMBparams), ctypes.c_char_p, POINTER(ctypes.c_long)] + read_inifile.restype = ctypes.c_bool + s, path_len = filepath_to_fortran(ini_filename) + if not read_inifile(cp, s, path_len): + config.check_global_error("read_ini") + finally: + if data: + os.unlink(ini_filename) + return cp + + +def get_matter_power_interpolator( + params, + zmin=0.0, + zmax=10.0, + nz_step=100, + zs=None, + kmax=10.0, + nonlinear=True, + var1=None, + var2=None, + hubble_units=True, + k_hunit=True, + return_z_k=False, + k_per_logint=None, + log_interp=True, + extrap_kmax=None, +): + r""" + Return a 2D spline interpolation object to evaluate matter power spectrum as function of z and k/h, e.g. + + .. code-block:: python + + from camb import get_matter_power_interpolator + + PK = get_matter_power_interpolator(params) + print("Power spectrum at z=0.5, k/h=0.1/Mpc is %s (Mpc/h)^3 " % (PK.P(0.5, 0.1))) + + For a description of outputs for different var1, var2 see :ref:`transfer-variables`. + + This function re-calculates results from scratch with the given parameters. + If you already have a :class:`~.results.CAMBdata` result object, you should instead + use :meth:`~.results.CAMBdata.get_matter_power_interpolator` + (call :meth:`.model.CAMBparams.set_matter_power` as need to set up the required ranges for the matter power + before calling get_results). + + :param params: :class:`.model.CAMBparams` instance + :param zmin: minimum z (use 0 or smaller than you want for good interpolation) + :param zmax: maximum z (use larger than you want for good interpolation) + :param nz_step: number of steps to sample in z (default max allowed is 100) + :param zs: instead of zmin,zmax, nz_step, can specific explicit array of z values to spline from + :param kmax: maximum k + :param nonlinear: include non-linear correction from halo model + :param var1: variable i (index, or name of variable; default delta_tot) + :param var2: variable j (index, or name of variable; default delta_tot) + :param hubble_units: if true, output power spectrum in :math:`({\rm Mpc}/h)^{3}` units, + otherwise :math:`{\rm Mpc}^{3}` + :param k_hunit: if true, matter power is a function of k/h, if false, just k (both :math:`{\rm Mpc}^{-1}` units) + :param return_z_k: if true, return interpolator, z, k where z, k are the grid used + :param k_per_logint: specific uniform sampling over log k (if not set, uses optimized irregular sampling) + :param log_interp: if true, interpolate log of power spectrum (unless any values are negative in which case ignored) + :param extrap_kmax: if set, use power law extrapolation beyond kmax to extrap_kmax (useful for tails of integrals) + :return: An object PK based on :class:`~scipy:scipy.interpolate.RectBivariateSpline`, that can be called + with PK.P(z,kh) or PK(z,log(kh)) to get log matter power values. + If return_z_k=True, instead return interpolator, z, k where z, k are the grid used. + """ + + pars = params.copy() + + if zs is None: + zs = zmin + np.exp(np.log(zmax - zmin + 1) * np.linspace(0, 1, nz_step)) - 1 + pars.set_matter_power(redshifts=zs, kmax=kmax, k_per_logint=k_per_logint, silent=True) + pars.NonLinear = model.NonLinear_none + results = get_results(pars) + + return results.get_matter_power_interpolator( + nonlinear=nonlinear, + var1=var1, + var2=var2, + hubble_units=hubble_units, + k_hunit=k_hunit, + return_z_k=return_z_k, + log_interp=log_interp, + extrap_kmax=extrap_kmax, + ) + + +def free_global_memory(): + """ + Clean up all globally allocated Fortran memory. + + This function calls various Fortran cleanup routines to deallocate + global allocatable arrays, particularly useful for memory leak testing. + """ + camblib.__camb_MOD_camb_freeglobalmemory() + + +CAMB_GetAge = camblib.__camb_MOD_camb_getage +CAMB_GetAge.restype = c_double +CAMB_GetAge.argtypes = [POINTER(model.CAMBparams)] diff --git a/camb/check_accuracy.py b/camb/check_accuracy.py new file mode 100644 index 00000000..0258225c --- /dev/null +++ b/camb/check_accuracy.py @@ -0,0 +1,2136 @@ +"""Check numerical stability of CAMB parameters against a higher-accuracy reference run. + +The command-line interface compares spectra and derived parameters from an input +``.ini`` file with a reference calculation using boosted accuracy settings. It +can also plot fractional differences, estimate a fiducial CMB delta chi-squared, +search for minimal top-level boosts, and refine which underlying component +accuracy settings are most relevant. + +Examples:: + + camb check_accuracy inifiles/params.ini + camb check_accuracy inifiles/params.ini --plot-dir accuracy_plots + camb check_accuracy inifiles/params.ini --find-minimal-boosts --refine-accuracy-components +""" + +from __future__ import annotations + +import argparse +import itertools +import math +import time +from collections.abc import Iterable +from dataclasses import dataclass +from pathlib import Path + +import numpy as np + +DEFAULT_ACCURACY_SETTINGS = { + "AccuracyBoost": 2.0, + "lSampleBoost": 2.0, + "lAccuracyBoost": 2.0, + "IntTolBoost": 2.0, + "DoLateRadTruncation": True, +} + +DEFAULT_NOISE_CONFIGS = { + "planck": { + "noise_muK_arcmin_T": 45.0, + "noise_muK_arcmin_P": 70.0, + "fwhm_arcmin": 7.0, + "fsky": 0.6, + }, + "so": { # optimistic extended 2034 numbers + "noise_muK_arcmin_T": 2.6, + "noise_muK_arcmin_P": 3.67, + "fwhm_arcmin": 1.4, + "fsky": 0.6, + }, +} + +TT_EE_TOLERANCES = [(0, 3e-3), (600, 1e-3), (3500, 3e-3), (6000, 2e-2)] +BB_TOLERANCES = [(0, 5e-3), (1000, 1e-2), (6000, 2e-2), (8000, 1e-1)] +LENSING_TOLERANCES = [(0, 5e-3), (2000, 5e-3), (6000, 2e-2)] +MPK_TOLERANCE = 1e-3 +DERIVED_TOLERANCE = 1e-3 + +CL_COLUMNS = {"TT": 0, "EE": 1, "BB": 2, "TE": 3} +OMEGA_K_FLAT = 5e-7 +BASE_ACCURACY_KEYS = ("AccuracyBoost", "lSampleBoost", "lAccuracyBoost", "IntTolBoost") +GLOBAL_BOOST_COMPONENT_KEYS = ( + "TimeStepBoost", + "BackgroundTimeStepBoost", + "TimeSwitchBoost", + "IntTolBoost", + "SourcekAccuracyBoost", + "IntkAccuracyBoost", + "TransferkBoost", + "NonFlatIntAccuracyBoost", + "BessIntBoost", + "LensingBoost", + "NonlinSourceBoost", + "BesselBoost", + "LimberBoost", + "SourceLimberBoost", + "KmaxBoost", + "neutrino_q_boost", +) +COMPONENT_REFINEMENT_KEYS = GLOBAL_BOOST_COMPONENT_KEYS +DISCRETE_ACCURACY_VALUES = {"neutrino_q_boost": (1.0, 1.5, 2.5)} + + +@dataclass(frozen=True) +class RangeTolerance: + start: int + stop: int | None + tolerance: float + + @property + def label(self) -> str: + if self.stop is None: + return f"L >= {self.start}" + return f"{self.start} <= L < {self.stop}" + + +@dataclass +class RunOutput: + label: str + params: object + results: object + cpu_time: float + wall_time: float + derived: dict[str, float] + lensed_cls: np.ndarray | None + lens_potential_cls: np.ndarray | None + matter_power: MatterPowerData | None + + +@dataclass(frozen=True) +class MatterPowerData: + k: np.ndarray + z: np.ndarray + pk: np.ndarray + requested_kmax: float + npoints: int + + +@dataclass +class StatRow: + quantity: str + range_label: str + max_abs: float + rms: float + tolerance: float + passed: bool + location: str = "" + + +@dataclass +class ComparisonResult: + derived_rows: list[StatRow] + cl_rows: list[StatRow] + lensing_rows: list[StatRow] + mpk_rows: list[StatRow] + + @property + def passed(self) -> bool: + rows = self.derived_rows + self.cl_rows + self.lensing_rows + self.mpk_rows + return all(row.passed for row in rows) + + @property + def worst_failure(self) -> StatRow | None: + failures = [ + row for row in self.derived_rows + self.cl_rows + self.lensing_rows + self.mpk_rows if not row.passed + ] + if not failures: + return None + return max(failures, key=stat_score) + + +def comparison_rows(comparison: ComparisonResult) -> list[StatRow]: + return comparison.derived_rows + comparison.cl_rows + comparison.lensing_rows + comparison.mpk_rows + + +def stat_score(row: StatRow) -> float: + if not math.isfinite(row.max_abs): + return math.inf + if row.tolerance == 0: + return 0.0 if row.max_abs == 0 else math.inf + score = abs(row.max_abs) / abs(row.tolerance) + return score if math.isfinite(score) else math.inf + + +def comparison_score(comparison: ComparisonResult) -> float: + rows = comparison_rows(comparison) + if not rows: + return 0.0 + return max(stat_score(row) for row in rows) + + +@dataclass(frozen=True) +class NoiseConfig: + name: str + noise_muK_arcmin_T: float + noise_muK_arcmin_P: float + fwhm_arcmin: float + fsky: float + lmin: int + lmax: int | None + fields: tuple[str, ...] + + +@dataclass(frozen=True) +class ChiSquaredResult: + delta_chi2: float + lmin: int + lmax: int + fields: tuple[str, ...] + fsky: float + noise_muK_arcmin_T: float + noise_muK_arcmin_P: float + fwhm_arcmin: float + worst_ell: int + worst_delta_chi2: float + + +@dataclass +class AccuracyCheckResult: + standard: RunOutput + reference: RunOutput + comparison: ComparisonResult + chi2: ChiSquaredResult | None = None + + +@dataclass(frozen=True) +class PassingCandidate: + settings: dict[str, float | bool] + comparison: ComparisonResult + run: RunOutput | None + + +@dataclass(frozen=True) +class SearchResult: + settings: dict[str, float | bool] + comparison: ComparisonResult + run: RunOutput | None = None + fastest: PassingCandidate | None = None + + def __iter__(self): + yield self.settings + yield self.comparison + + +def tolerance_ranges(spec: list[tuple[int, float]]) -> list[RangeTolerance]: + return [ + RangeTolerance(start, spec[index + 1][0] if index + 1 < len(spec) else None, tolerance) + for index, (start, tolerance) in enumerate(spec) + ] + + +def positive_int(value: str) -> int: + parsed = int(value) + if parsed < 1: + raise argparse.ArgumentTypeError("must be at least 1") + return parsed + + +def comma_separated_floats(value: str) -> list[float]: + values = [float(item.strip()) for item in value.split(",") if item.strip()] + if not values: + raise argparse.ArgumentTypeError("must contain at least one value") + return sorted(set(values)) + + +def cmb_fields(value: str) -> tuple[str, ...]: + fields = tuple(field.strip().upper() for field in value.replace(",", " ").split() if field.strip()) + allowed = {"T", "E", "B"} + if not fields or any(field not in allowed for field in fields): + raise argparse.ArgumentTypeError("fields must be a comma- or space-separated subset of T,E,B") + if len(set(fields)) != len(fields): + raise argparse.ArgumentTypeError("fields must not contain duplicates") + return fields + + +def build_parser(prog: str | None = None) -> argparse.ArgumentParser: + """Build the command-line parser for the accuracy checker.""" + parser = argparse.ArgumentParser( + prog=prog, description="Check stability of one CAMB ini file against a boosted-accuracy calculation." + ) + parser.add_argument("ini_file", help="CAMB ini file to load with camb.read_ini") + parser.add_argument( + "--no-validate", + action="store_true", + help="pass no_validate=True to camb.read_ini", + ) + parser.add_argument( + "--lmax", + type=positive_int, + help="maximum L to compare; default is common calculated lmax", + ) + parser.add_argument( + "--set-for-lmax", + type=positive_int, + help="call params.set_for_lmax before running; if --lmax is unset, this is also used for comparison", + ) + parser.add_argument( + "--lens-margin", + type=int, + help="lens margin to use for both runs; can be set with or without --set-for-lmax", + ) + parser.add_argument( + "--lens-potential-accuracy", + type=float, + help="lens_potential_accuracy to use for both runs; can be set with or without --set-for-lmax", + ) + parser.add_argument( + "--reference-lens-margin", + type=int, + help="lens margin override for the boosted reference only", + ) + parser.add_argument( + "--reference-lens-potential-accuracy", + type=float, + help="lens_potential_accuracy override for the boosted reference only", + ) + parser.add_argument( + "--mpk-npoints", + type=positive_int, + default=500, + help="number of k samples for matter power comparison grid", + ) + parser.add_argument( + "--mpk-kmin", + type=float, + default=1e-4, + help="minimum k/h for matter power comparison", + ) + parser.add_argument("--mpk-tolerance", type=float, default=None) + parser.add_argument("--derived-tolerance", type=float, default=DERIVED_TOLERANCE) + parser.add_argument( + "--plot-dir", + type=Path, + help="write plots of fractional errors to this directory", + ) + parser.add_argument( + "--chi2", + action="store_true", + help="calculate a fiducial Gaussian CMB delta chi-squared for standard vs boosted C_L", + ) + parser.add_argument( + "--chi2-config", + choices=tuple(DEFAULT_NOISE_CONFIGS) + ("custom",), + default="so", + help="noise configuration for --chi2", + ) + parser.add_argument("--chi2-lmin", type=positive_int, default=2) + parser.add_argument( + "--chi2-lmax", + type=positive_int, + help="maximum L for --chi2; defaults to comparison lmax", + ) + parser.add_argument("--chi2-fields", type=cmb_fields, default=("T", "E", "B")) + parser.add_argument("--noise-muk-arcmin-t", type=float, help="temperature white noise for --chi2") + parser.add_argument("--noise-muk-arcmin-p", type=float, help="polarization white noise for --chi2") + parser.add_argument("--beam-fwhm-arcmin", type=float, help="Gaussian beam FWHM for --chi2") + parser.add_argument("--fsky", type=float, help="sky fraction for --chi2") + parser.add_argument( + "--find-minimal-boosts", + action="store_true", + help="search for the lowest-cost boosted settings that pass against the high-accuracy run", + ) + parser.add_argument( + "--exhaustive-boost-search", + action="store_true", + help="continue boost search after a passing single-parameter path is found", + ) + parser.add_argument( + "--refine-accuracy-components", + action="store_true", + help="after --find-minimal-boosts, try AccuracyBoost=1 with underlying accuracy fields tweaked instead", + ) + parser.add_argument( + "--component-search-size", + type=positive_int, + default=4, + help="maximum number of component accuracy fields to try together", + ) + parser.add_argument( + "--search-grid", + type=comma_separated_floats, + help="optional comma-separated seed values for the four boost parameters; the default search does not need this", + ) + parser.add_argument( + "--search-tolerance", + type=float, + default=0.02, + help="target precision for refining numeric boost values", + ) + parser.add_argument( + "--max-search-runs", + type=positive_int, + help="maximum candidate runs for boost search", + ) + + parser.add_argument( + "--accuracy-boost", + type=float, + default=DEFAULT_ACCURACY_SETTINGS["AccuracyBoost"], + ) + parser.add_argument( + "--l-sample-boost", + type=float, + default=DEFAULT_ACCURACY_SETTINGS["lSampleBoost"], + ) + parser.add_argument( + "--l-accuracy-boost", + type=float, + default=DEFAULT_ACCURACY_SETTINGS["lAccuracyBoost"], + ) + parser.add_argument("--int-tol-boost", type=float, default=DEFAULT_ACCURACY_SETTINGS["IntTolBoost"]) + parser.add_argument( + "--do-late-rad-truncation", + action=argparse.BooleanOptionalAction, + default=DEFAULT_ACCURACY_SETTINGS["DoLateRadTruncation"], + ) + return parser + + +def requested_accuracy_settings(args: argparse.Namespace) -> dict[str, float | bool]: + return { + "AccuracyBoost": args.accuracy_boost, + "lSampleBoost": args.l_sample_boost, + "lAccuracyBoost": args.l_accuracy_boost, + "IntTolBoost": args.int_tol_boost, + "DoLateRadTruncation": args.do_late_rad_truncation, + } + + +def apply_accuracy_settings(params, settings: dict[str, float | bool], *, boost_from_raw: bool = False) -> None: + for key, setting in settings.items(): + if key == "DoLateRadTruncation": + params.DoLateRadTruncation = bool(setting) + continue + if not hasattr(params.Accuracy, key): + raise ValueError(f"Unknown accuracy setting {key}") + value = float(setting) + if boost_from_raw: + value = max(float(getattr(params.Accuracy, key)), value) + setattr(params.Accuracy, key, value) + + +def copy_params(params): + return params.copy() + + +def apply_lensing_settings( + params, + *, + set_for_lmax: int | None = None, + lens_margin: int | None = None, + lens_potential_accuracy: float | None = None, +) -> None: + if set_for_lmax is None and lens_margin is None and lens_potential_accuracy is None: + return + + lens_accuracy = 0.0 if lens_potential_accuracy is None else lens_potential_accuracy + if set_for_lmax is not None: + params.set_for_lmax( + set_for_lmax, + lens_margin=150 if lens_margin is None else lens_margin, + lens_potential_accuracy=lens_accuracy, + nonlinear=current_uses_nonlinear_lensing(params), + ) + return + + margin = 0 if lens_margin is None else lens_margin + target_lmax = params.max_l - margin if params.DoLensing else params.max_l + max_eta_k = params.max_eta_k if lens_potential_accuracy is None else None + params.set_for_lmax( + max(1, target_lmax), + max_eta_k=max_eta_k, + lens_margin=margin, + lens_potential_accuracy=lens_accuracy, + nonlinear=current_uses_nonlinear_lensing(params), + ) + + +def current_uses_nonlinear_lensing(params) -> bool: + return str(params.NonLinear) in {"NonLinear_lens", "NonLinear_both"} + + +def load_params( + ini_file: Path, + *, + no_validate: bool, + settings: dict[str, float | bool] | None = None, + set_for_lmax: int | None = None, + lens_margin: int | None = None, + lens_potential_accuracy: float | None = None, +): + import camb + + params = camb.read_ini(str(ini_file), no_validate=no_validate) + apply_lensing_settings( + params, + set_for_lmax=set_for_lmax, + lens_margin=lens_margin, + lens_potential_accuracy=lens_potential_accuracy, + ) + if settings: + apply_accuracy_settings(params, settings) + return params + + +def run_params_case( + params, + label: str, + *, + lmax: int | None, + mpk_kmin: float, + mpk_npoints: int, +) -> RunOutput: + import camb + + cpu_start = time.process_time() + wall_start = time.perf_counter() + results = camb.get_results(params) + cpu_time = time.process_time() - cpu_start + wall_time = time.perf_counter() - wall_start + + derived = results.get_derived_params() if params.WantDerivedParameters else {} + lensed_cls = get_lensed_cls(results, lmax) + lens_potential_cls = get_lens_potential_cls(results, lmax) + matter_power = get_matter_power(results, mpk_kmin, mpk_npoints) + + return RunOutput( + label=label, + params=params, + results=results, + cpu_time=cpu_time, + wall_time=wall_time, + derived=derived, + lensed_cls=lensed_cls, + lens_potential_cls=lens_potential_cls, + matter_power=matter_power, + ) + + +def run_case( + ini_file: Path, + label: str, + *, + no_validate: bool, + accuracy_settings: dict[str, float | bool] | None, + lmax: int | None, + set_for_lmax: int | None, + lens_margin: int | None, + lens_potential_accuracy: float | None, + mpk_kmin: float, + mpk_npoints: int, +) -> RunOutput: + params = load_params( + ini_file, + no_validate=no_validate, + settings=accuracy_settings, + set_for_lmax=set_for_lmax, + lens_margin=lens_margin, + lens_potential_accuracy=lens_potential_accuracy, + ) + return run_params_case( + params, + label, + lmax=lmax, + mpk_kmin=mpk_kmin, + mpk_npoints=mpk_npoints, + ) + + +def run_timing_summary(run: RunOutput) -> str: + return f"cpu={run.cpu_time:.2f}s wall={run.wall_time:.2f}s" + + +def run_timing_key(run: RunOutput) -> tuple[float, float]: + return run.cpu_time, run.wall_time + + +def get_lensed_cls(results, lmax: int | None) -> np.ndarray | None: + params = results.Params + if not (params.WantCls and params.Want_CMB): + return None + return results.get_total_cls(lmax) + + +def get_lens_potential_cls(results, lmax: int | None) -> np.ndarray | None: + params = results.Params + if not (params.WantCls and params.Want_CMB_lensing): + return None + return results.get_lens_potential_cls(lmax) + + +def get_matter_power(results, minkh: float, npoints: int) -> MatterPowerData | None: + params = results.Params + if not params.WantTransfer: + return None + requested_kmax = params.Transfer.kmax / (params.H0 / 100.0) + if requested_kmax <= minkh: + return None + nonlinear = str(params.NonLinear) in {"NonLinear_pk", "NonLinear_both"} + k, z, pk = results.get_linear_matter_power_spectrum( + have_power_spectra=True, + nonlinear=nonlinear, + ) + mask = k >= minkh + if not np.any(mask): + return None + return MatterPowerData(k=k[mask], z=z, pk=pk[:, mask], requested_kmax=requested_kmax, npoints=npoints) + + +def fractional_delta(values: np.ndarray, reference: np.ndarray, *, floor: float = 0.0) -> np.ndarray: + denominator = np.abs(reference) + if floor: + denominator = np.maximum(denominator, floor) + with np.errstate(divide="ignore", invalid="ignore"): + delta = (values - reference) / denominator + return delta + + +def normalized_te_delta(values: np.ndarray, reference: np.ndarray) -> np.ndarray: + denominator = np.sqrt(np.maximum(reference[:, CL_COLUMNS["TT"]] * reference[:, CL_COLUMNS["EE"]], 0.0)) + with np.errstate(divide="ignore", invalid="ignore"): + delta = (values[:, CL_COLUMNS["TE"]] - reference[:, CL_COLUMNS["TE"]]) / denominator + return delta + + +def finite_stats( + quantity: str, + range_label: str, + errors: np.ndarray, + tolerance: float, + locations: np.ndarray | None = None, +) -> StatRow: + finite = np.isfinite(errors) + if not np.all(finite): + bad_index = int(np.flatnonzero(~finite)[0]) + location = "" + if locations is not None: + location = str(locations[bad_index]) + return StatRow( + quantity, + range_label, + math.inf, + math.inf, + tolerance, + False, + location or "non-finite sample", + ) + if not np.any(finite): + return StatRow( + quantity, + range_label, + math.inf, + math.inf, + tolerance, + False, + "no finite samples", + ) + valid_errors = errors[finite] + max_index = int(np.argmax(np.abs(valid_errors))) + max_abs = float(np.abs(valid_errors[max_index])) + rms = float(np.sqrt(np.mean(valid_errors**2))) + location = "" + if locations is not None: + location = str(locations[finite][max_index]) + return StatRow(quantity, range_label, max_abs, rms, tolerance, max_abs <= tolerance, location) + + +def compare_derived(standard: RunOutput, reference: RunOutput, tolerance: float) -> list[StatRow]: + rows = [] + for name in reference.derived: + if name not in standard.derived: + continue + ref = float(reference.derived[name]) + value = float(standard.derived[name]) + denominator = abs(ref) if ref else 1.0 + error = abs((value - ref) / denominator) + rows.append( + StatRow( + f"derived:{name}", + "fractional", + error, + error, + tolerance, + error <= tolerance, + ) + ) + return rows + + +def compare_cls(standard: RunOutput, reference: RunOutput) -> list[StatRow]: + if standard.lensed_cls is None or reference.lensed_cls is None: + return [] + lmax = min(standard.lensed_cls.shape[0], reference.lensed_cls.shape[0]) - 1 + standard_cls = standard.lensed_cls[: lmax + 1] + reference_cls = reference.lensed_cls[: lmax + 1] + ell = np.arange(lmax + 1) + rows = [] + + for quantity in ("TT", "EE"): + errors = fractional_delta( + standard_cls[:, CL_COLUMNS[quantity]], + reference_cls[:, CL_COLUMNS[quantity]], + ) + rows.extend(compare_l_ranges(quantity, errors, ell, tolerance_ranges(TT_EE_TOLERANCES), min_ell=2)) + + te_errors = normalized_te_delta(standard_cls, reference_cls) + rows.extend(compare_l_ranges("TE", te_errors, ell, tolerance_ranges(TT_EE_TOLERANCES), min_ell=2)) + + bb_errors = fractional_delta(standard_cls[:, CL_COLUMNS["BB"]], reference_cls[:, CL_COLUMNS["BB"]]) + rows.extend(compare_l_ranges("BB", bb_errors, ell, tolerance_ranges(bb_tolerances(standard.params)), min_ell=2)) + return rows + + +def bb_tolerances(params) -> list[tuple[int, float]]: + if not bool(params.Accuracy.AccurateBB): + return [(0, 2e-2), (8000, 1e-1)] + return BB_TOLERANCES + + +def compare_lensing(standard: RunOutput, reference: RunOutput) -> list[StatRow]: + if standard.lens_potential_cls is None or reference.lens_potential_cls is None: + return [] + lmax = min(standard.lens_potential_cls.shape[0], reference.lens_potential_cls.shape[0]) - 1 + standard_pp = standard.lens_potential_cls[: lmax + 1, 0] + reference_pp = reference.lens_potential_cls[: lmax + 1, 0] + ell = np.arange(lmax + 1) + errors = fractional_delta(standard_pp, reference_pp) + return compare_l_ranges("lens PP", errors, ell, tolerance_ranges(LENSING_TOLERANCES), min_ell=2) + + +def compare_l_ranges( + quantity: str, + errors: np.ndarray, + ell: np.ndarray, + ranges: Iterable[RangeTolerance], + *, + min_ell: int, +) -> list[StatRow]: + rows = [] + for range_tolerance in ranges: + mask = ell >= max(min_ell, range_tolerance.start) + if range_tolerance.stop is not None: + mask &= ell < range_tolerance.stop + if not np.any(mask): + continue + selected_ell = ell[mask] + rows.append( + finite_stats( + quantity, + ell_range_label(selected_ell, range_tolerance), + errors[mask], + range_tolerance.tolerance, + locations=ell[mask], + ) + ) + return rows + + +def ell_range_label(selected_ell: np.ndarray, range_tolerance: RangeTolerance) -> str: + start = int(selected_ell[0]) + stop = int(selected_ell[-1]) + if range_tolerance.stop is None: + return f"L >= {start}" + if stop < range_tolerance.stop - 1: + return f"{start} <= L <= {stop}" + return range_tolerance.label + + +def compare_matter_power(standard: RunOutput, reference: RunOutput, tolerance: float) -> list[StatRow]: + if standard.matter_power is None or reference.matter_power is None: + return [] + common_k, z, standard_pk, reference_pk = common_matter_power_grid(standard.matter_power, reference.matter_power) + errors = fractional_delta(standard_pk, reference_pk) + z_grid, k_grid = np.meshgrid(z, common_k, indexing="ij") + locations = np.array([f"z={z_value:.6g}, k/h={k:.6g}" for z_value, k in zip(z_grid.ravel(), k_grid.ravel())]) + range_label = f"all z, {common_k[0]:.4g} <= k/h <= {common_k[-1]:.4g}" + return [finite_stats("matter P(k)", range_label, errors.ravel(), tolerance, locations=locations)] + + +def common_matter_power_grid( + standard_matter_power: MatterPowerData, + reference_matter_power: MatterPowerData, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + std_k, std_z, std_pk = ( + standard_matter_power.k, + standard_matter_power.z, + standard_matter_power.pk, + ) + ref_k, ref_z, ref_pk = ( + reference_matter_power.k, + reference_matter_power.z, + reference_matter_power.pk, + ) + if std_pk.shape[0] != ref_pk.shape[0] or not np.allclose(std_z, ref_z): + raise ValueError("Matter power redshift grids differ") + + kmin = max(float(std_k[0]), float(ref_k[0])) + requested_kmax = min(standard_matter_power.requested_kmax, reference_matter_power.requested_kmax) + kmax = min(float(std_k[-1]), float(ref_k[-1]), requested_kmax) + if kmax <= kmin: + raise ValueError("Matter power k grids do not overlap") + npoints = min(standard_matter_power.npoints, reference_matter_power.npoints) + common_k = np.exp(np.linspace(np.log(kmin), np.log(kmax), npoints)) + standard_pk = interpolate_pk_to_grid(std_k, std_pk, common_k) + reference_pk = interpolate_pk_to_grid(ref_k, ref_pk, common_k) + return common_k, std_z, standard_pk, reference_pk + + +def interpolate_pk_to_grid(k: np.ndarray, pk: np.ndarray, common_k: np.ndarray) -> np.ndarray: + log_k = np.log(k) + log_common_k = np.log(common_k) + interpolated = np.empty((pk.shape[0], len(common_k))) + for index, values in enumerate(pk): + if np.all(values > 0): + interpolated[index] = np.exp(np.interp(log_common_k, log_k, np.log(values))) + else: + interpolated[index] = np.interp(log_common_k, log_k, values) + return interpolated + + +def compare_runs( + standard: RunOutput, + reference: RunOutput, + *, + derived_tolerance: float, + mpk_tolerance: float, +) -> ComparisonResult: + return ComparisonResult( + derived_rows=compare_derived(standard, reference, derived_tolerance), + cl_rows=compare_cls(standard, reference), + lensing_rows=compare_lensing(standard, reference), + mpk_rows=compare_matter_power(standard, reference, mpk_tolerance), + ) + + +def compare_params_accuracy( + params, + *, + reference_accuracy_settings: dict[str, float | bool] | None = None, + comparator_accuracy_settings: dict[str, float | bool] | None = None, + lmax: int | None = None, + set_for_lmax: int | None = None, + lens_margin: int | None = None, + lens_potential_accuracy: float | None = None, + reference_lens_margin: int | None = None, + reference_lens_potential_accuracy: float | None = None, + mpk_kmin: float = 1e-4, + mpk_npoints: int = 500, + derived_tolerance: float = DERIVED_TOLERANCE, + mpk_tolerance: float | None = None, + chi2_config: NoiseConfig | None = None, +) -> AccuracyCheckResult: + """Compare a :class:`~camb.model.CAMBparams` object against a higher-accuracy copy. + + The input parameters are copied before modification. The returned result + includes both CAMB runs, row-wise comparison statistics, and optionally a + fiducial CMB delta chi-squared estimate. + """ + if mpk_tolerance is None: + mpk_tolerance = MPK_TOLERANCE if params.Transfer.high_precision else 3e-3 + reference_accuracy_settings = reference_accuracy_settings or DEFAULT_ACCURACY_SETTINGS + + standard_params = copy_params(params) + apply_lensing_settings( + standard_params, + set_for_lmax=set_for_lmax, + lens_margin=lens_margin, + lens_potential_accuracy=lens_potential_accuracy, + ) + if comparator_accuracy_settings: + apply_accuracy_settings(standard_params, comparator_accuracy_settings) + + reference_params = copy_params(params) + apply_lensing_settings( + reference_params, + set_for_lmax=set_for_lmax, + lens_margin=reference_lens_margin if reference_lens_margin is not None else lens_margin, + lens_potential_accuracy=( + reference_lens_potential_accuracy + if reference_lens_potential_accuracy is not None + else lens_potential_accuracy + ), + ) + apply_accuracy_settings(reference_params, reference_accuracy_settings, boost_from_raw=True) + + lmax = lmax or set_for_lmax + standard = run_params_case( + standard_params, + "standard", + lmax=lmax, + mpk_kmin=mpk_kmin, + mpk_npoints=mpk_npoints, + ) + reference = run_params_case( + reference_params, + "boosted", + lmax=lmax, + mpk_kmin=mpk_kmin, + mpk_npoints=mpk_npoints, + ) + comparison = compare_runs( + standard, + reference, + derived_tolerance=derived_tolerance, + mpk_tolerance=mpk_tolerance, + ) + chi2 = calculate_cmb_delta_chi2(standard, reference, chi2_config) if chi2_config else None + return AccuracyCheckResult(standard=standard, reference=reference, comparison=comparison, chi2=chi2) + + +def chi2_noise_config(args: argparse.Namespace) -> NoiseConfig: + if args.chi2_config == "custom": + defaults = {} + else: + defaults = DEFAULT_NOISE_CONFIGS[args.chi2_config] + + noise_t = args.noise_muk_arcmin_t + if noise_t is None: + noise_t = defaults.get("noise_muK_arcmin_T") + noise_p = args.noise_muk_arcmin_p + if noise_p is None: + noise_p = defaults.get("noise_muK_arcmin_P", noise_t) + fwhm = args.beam_fwhm_arcmin + if fwhm is None: + fwhm = defaults.get("fwhm_arcmin") + fsky = args.fsky + if fsky is None: + fsky = defaults.get("fsky", 1.0) + + missing = [ + name + for name, value in ( + ("noise_muK_arcmin_T", noise_t), + ("noise_muK_arcmin_P", noise_p), + ("fwhm_arcmin", fwhm), + ) + if value is None + ] + if missing: + raise ValueError(f"--chi2-config custom requires {', '.join(missing)}") + if fsky <= 0: + raise ValueError("--fsky must be positive") + + return NoiseConfig( + name=args.chi2_config, + noise_muK_arcmin_T=float(noise_t), + noise_muK_arcmin_P=float(noise_p), + fwhm_arcmin=float(fwhm), + fsky=float(fsky), + lmin=args.chi2_lmin, + lmax=args.chi2_lmax or effective_lmax(args), + fields=args.chi2_fields, + ) + + +def white_noise_from_muK_arcmin(noise_muK_arcmin: float) -> float: + return (noise_muK_arcmin * np.pi / (180.0 * 60.0)) ** 2 + + +def make_cmb_noise_spectra(ell: np.ndarray, config: NoiseConfig) -> dict[str, np.ndarray]: + noise_var_t = white_noise_from_muK_arcmin(config.noise_muK_arcmin_T) + noise_var_p = white_noise_from_muK_arcmin(config.noise_muK_arcmin_P) + fwhm_degrees = config.fwhm_arcmin / 60.0 + xlc = 180.0 * np.sqrt(8.0 * np.log(2.0)) / np.pi + sigma2 = (fwhm_degrees / xlc) ** 2 + beam_fac = np.exp(ell * (ell + 1.0) * sigma2) + dl_fac = ell * (ell + 1.0) / (2.0 * np.pi) + return { + "T": dl_fac * noise_var_t * beam_fac, + "E": dl_fac * noise_var_p * beam_fac, + "B": dl_fac * noise_var_p * beam_fac, + } + + +def calculate_cmb_delta_chi2(standard: RunOutput, reference: RunOutput, config: NoiseConfig) -> ChiSquaredResult | None: + if not ( + standard.params.WantCls and reference.params.WantCls and standard.params.Want_CMB and reference.params.Want_CMB + ): + return None + + lmax_available = min(standard.results.Params.max_l, reference.results.Params.max_l) + if config.lmax is not None: + lmax_available = min(lmax_available, config.lmax) + lmin = max(2, config.lmin) + if lmax_available < lmin: + return None + + standard_cls = standard.results.get_total_cls(lmax_available, CMB_unit="muK") + reference_cls = reference.results.get_total_cls(lmax_available, CMB_unit="muK") + ell = np.arange(lmax_available + 1) + noise = make_cmb_noise_spectra(ell, config) + + delta_chi2 = 0.0 + worst_ell = lmin + worst_delta_chi2 = -math.inf + for multipole in range(lmin, lmax_available + 1): + model_cov = cmb_covariance_at_l(standard_cls[multipole], noise, multipole, config.fields) + fiducial_cov = cmb_covariance_at_l(reference_cls[multipole], noise, multipole, config.fields) + ell_chi2 = gaussian_delta_chi2_at_l(model_cov, fiducial_cov, multipole, config.fsky) + delta_chi2 += ell_chi2 + if ell_chi2 > worst_delta_chi2: + worst_delta_chi2 = ell_chi2 + worst_ell = multipole + + return ChiSquaredResult( + delta_chi2=delta_chi2, + lmin=lmin, + lmax=lmax_available, + fields=config.fields, + fsky=config.fsky, + noise_muK_arcmin_T=config.noise_muK_arcmin_T, + noise_muK_arcmin_P=config.noise_muK_arcmin_P, + fwhm_arcmin=config.fwhm_arcmin, + worst_ell=worst_ell, + worst_delta_chi2=worst_delta_chi2, + ) + + +def cmb_covariance_at_l(cls: np.ndarray, noise: dict[str, np.ndarray], ell: int, fields: tuple[str, ...]) -> np.ndarray: + covariance = np.zeros((len(fields), len(fields))) + for i, field_i in enumerate(fields): + for j, field_j in enumerate(fields): + if i == j: + covariance[i, j] = cls[CL_COLUMNS[field_i + field_i]] + noise[field_i][ell] + elif {field_i, field_j} == {"T", "E"}: + covariance[i, j] = cls[CL_COLUMNS["TE"]] + return covariance + + +def gaussian_delta_chi2_at_l( + model_cov: np.ndarray, + fiducial_cov: np.ndarray, + ell: int, + fsky: float, +) -> float: + sign_model, logdet_model = np.linalg.slogdet(model_cov) + sign_fiducial, logdet_fiducial = np.linalg.slogdet(fiducial_cov) + if sign_model <= 0 or sign_fiducial <= 0: + return math.inf + trace = np.trace(np.linalg.solve(model_cov, fiducial_cov)) + return float(fsky * (2 * ell + 1) * (trace + logdet_model - logdet_fiducial - model_cov.shape[0])) + + +def print_chi2_result(result: ChiSquaredResult | None, config_name: str) -> None: + if result is None: + print("\nFiducial CMB delta chi-squared: not calculated") + return + fields = ",".join(result.fields) + print("\nFiducial CMB delta chi-squared:") + print( + f" config={config_name}, fields={fields}, L={result.lmin}..{result.lmax}, " + f"fsky={result.fsky:g}, beam={result.fwhm_arcmin:g} arcmin" + ) + print(f" noise T={result.noise_muK_arcmin_T:g} muK-arcmin, P={result.noise_muK_arcmin_P:g} muK-arcmin") + print( + f" delta chi2={result.delta_chi2:.6g}; largest per-L contribution={result.worst_delta_chi2:.6g} at L={result.worst_ell}" + ) + + +def print_run_summary(standard: RunOutput, reference: RunOutput) -> None: + print("Runs:") + for run in (standard, reference): + print(f" {run.label}: {run_timing_summary(run)}") + if reference.cpu_time: + print(f" cpu ratio {standard.label}/{reference.label}: {standard.cpu_time / reference.cpu_time:.3g}") + if reference.wall_time: + print(f" wall ratio {standard.label}/{reference.label}: {standard.wall_time / reference.wall_time:.3g}") + + +def print_table(title: str, rows: list[StatRow]) -> None: + if not rows: + print(f"\n{title}: not calculated") + return + print(f"\n{title}:") + print(f" {'quantity':<20} {'range':<20} {'max abs':>12} {'rms':>12} {'tol':>12} status") + for row in rows: + max_abs = "nan" if math.isnan(row.max_abs) else f"{row.max_abs:.4g}" + rms = "nan" if math.isnan(row.rms) else f"{row.rms:.4g}" + status = "OK" if row.passed else "FAIL" + location = f" at {row.location}" if row.location and row.location != "no finite samples" else "" + print( + f" {row.quantity:<20} {row.range_label:<20} {max_abs:>12} {rms:>12} " + f"{row.tolerance:>12.4g} {status}{location}" + ) + + +def print_derived_table(rows: list[StatRow]) -> None: + title = "Derived parameter fractional changes" + if not rows: + print(f"\n{title}: not calculated") + return + print(f"\n{title}:") + print(f" {'quantity':<20} {'frac change':>12} {'tol':>12} status") + for row in rows: + value = "nan" if math.isnan(row.max_abs) else f"{row.max_abs:.4g}" + status = "OK" if row.passed else "FAIL" + print(f" {row.quantity:<20} {value:>12} {row.tolerance:>12.4g} {status}") + + +def print_comparison(comparison: ComparisonResult) -> None: + print_derived_table(comparison.derived_rows) + print_table("Lensed CMB spectra errors", comparison.cl_rows) + print_table("Lensing potential errors", comparison.lensing_rows) + print_table("Matter power errors", comparison.mpk_rows) + + +def failure_summary(comparison: ComparisonResult) -> str: + worst = comparison.worst_failure + if worst is None: + return "FAIL: at least one comparison exceeded its tolerance." + location = f" at {worst.location}" if worst.location else "" + return ( + "FAIL: worst failure is " + f"{worst.quantity} ({worst.range_label}) max abs {worst.max_abs:.4g} > tol {worst.tolerance:.4g}" + f"{location}." + ) + + +def comparison_status(comparison: ComparisonResult) -> str: + score = comparison_score(comparison) + if comparison.passed: + return f"PASS score={score:.4g}" + worst = comparison.worst_failure + if worst is None: + return f"FAIL score={score:.4g}" + location = f" at {worst.location}" if worst.location else "" + return ( + f"FAIL score={score:.4g}; worst={worst.quantity} {worst.range_label}{location}, " + f"{worst.max_abs:.4g}/{worst.tolerance:.4g}" + ) + + +def search_timing_summary(result: SearchResult) -> list[str]: + lines = [] + if result.run is not None: + lines.append(f" selected timing: {run_timing_summary(result.run)}") + fastest = result.fastest + if ( + fastest is not None + and fastest.run is not None + and settings_key(fastest.settings) != settings_key(result.settings) + ): + lines.append( + " fastest passing candidate seen: " + f"{run_timing_summary(fastest.run)} with {format_settings(changed_settings(fastest.settings, result.settings))}" + ) + return lines + + +def print_search_timing_summary(result: SearchResult) -> None: + for line in search_timing_summary(result): + print(line) + + +def plot_errors(standard: RunOutput, reference: RunOutput, plot_dir: Path) -> None: + import matplotlib.pyplot as plt + + plot_dir.mkdir(parents=True, exist_ok=True) + if standard.lensed_cls is not None and reference.lensed_cls is not None: + lmax = min(standard.lensed_cls.shape[0], reference.lensed_cls.shape[0]) - 1 + ell = np.arange(lmax + 1) + std_cls = standard.lensed_cls[: lmax + 1] + ref_cls = reference.lensed_cls[: lmax + 1] + plt.figure(figsize=(9, 5)) + for quantity in ("TT", "EE", "BB"): + errors = fractional_delta(std_cls[:, CL_COLUMNS[quantity]], ref_cls[:, CL_COLUMNS[quantity]]) + plot_log_l_errors(plt, ell, errors, quantity) + plot_log_l_errors(plt, ell, normalized_te_delta(std_cls, ref_cls), "TE/sqrt(TT*EE)") + plt.axhline(0, color="black", linewidth=0.8) + plt.xlabel("L") + plt.ylabel("fractional error") + plt.legend() + plt.tight_layout() + path = plot_dir / "lensed_cls_errors.png" + plt.savefig(path, dpi=150) + plt.close() + print(f"Wrote {path}") + + if standard.lens_potential_cls is not None and reference.lens_potential_cls is not None: + lmax = ( + min( + standard.lens_potential_cls.shape[0], + reference.lens_potential_cls.shape[0], + ) + - 1 + ) + ell = np.arange(lmax + 1) + errors = fractional_delta( + standard.lens_potential_cls[: lmax + 1, 0], + reference.lens_potential_cls[: lmax + 1, 0], + ) + plt.figure(figsize=(9, 5)) + plot_log_l_errors(plt, ell, errors, "PP") + plt.axhline(0, color="black", linewidth=0.8) + plt.xlabel("L") + plt.ylabel("fractional error") + plt.legend() + plt.tight_layout() + path = plot_dir / "lens_potential_errors.png" + plt.savefig(path, dpi=150) + plt.close() + print(f"Wrote {path}") + + if standard.matter_power is not None and reference.matter_power is not None: + std_k, std_z, std_pk, ref_pk = common_matter_power_grid(standard.matter_power, reference.matter_power) + errors = fractional_delta(std_pk, ref_pk) + plt.figure(figsize=(9, 5)) + for index, z in enumerate(std_z): + plt.semilogx(std_k, errors[index], label=f"z={z:.4g}") + plt.axhline(0, color="black", linewidth=0.8) + plt.xlabel("k/h") + plt.ylabel("fractional error") + plt.legend() + plt.tight_layout() + path = plot_dir / "matter_power_errors.png" + plt.savefig(path, dpi=150) + plt.close() + print(f"Wrote {path}") + + +def plot_log_l_errors(plt, ell: np.ndarray, errors: np.ndarray, label: str) -> None: + mask = (ell >= 2) & np.isfinite(errors) + if np.any(mask): + plt.semilogx(ell[mask], errors[mask], label=label, alpha=0.85) + + +def settings_cost(settings: dict[str, float | bool], raw_settings: dict[str, float | bool]) -> tuple[float, int, tuple]: + changed = 0 + product = 1.0 + values = [] + numeric_keys = sorted(key for key in set(settings) | set(raw_settings) if key != "DoLateRadTruncation") + for key in numeric_keys: + value = float(settings[key]) + raw_value = float(raw_settings[key]) + changed += int(not math.isclose(value, raw_value)) + product *= value + values.append(value) + if "DoLateRadTruncation" in settings or "DoLateRadTruncation" in raw_settings: + changed += int(settings.get("DoLateRadTruncation") != raw_settings.get("DoLateRadTruncation")) + values.append(int(bool(settings.get("DoLateRadTruncation")))) + return product, changed, tuple(values) + + +def raw_accuracy_settings(params) -> dict[str, float | bool]: + return { + "AccuracyBoost": float(params.Accuracy.AccuracyBoost), + "lSampleBoost": float(params.Accuracy.lSampleBoost), + "lAccuracyBoost": float(params.Accuracy.lAccuracyBoost), + "IntTolBoost": float(params.Accuracy.IntTolBoost), + "DoLateRadTruncation": bool(params.DoLateRadTruncation), + } + + +def component_accuracy_settings(params) -> dict[str, float | bool]: + settings = {"AccuracyBoost": 1.0} + for key in component_refinement_keys(params): + settings[key] = float(getattr(params.Accuracy, key)) + settings["DoLateRadTruncation"] = bool(params.DoLateRadTruncation) + return settings + + +def component_refinement_keys(params) -> tuple[str, ...]: + keys = list(COMPONENT_REFINEMENT_KEYS) + if abs(float(getattr(params, "omk", 0.0))) <= OMEGA_K_FLAT: + keys.remove("NonFlatIntAccuracyBoost") + if not uses_nonlinear_sources(params): + keys.remove("NonlinSourceBoost") + if not has_redshift_windows(params): + keys.remove("KmaxBoost") + keys.remove("SourceLimberBoost") + return tuple(keys) + + +def uses_nonlinear_sources(params) -> bool: + return str(getattr(params, "NonLinear", "NonLinear_none")) in {"NonLinear_pk", "NonLinear_both"} + + +def has_redshift_windows(params) -> bool: + source_windows = getattr(params, "SourceWindows", None) + if source_windows is not None: + try: + return len(source_windows) >= 1 + except TypeError: + pass + return int(getattr(params, "num_redshiftwindows", 0)) >= 1 + + +def component_target_settings( + raw_settings: dict[str, float | bool], + passing_settings: dict[str, float | bool], +) -> dict[str, float | bool]: + global_boost = float(passing_settings.get("AccuracyBoost", 1.0)) + target = dict(raw_settings) + target["AccuracyBoost"] = 1.0 + target["DoLateRadTruncation"] = passing_settings.get( + "DoLateRadTruncation", raw_settings.get("DoLateRadTruncation", True) + ) + for key in GLOBAL_BOOST_COMPONENT_KEYS: + if key not in raw_settings: + continue + direct_value = float(passing_settings.get(key, raw_settings[key])) + boosted_value = float(raw_settings[key]) * global_boost + target_value = max( + float(raw_settings[key]), + direct_value, + boosted_value, + ) + target[key] = next_meaningful_accuracy_value(key, float(raw_settings[key]), target_value) + for key in ("lSampleBoost", "lAccuracyBoost"): + if key in raw_settings: + target[key] = max( + float(raw_settings[key]), + float(passing_settings.get(key, raw_settings[key])), + ) + return target + + +def accuracy_search_target_settings( + raw_settings: dict[str, float | bool], + requested_settings: dict[str, float | bool], +) -> dict[str, float | bool]: + target = dict(requested_settings) + for key in BASE_ACCURACY_KEYS: + target[key] = max(float(raw_settings[key]), float(requested_settings[key])) + target["DoLateRadTruncation"] = bool(requested_settings["DoLateRadTruncation"]) + return target + + +def next_meaningful_accuracy_value(key: str, raw_value: float, target_value: float) -> float: + if key not in DISCRETE_ACCURACY_VALUES: + return target_value + for value in DISCRETE_ACCURACY_VALUES[key]: + if value >= raw_value and value >= target_value: + return value + return max(raw_value, DISCRETE_ACCURACY_VALUES[key][-1]) + + +def search_candidates( + raw_settings: dict[str, float | bool], + high_accuracy_settings: dict[str, float | bool], + grid: Iterable[float] | None, +) -> list[dict[str, float | bool]]: + if grid is None: + return [] + candidate_values = {} + for key in BASE_ACCURACY_KEYS: + raw_value = float(raw_settings[key]) + high_value = max(raw_value, float(high_accuracy_settings[key])) + candidate_values[key] = sorted({max(raw_value, value) for value in grid if value <= high_value} | {high_value}) + late_rad_values = sorted( + { + bool(raw_settings["DoLateRadTruncation"]), + bool(high_accuracy_settings["DoLateRadTruncation"]), + }, + key=int, + ) + candidates = [ + { + "AccuracyBoost": accuracy_boost, + "lSampleBoost": l_sample_boost, + "lAccuracyBoost": l_accuracy_boost, + "IntTolBoost": int_tol_boost, + "DoLateRadTruncation": do_late_rad_truncation, + } + for accuracy_boost, l_sample_boost, l_accuracy_boost, int_tol_boost, do_late_rad_truncation in itertools.product( + candidate_values["AccuracyBoost"], + candidate_values["lSampleBoost"], + candidate_values["lAccuracyBoost"], + candidate_values["IntTolBoost"], + late_rad_values, + ) + ] + return sorted(candidates, key=lambda settings: settings_cost(settings, raw_settings)) + + +def bracket_then_refine_boost_subset( + subset: tuple[str, ...], + raw_settings: dict[str, float | bool], + target_settings: dict[str, float | bool], + late_rad_value: bool, + evaluator, + tolerance: float, +) -> tuple[dict[str, float | bool], ComparisonResult | None] | None: + if not any(float(target_settings[key]) > float(raw_settings[key]) for key in subset): + return None + + settings = subset_target_settings(raw_settings, target_settings, subset, late_rad_value) + comparison = evaluator(settings) + if comparison is None: + return settings, None + if not comparison.passed: + return None + print(f"Found passing subset {subset}; refining within bracket.") + return refine_numeric_boosts( + settings, + raw_settings, + evaluator, + tolerance, + cost_function=settings_cost, + ) + + +def subset_target_settings( + raw_settings: dict[str, float | bool], + target_settings: dict[str, float | bool], + subset: tuple[str, ...], + late_rad_value: bool, +) -> dict[str, float | bool]: + settings = dict(raw_settings) + settings["DoLateRadTruncation"] = late_rad_value + for key in subset: + settings[key] = target_settings[key] + return settings + + +def find_minimal_boosts( + ini_file: Path, + args: argparse.Namespace, + reference: RunOutput, + high_accuracy_settings: dict[str, float | bool], + raw_comparison: ComparisonResult | None = None, + raw_run: RunOutput | None = None, +) -> SearchResult | tuple[None, None]: + raw_params = load_params( + ini_file, + no_validate=args.no_validate, + set_for_lmax=args.set_for_lmax, + lens_margin=args.lens_margin, + lens_potential_accuracy=args.lens_potential_accuracy, + ) + raw_settings = raw_accuracy_settings(raw_params) + target_settings = accuracy_search_target_settings(raw_settings, high_accuracy_settings) + candidates = search_candidates(raw_settings, target_settings, args.search_grid) + comparisons: dict[tuple, ComparisonResult] = {} + runs: dict[tuple, RunOutput] = {} + comparison_runs: dict[int, RunOutput] = {} + if raw_comparison is not None: + raw_key = settings_key(raw_settings) + comparisons[raw_key] = raw_comparison + if raw_run is not None: + runs[raw_key] = raw_run + comparison_runs[id(raw_comparison)] = raw_run + fastest: PassingCandidate | None = None + runs_done = 0 + + def make_result(settings: dict[str, float | bool], comparison: ComparisonResult) -> SearchResult: + key = settings_key(refine_discrete_accuracy_settings(settings, raw_settings)) + return SearchResult(settings, comparison, runs.get(key) or comparison_runs.get(id(comparison)), fastest) + + def run_candidate(settings: dict[str, float | bool]) -> ComparisonResult | None: + nonlocal fastest, runs_done + settings = refine_discrete_accuracy_settings(settings, raw_settings) + key = settings_key(settings) + if key in comparisons: + return comparisons[key] + if args.max_search_runs is not None and runs_done >= args.max_search_runs: + return None + runs_done += 1 + print(f" [{runs_done}] {format_settings(settings)}") + candidate = run_case( + ini_file, + "candidate", + no_validate=args.no_validate, + accuracy_settings=settings, + lmax=effective_lmax(args), + set_for_lmax=args.set_for_lmax, + lens_margin=args.lens_margin, + lens_potential_accuracy=args.lens_potential_accuracy, + mpk_kmin=args.mpk_kmin, + mpk_npoints=args.mpk_npoints, + ) + runs[key] = candidate + print(f" {run_timing_summary(candidate)}") + comparison = compare_runs( + candidate, + reference, + derived_tolerance=args.derived_tolerance, + mpk_tolerance=args.mpk_tolerance, + ) + comparisons[key] = comparison + comparison_runs[id(comparison)] = candidate + if comparison.passed: + passing = PassingCandidate(settings, comparison, candidate) + if fastest is None or run_timing_key(candidate) < run_timing_key(fastest.run): + fastest = passing + print(f" {comparison_status(comparison)}") + return comparison + + raw_comparison = run_candidate(raw_settings) + if raw_comparison is None: + return None, None + if raw_comparison.passed: + return make_result(raw_settings, raw_comparison) + + numeric_keys = BASE_ACCURACY_KEYS + late_rad_values = [bool(raw_settings["DoLateRadTruncation"])] + if target_settings["DoLateRadTruncation"] != raw_settings["DoLateRadTruncation"]: + late_rad_values.append(bool(target_settings["DoLateRadTruncation"])) + exhaustive = getattr(args, "exhaustive_boost_search", False) is True + if target_settings["DoLateRadTruncation"] != raw_settings["DoLateRadTruncation"]: + settings = dict(raw_settings) + settings["DoLateRadTruncation"] = target_settings["DoLateRadTruncation"] + comparison = run_candidate(settings) + if comparison is None: + return None, None + if comparison.passed: + return make_result(settings, comparison) + + print("\nSearching single boost parameters from values close to raw...") + best_settings = None + best_comparison = None + best_cost = None + for subset_size in range(1, len(numeric_keys) + 1): + if subset_size == 2 and best_settings is not None and not exhaustive: + return make_result(best_settings, best_comparison) + if subset_size == 2: + if best_settings is None: + print("\nNo single-parameter path passed; searching boost parameter combinations...") + else: + print("\nExhaustive search requested; searching boost parameter combinations...") + elif subset_size > 2: + print(f"\nSearching {subset_size}-parameter boost combinations...") + for late_rad_value in late_rad_values: + for subset in itertools.combinations(numeric_keys, subset_size): + result = bracket_then_refine_boost_subset( + subset, + raw_settings, + target_settings, + late_rad_value, + run_candidate, + args.search_tolerance, + ) + if result is None: + continue + refined_settings, refined_comparison = result + if refined_comparison is None: + if best_settings is not None: + print("Search run budget exhausted; returning the best passing subset found so far.") + return make_result(best_settings, best_comparison) + print("Search run budget exhausted before finding a passing subset.") + return None, None + cost = settings_cost(refined_settings, raw_settings) + if best_cost is None or cost < best_cost: + best_settings = refined_settings + best_comparison = refined_comparison + best_cost = cost + if best_settings is not None and not exhaustive: + return make_result(best_settings, best_comparison) + if best_settings is not None: + return make_result(best_settings, best_comparison) + + if not candidates: + return None, None + + print(f"\nAdaptive combination search did not pass; trying {len(candidates)} optional seed candidates...") + for settings in candidates: + comparison = run_candidate(settings) + if comparison is None: + if best_settings is not None: + print("Search run budget exhausted; returning the best passing settings found so far.") + return make_result(best_settings, best_comparison) + print("Search run budget exhausted before finding a passing coarse candidate.") + return None, None + if comparison.passed: + print("Found passing coarse settings; refining numeric boosts.") + refined_settings, refined_comparison = refine_numeric_boosts( + settings, + raw_settings, + run_candidate, + args.search_tolerance, + cost_function=settings_cost, + ) + return make_result(refined_settings, refined_comparison) + return None, None + + +def refine_accuracy_components( + ini_file: Path, + args: argparse.Namespace, + reference: RunOutput, + reference_accuracy_settings: dict[str, float | bool], +) -> SearchResult | tuple[None, ComparisonResult | None]: + raw_params = load_params( + ini_file, + no_validate=args.no_validate, + set_for_lmax=args.set_for_lmax, + lens_margin=args.lens_margin, + lens_potential_accuracy=args.lens_potential_accuracy, + ) + target_accuracy_settings = accuracy_search_target_settings( + raw_accuracy_settings(raw_params), reference_accuracy_settings + ) + raw_settings = component_accuracy_settings(raw_params) + target_settings = component_target_settings(raw_settings, target_accuracy_settings) + comparisons: dict[tuple, ComparisonResult] = {} + runs: dict[tuple, RunOutput] = {} + comparison_runs: dict[int, RunOutput] = {} + fastest: PassingCandidate | None = None + runs_done = 0 + + def make_result(settings: dict[str, float | bool], comparison: ComparisonResult) -> SearchResult: + key = settings_key(refine_discrete_accuracy_settings(settings, raw_settings)) + return SearchResult(settings, comparison, runs.get(key) or comparison_runs.get(id(comparison)), fastest) + + def run_candidate(settings: dict[str, float | bool]) -> ComparisonResult | None: + nonlocal fastest, runs_done + settings = refine_discrete_accuracy_settings(settings, raw_settings) + key = settings_key(settings) + if key in comparisons: + return comparisons[key] + if args.max_search_runs is not None and runs_done >= args.max_search_runs: + return None + runs_done += 1 + print(f" [component {runs_done}] {format_settings(changed_settings(settings, raw_settings))}") + candidate = run_case( + ini_file, + "component-candidate", + no_validate=args.no_validate, + accuracy_settings=settings, + lmax=effective_lmax(args), + set_for_lmax=args.set_for_lmax, + lens_margin=args.lens_margin, + lens_potential_accuracy=args.lens_potential_accuracy, + mpk_kmin=args.mpk_kmin, + mpk_npoints=args.mpk_npoints, + ) + runs[key] = candidate + print(f" {run_timing_summary(candidate)}") + comparison = compare_runs( + candidate, + reference, + derived_tolerance=args.derived_tolerance, + mpk_tolerance=args.mpk_tolerance, + ) + comparisons[key] = comparison + comparison_runs[id(comparison)] = candidate + if comparison.passed: + passing = PassingCandidate(settings, comparison, candidate) + if fastest is None or run_timing_key(candidate) < run_timing_key(fastest.run): + fastest = passing + print(f" {comparison_status(comparison)}") + return comparison + + keys = tuple( + key for key in raw_settings if key != "DoLateRadTruncation" and target_settings[key] > raw_settings[key] + ) + max_size = min(args.component_search_size, len(keys)) + print("\nRefining with AccuracyBoost=1 and component accuracy parameters...") + if not keys: + return None, None + + all_target = dict(raw_settings) + all_target["DoLateRadTruncation"] = target_settings["DoLateRadTruncation"] + for key in keys: + all_target[key] = target_settings[key] + print("Checking all component targets first...") + all_target_comparison = run_candidate(all_target) + if all_target_comparison is None: + print("Component search run budget exhausted.") + return None, None + if not all_target_comparison.passed: + print( + "All component targets with AccuracyBoost=1 still fail; " + "the passing run likely depends on direct AccuracyBoost effects." + ) + print(failure_summary(all_target_comparison)) + return None, all_target_comparison + + print("Pruning unnecessary components from the all-target pass...") + pruned_settings, pruned_comparison = greedy_prune_component_settings( + all_target, + raw_settings, + keys, + run_candidate, + ) + if pruned_comparison is None: + print("Component search run budget exhausted.") + return None, None + pruned_keys = component_changed_numeric_keys(pruned_settings, raw_settings) + if component_settings_cost(pruned_settings, raw_settings) < component_settings_cost(all_target, raw_settings): + print(f"Greedy pruning reduced component count to {len(pruned_keys)}.") + if not pruned_keys: + return make_result(pruned_settings, pruned_comparison) + + best_settings = None + best_cost = None + max_size = min(max_size, len(pruned_keys)) + if max_size >= 1: + print("Checking target-valued subsets within the pruned component set...") + for subset_size in range(1, max_size + 1): + for subset in itertools.combinations(pruned_keys, subset_size): + settings = subset_target_settings( + raw_settings, + target_settings, + subset, + bool(target_settings["DoLateRadTruncation"]), + ) + comparison = run_candidate(settings) + if comparison is None: + print("Component search run budget exhausted.") + return None, None + if not comparison.passed: + continue + cost = component_settings_cost(settings, raw_settings) + if best_cost is None or cost < best_cost: + best_settings = settings + best_cost = cost + if best_settings is not None: + print("Found passing component subset; refining values.") + refined_settings, refined_comparison = refine_numeric_boosts( + best_settings, + raw_settings, + run_candidate, + args.search_tolerance, + cost_function=component_settings_cost, + ) + return make_result(refined_settings, refined_comparison) + + print("No smaller target-valued subset passed; refining the pruned component set.") + refined_settings, refined_comparison = refine_numeric_boosts( + pruned_settings, + raw_settings, + run_candidate, + args.search_tolerance, + cost_function=component_settings_cost, + ) + return make_result(refined_settings, refined_comparison) + + +def greedy_prune_component_settings( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + keys: tuple[str, ...], + evaluator, +) -> tuple[dict[str, float | bool], ComparisonResult | None]: + current = dict(settings) + current_comparison = evaluator(current) + if current_comparison is None or not current_comparison.passed: + return current, current_comparison + + removable_keys = [ + key for key in keys if key in current and not math.isclose(float(current[key]), float(raw_settings[key])) + ] + improved = True + while improved: + improved = False + best_trial = None + best_comparison = None + best_cost = None + for key in removable_keys: + if math.isclose(float(current[key]), float(raw_settings[key])): + continue + trial = dict(current) + trial[key] = raw_settings[key] + comparison = evaluator(trial) + if comparison is None: + return current, None + if not comparison.passed: + continue + cost = component_settings_cost(trial, raw_settings) + if best_cost is None or cost < best_cost: + best_trial = trial + best_comparison = comparison + best_cost = cost + if best_trial is not None: + current = best_trial + current_comparison = best_comparison + improved = True + return current, current_comparison + + +def component_changed_numeric_keys( + settings: dict[str, float | bool], raw_settings: dict[str, float | bool] +) -> tuple[str, ...]: + return tuple( + key + for key, value in settings.items() + if key not in {"AccuracyBoost", "DoLateRadTruncation"} + and not math.isclose(float(value), float(raw_settings[key])) + ) + + +def changed_settings( + settings: dict[str, float | bool], raw_settings: dict[str, float | bool] +) -> dict[str, float | bool]: + return { + key: value + for key, value in settings.items() + if key == "AccuracyBoost" + or key == "DoLateRadTruncation" + or not math.isclose(float(value), float(raw_settings[key])) + } + + +def component_settings_cost(settings: dict[str, float | bool], raw_settings: dict[str, float | bool]) -> tuple: + changed = changed_settings(settings, raw_settings) + numeric = [ + float(value) / max(float(raw_settings[key]), 1e-30) + for key, value in changed.items() + if key != "DoLateRadTruncation" + ] + return len(numeric), math.prod(numeric) if numeric else 1.0, tuple(sorted(changed)) + + +def refine_numeric_boosts( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + evaluator, + tolerance: float, + cost_function=None, +) -> tuple[dict[str, float | bool], ComparisonResult]: + cost_function = settings_cost if cost_function is None else cost_function + initial_settings = refine_discrete_accuracy_settings(settings, raw_settings) + initial_comparison = evaluator(initial_settings) + if initial_comparison is None or not initial_comparison.passed: + raise RuntimeError("refine_numeric_boosts requires an initially passing settings point") + + numeric_keys = tuple( + key + for key in initial_settings + if key != "DoLateRadTruncation" and float(initial_settings[key]) - float(raw_settings[key]) > tolerance + ) + best_settings, best_comparison = maybe_restore_late_rad_truncation( + initial_settings, raw_settings, evaluator, initial_comparison + ) + + if len(numeric_keys) == 1: + key = numeric_keys[0] + best_settings, best_comparison = refine_single_numeric_boost( + best_settings, + raw_settings, + evaluator, + tolerance, + key, + best_comparison, + ) + best_settings, best_comparison = maybe_restore_late_rad_truncation( + best_settings, + raw_settings, + evaluator, + best_comparison, + ) + return rounded_numeric_settings(best_settings), best_comparison + + starts: list[tuple[dict[str, float | bool], ComparisonResult]] = [(best_settings, best_comparison)] + balanced_settings, balanced_comparison = refine_balanced_path( + initial_settings, raw_settings, evaluator, tolerance, numeric_keys + ) + if balanced_comparison is not None: + starts.append( + maybe_restore_late_rad_truncation(balanced_settings, raw_settings, evaluator, balanced_comparison) + ) + + key_orders = refinement_key_orders(numeric_keys) + for start_settings, start_comparison in starts: + for key_order in key_orders: + refined_settings, refined_comparison = coordinate_refine_numeric_boosts( + start_settings, + raw_settings, + evaluator, + tolerance, + key_order, + start_comparison, + ) + if cost_function(refined_settings, raw_settings) < cost_function(best_settings, raw_settings): + best_settings = refined_settings + best_comparison = refined_comparison + + return rounded_numeric_settings(best_settings), best_comparison + + +def maybe_restore_late_rad_truncation( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + evaluator, + comparison: ComparisonResult, +) -> tuple[dict[str, float | bool], ComparisonResult]: + current = dict(settings) + current_comparison = comparison + if current.get("DoLateRadTruncation") != raw_settings.get("DoLateRadTruncation"): + trial = dict(current) + trial["DoLateRadTruncation"] = raw_settings["DoLateRadTruncation"] + comparison = evaluator(trial) + if comparison is not None and comparison.passed: + current = trial + current_comparison = comparison + return current, current_comparison + + +def refine_balanced_path( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + evaluator, + tolerance: float, + numeric_keys: tuple[str, ...], +) -> tuple[dict[str, float | bool], ComparisonResult | None]: + continuous_keys = tuple(key for key in numeric_keys if key not in DISCRETE_ACCURACY_VALUES) + if len(continuous_keys) != len(numeric_keys): + settings = refine_discrete_accuracy_settings(settings, raw_settings) + if not numeric_keys: + comparison = evaluator(settings) + return dict(settings), comparison + if not continuous_keys: + comparison = evaluator(settings) + return dict(settings), comparison + + span = max(float(settings[key]) - float(raw_settings[key]) for key in continuous_keys) + if span <= tolerance: + comparison = evaluator(settings) + return dict(settings), comparison + + low = 0.0 + high = 1.0 + current = dict(settings) + current_comparison = evaluator(current) + while (high - low) * span > tolerance: + mid = (low + high) / 2 + trial = dict(settings) + for key in continuous_keys: + trial[key] = float(raw_settings[key]) + mid * (float(settings[key]) - float(raw_settings[key])) + comparison = evaluator(trial) + if comparison is None: + break + if comparison.passed: + current = trial + current_comparison = comparison + high = mid + else: + low = mid + return current, current_comparison + + +def refinement_key_orders(numeric_keys: tuple[str, ...]) -> list[tuple[str, ...]]: + if not numeric_keys: + return [()] + orders = [numeric_keys, tuple(reversed(numeric_keys))] + orders.extend(numeric_keys[index:] + numeric_keys[:index] for index in range(1, len(numeric_keys))) + return list(dict.fromkeys(orders)) + + +def coordinate_refine_numeric_boosts( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + evaluator, + tolerance: float, + key_order: tuple[str, ...], + comparison: ComparisonResult, +) -> tuple[dict[str, float | bool], ComparisonResult]: + current = dict(settings) + current_comparison = comparison + improved = True + while improved: + improved = False + for key in key_order: + before = float(current[key]) + current, current_comparison = refine_single_numeric_boost( + current, raw_settings, evaluator, tolerance, key, current_comparison + ) + improved = improved or float(current[key]) < before - tolerance / 2 + return current, current_comparison + + +def refine_single_numeric_boost( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + evaluator, + tolerance: float, + key: str, + comparison: ComparisonResult, +) -> tuple[dict[str, float | bool], ComparisonResult]: + current = dict(settings) + current_comparison = comparison + if key in DISCRETE_ACCURACY_VALUES: + return refine_discrete_numeric_boost(current, raw_settings, evaluator, key, current_comparison) + low = float(raw_settings[key]) + high = float(current[key]) + if high - low <= tolerance: + return current, current_comparison + while high - low > tolerance: + mid = (low + high) / 2 + trial = dict(current) + trial[key] = mid + trial_comparison = evaluator(trial) + if trial_comparison is None: + break + if trial_comparison.passed: + current = trial + current_comparison = trial_comparison + high = mid + else: + low = mid + return current, current_comparison + + +def refine_discrete_accuracy_settings(settings: dict[str, float | bool], raw_settings: dict[str, float | bool]): + refined = dict(settings) + for key in DISCRETE_ACCURACY_VALUES: + if key in refined and key in raw_settings: + refined[key] = next_meaningful_accuracy_value(key, float(raw_settings[key]), float(refined[key])) + return refined + + +def refine_discrete_numeric_boost( + settings: dict[str, float | bool], + raw_settings: dict[str, float | bool], + evaluator, + key: str, + comparison: ComparisonResult, +) -> tuple[dict[str, float | bool], ComparisonResult]: + current = dict(settings) + current_comparison = comparison + candidates = discrete_accuracy_candidates(key, float(raw_settings[key]), float(current[key])) + for value in candidates: + trial = dict(current) + trial[key] = value + trial_comparison = evaluator(trial) + if trial_comparison is not None and trial_comparison.passed: + current = trial + current_comparison = trial_comparison + break + return current, current_comparison + + +def discrete_accuracy_candidates(key: str, raw_value: float, high_value: float) -> tuple[float, ...]: + values = [raw_value] + values.extend(value for value in DISCRETE_ACCURACY_VALUES[key] if raw_value < value <= high_value) + if high_value not in values: + values.append(high_value) + return tuple(dict.fromkeys(values)) + + +def rounded_numeric_settings( + settings: dict[str, float | bool], +) -> dict[str, float | bool]: + rounded = dict(settings) + for key, value in rounded.items(): + if key != "DoLateRadTruncation": + rounded[key] = round(float(value), 6) + return rounded + + +def settings_key(settings: dict[str, float | bool]) -> tuple: + return tuple( + (key, bool(value) if key == "DoLateRadTruncation" else round(float(value), 8)) + for key, value in sorted(settings.items()) + ) + + +def format_settings(settings: dict[str, float | bool]) -> str: + preferred_order = ( + "AccuracyBoost", + "lSampleBoost", + "lAccuracyBoost", + "IntTolBoost", + "DoLateRadTruncation", + ) + ordered_keys = [key for key in preferred_order if key in settings] + sorted( + key for key in settings if key not in preferred_order + ) + return ", ".join(f"{key}={settings[key]}" for key in ordered_keys) + + +def effective_lmax(args: argparse.Namespace) -> int | None: + return args.lmax or args.set_for_lmax + + +def main(argv: list[str] | None = None, *, prog: str | None = None) -> int: + """Run the accuracy checker command-line interface.""" + parser = build_parser(prog=prog) + args = parser.parse_args(argv) + ini_file = Path(args.ini_file) + high_accuracy_settings = requested_accuracy_settings(args) + + print(f"Input ini: {ini_file}") + print(f"High-accuracy settings: {format_settings(high_accuracy_settings)}") + + base_params = load_params(ini_file, no_validate=args.no_validate) + if args.mpk_tolerance is None: + args.mpk_tolerance = MPK_TOLERANCE if base_params.Transfer.high_precision else 3e-3 + noise_config = None + if args.chi2: + try: + noise_config = chi2_noise_config(args) + except ValueError as exc: + parser.error(str(exc)) + result = compare_params_accuracy( + base_params, + reference_accuracy_settings=high_accuracy_settings, + lmax=effective_lmax(args), + set_for_lmax=args.set_for_lmax, + lens_margin=args.lens_margin, + lens_potential_accuracy=args.lens_potential_accuracy, + reference_lens_margin=args.reference_lens_margin, + reference_lens_potential_accuracy=args.reference_lens_potential_accuracy, + mpk_kmin=args.mpk_kmin, + mpk_npoints=args.mpk_npoints, + derived_tolerance=args.derived_tolerance, + mpk_tolerance=args.mpk_tolerance, + chi2_config=noise_config, + ) + standard = result.standard + reference = result.reference + comparison = result.comparison + + print_run_summary(standard, reference) + print_comparison(comparison) + + if args.chi2: + print_chi2_result(result.chi2, noise_config.name) + + if args.plot_dir: + plot_errors(standard, reference, args.plot_dir) + + if args.find_minimal_boosts: + search_result = find_minimal_boosts( + ini_file, + args, + reference, + high_accuracy_settings, + raw_comparison=comparison, + raw_run=standard, + ) + settings, search_comparison = search_result + if settings is None: + print("\nNo passing candidate settings found.") + print(f"\n{failure_summary(comparison)}") + return 1 + else: + print(f"\nMinimal passing settings from search: {format_settings(settings)}") + if isinstance(search_result, SearchResult): + print_search_timing_summary(search_result) + if search_comparison: + print_comparison(search_comparison) + if args.refine_accuracy_components: + component_result = refine_accuracy_components(ini_file, args, reference, high_accuracy_settings) + component_settings, component_comparison = component_result + if component_settings is None: + print("\nNo passing AccuracyBoost=1 component settings found.") + else: + print( + "\nPassing AccuracyBoost=1 component settings: " + f"{format_settings(changed_settings(component_settings, component_accuracy_settings(base_params)))}" + ) + if isinstance(component_result, SearchResult): + print_search_timing_summary(component_result) + if component_comparison: + print_comparison(component_comparison) + return 0 + + if comparison.passed: + print("\nPASS: standard results are stable against boosted accuracy settings.") + return 0 + print(f"\n{failure_summary(comparison)}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/camb/inifile.py b/camb/inifile.py index 5e255965..af579d11 100644 --- a/camb/inifile.py +++ b/camb/inifile.py @@ -1,431 +1,431 @@ -import os - -import numpy as np - - -class IniError(Exception): - pass - - -class IniFile: - """ - Class for storing option parameter values and reading/saving to file. - - Unlike standard .ini files, IniFile allows inheritance, in that a .ini file can use - INCLUDE(..) and DEFAULT(...) to include or override settings in another file (to avoid duplication). - - :ivar params: dictionary of name, values stored - :ivar comments: dictionary of optional comments for parameter names - """ - - def __init__(self, settings=None, keep_includes=False, expand_environment_variables=True): - """ - - :param settings: a filename of a .ini file to read, or a dictionary of name/values - :param keep_includes: - - False: load all INCLUDE and DEFAULT files, making one params dictionary - - True: only load settings in main file, and store INCLUDE and DEFAULT entries into defaults - and includes filename lists. - :param expand_environment_variables: whether to expand $(var) placeholders in parameter values - using environment variables - """ - - self.params = dict() - self.comments = dict() - self.readOrder = [] - self.defaults = [] - self.includes = [] - self.original_filename = None - self.expand_environment_variables = expand_environment_variables - if isinstance(settings, str): - self.readFile(settings, keep_includes) - elif settings: - self.params.update(settings) - - def expand_placeholders(self, s): - """Expand shell variables of the forms $(var), like in Makefiles.""" - if "$" not in s: - return s - - res = [] - index = 0 - while index < len(s): - c = s[index] - if c == "$": - if index + 1 >= len(s): - res.append(c) - break - - if s[index + 1] == "$": - res.append(c) - index += 2 - continue - - if s[index + 1] == "(": - close_index = s.find(")", index + 2) - if close_index == -1: - raise IniError(f"Unterminated environment variable placeholder in {s!r}") - - var = s[index + 2 : close_index] - res.append(os.environ.get(var, s[index : close_index + 1])) - index = close_index + 1 - continue - - res.append(c) - else: - res.append(c) - index += 1 - return "".join(res) - - def readFile(self, filename, keep_includes=False, if_not_defined=False): - try: - fileincludes = [] - filedefaults = [] - self.original_filename = filename - comments = [] - with open(filename, encoding="utf-8-sig") as textFileHandle: - for line in textFileHandle: - s = line.strip() - if s == "END": - break - if s.startswith("#"): - comments.append(s[1:].rstrip()) - continue - elif s.startswith("INCLUDE("): - fileincludes.append(s[s.find("(") + 1 : s.rfind(")")]) - elif s.startswith("DEFAULT("): - filedefaults.append(s[s.find("(") + 1 : s.rfind(")")]) - elif s != "": - eq = s.find("=") - if eq >= 0: - key = s[0:eq].strip() - if key in self.params: - if if_not_defined: - continue - raise IniError("Error: duplicate key: " + key + " in " + filename) - value = s[eq + 1 :].strip() - if self.expand_environment_variables: - value = self.expand_placeholders(value) - self.params[key] = value - self.readOrder.append(key) - if len(comments): - self.comments[key] = comments - if not s.startswith("#"): - comments = [] - - if keep_includes: - self.includes += fileincludes - self.defaults += filedefaults - else: - for ffile in fileincludes: - if os.path.isabs(ffile): - self.readFile(ffile, if_not_defined=if_not_defined) - else: - self.readFile( - os.path.join(os.path.dirname(filename), ffile), - if_not_defined=if_not_defined, - ) - for ffile in filedefaults: - if os.path.isabs(ffile): - self.readFile(ffile, if_not_defined=True) - else: - self.readFile( - os.path.join(os.path.dirname(filename), ffile), - if_not_defined=True, - ) - - return self.params - except Exception: - print("Error in " + filename) - raise - - def __str__(self): - return "\n".join(self.fileLines()) - - def saveFile(self, filename=None): - """ - Save to a .ini file. - - :param filename: name of file to save to - """ - - if not filename: - filename = self.original_filename - if not filename: - raise IniError("No filename for iniFile.saveFile()") - with open(filename, "w", encoding="utf-8") as f: - f.write(str(self)) - - def fileLines(self): - def asIniText(value): - if isinstance(value, str): - return value - if isinstance(value, bool): - return str(value)[0] - return str(value) - - parameterLines = [] - for include in self.includes: - parameterLines.append("INCLUDE(" + include + ")") - for default in self.defaults: - parameterLines.append("DEFAULT(" + default + ")") - - keys = list(self.params.keys()) - keys.sort() - - for key in self.readOrder: - if key in keys: - parameterLines.append(key + "=" + asIniText(self.params[key])) - keys.remove(key) - for key in keys: - parameterLines.append(key + "=" + asIniText(self.params[key])) - - return parameterLines - - def replaceTags(self, placeholder, text): - for key in self.params: - self.params[key] = self.params[key].replace(placeholder, text) - return self.params - - def delete_keys(self, keys): - for k in keys: - self.params.pop(k, None) - - def _undefined(self, name): - raise IniError("parameter not defined: " + name) - - def hasKey(self, name): - """ - Test if key name exists. - - :param name: parameter name - :return: True or False test if key name exists - """ - return name in self.params - - def isSet(self, name, allowEmpty=False): - """ - Tests whether value for name is set or is empty. - - :param name: name of parameter - :param allowEmpty: whether to allow empty strings (return True if parameter name exists but is not set, "x = ") - """ - - return name in self.params and (allowEmpty or self.params[name] != "") - - def asType(self, name, tp, default=None, allowEmpty=False): - if self.isSet(name, allowEmpty): - if tp is bool: - return self.bool(name, default) - elif tp is list: - return self.split(name, default) - elif tp is np.ndarray: - return self.ndarray(name, default) - else: - return tp(self.params[name]) - elif default is not None: - return default - else: - self._undefined(name) - - def setAttr(self, name, instance, default=None, allowEmpty=False): - """ - Set attribute of an object to value of parameter, using same type as existing value or default. - - :param name: parameter name - :param instance: instance of an object, so instance.name is the value to set - :param default: default value if instance.name does not exist - :param allowEmpty: whether to allow empty values - """ - default = getattr(instance, name, default) - setattr( - instance, - name, - self.asType(name, type(default), default, allowEmpty=allowEmpty), - ) - - def getAttr(self, instance, name, default=None, comment=None): - val = getattr(instance, name, default) - self.params[name] = val - if comment: - self.comments[name] = comment - - def bool(self, name, default=False): - """ - Get boolean value. - - :param name: parameter name - :param default: default value if not set - """ - if self.isSet(name): - s = self.params[name] - if isinstance(s, bool): - return s - if s[0] == "T": - return True - elif s[0] == "F": - return False - raise IniError("parameter does not have valid T(rue) or F(alse) boolean value: " + name) - elif default is not None: - return default - else: - self._undefined(name) - - def bool_list(self, name, default=None): - """ - Get list of boolean values, e.g. from name = T F T. - - :param name: parameter name - :param default: default value if not set - """ - - if not default: - default = [] - return self.split(name, default, tp=bool) - - def string(self, name, default=None, allowEmpty=True): - """ - Get string value. - - :param name: parameter name - :param default: default value if not set - :param allowEmpty: whether to return empty string if value is empty (otherwise return default) - """ - return self.asType(name, str, default, allowEmpty=allowEmpty) - - def list(self, name, default=None, tp=None): - """ - Get list (from space-separated values). - - :param name: parameter name - :param default: default value - :param tp: type for each member of the list - """ - - if not default: - default = [] - return self.split(name, default, tp) - - def float(self, name, default=None): - """ - Get float value. - - :param name: parameter name - :param default: default value - """ - return self.asType(name, float, default) - - def float_list(self, name, default=None): - """ - Get list of float values. - - :param name: parameter name - :param default: default value if not set - """ - - if not default: - default = [] - return self.split(name, default, tp=float) - - def int(self, name, default=None): - """ - Get int value. - - :param name: parameter name - :param default: default value - """ - - return self.asType(name, int, default) - - def int_list(self, name, default=None): - """ - Get list of int values. - - :param name: parameter name - :param default: default value - """ - - if not default: - default = [] - return self.split(name, default, tp=int) - - def split(self, name, default=None, tp=None): - """ - Gets a list of values, optionally cast to type tp. - - :param name: parameter name - :param default: default value - :param tp: type for each list member - """ - if name in self.params and isinstance(self.params[name], (list, tuple)): - if tp is None: - return self.params[name] - else: - return [tp(x) for x in self.params[name]] - - s = self.string(name, default) - if isinstance(s, str): - if tp is not None: - return [tp(x) for x in s.split()] - return s.split() - else: - return s - - def ndarray(self, name, default=None, tp=np.float64): - """ - Get numpy array of values. - - :param name: parameter name - :param default: default value - :param tp: type for array - """ - return np.array(self.split(name, default, tp=tp)) - - def array_int(self, name, index=1, default=None): - """ - Get one int value, for entries of the form name(index). - - :param name: base parameter name - :param index: index (in brackets) - :param default: default value - """ - return self.int(name + "(%u)" % index, default) - - def array_string(self, name, index=1, default=None): - """ - Get one str value, for entries of the form name(index). - - :param name: base parameter name - :param index: index (in brackets) - :param default: default value - """ - - return self.string(name + "(%u)" % index, default) - - def array_bool(self, name, index=1, default=None): - """ - Get one boolean value, for entries of the form name(index). - - :param name: base parameter name - :param index: index (in brackets) - :param default: default value - """ - - return self.bool(name + "(%u)" % index, default) - - def array_float(self, name, index=1, default=None): - """ - Get one float value, for entries of the form name(index). - - :param name: base parameter name - :param index: index (in brackets) - :param default: default value - """ - - return self.float(name + "(%u)" % index, default) - - def relativeFileName(self, name, default=None): - s = self.string(name, default) - if not os.path.isabs(s) and self.original_filename is not None: - return os.path.join(os.path.dirname(self.original_filename), s) - return s +import os + +import numpy as np + + +class IniError(Exception): + pass + + +class IniFile: + """ + Class for storing option parameter values and reading/saving to file. + + Unlike standard .ini files, IniFile allows inheritance, in that a .ini file can use + INCLUDE(..) and DEFAULT(...) to include or override settings in another file (to avoid duplication). + + :ivar params: dictionary of name, values stored + :ivar comments: dictionary of optional comments for parameter names + """ + + def __init__(self, settings=None, keep_includes=False, expand_environment_variables=True): + """ + + :param settings: a filename of a .ini file to read, or a dictionary of name/values + :param keep_includes: + - False: load all INCLUDE and DEFAULT files, making one params dictionary + - True: only load settings in main file, and store INCLUDE and DEFAULT entries into defaults + and includes filename lists. + :param expand_environment_variables: whether to expand $(var) placeholders in parameter values + using environment variables + """ + + self.params = dict() + self.comments = dict() + self.readOrder = [] + self.defaults = [] + self.includes = [] + self.original_filename = None + self.expand_environment_variables = expand_environment_variables + if isinstance(settings, str): + self.readFile(settings, keep_includes) + elif settings: + self.params.update(settings) + + def expand_placeholders(self, s): + """Expand shell variables of the forms $(var), like in Makefiles.""" + if "$" not in s: + return s + + res = [] + index = 0 + while index < len(s): + c = s[index] + if c == "$": + if index + 1 >= len(s): + res.append(c) + break + + if s[index + 1] == "$": + res.append(c) + index += 2 + continue + + if s[index + 1] == "(": + close_index = s.find(")", index + 2) + if close_index == -1: + raise IniError(f"Unterminated environment variable placeholder in {s!r}") + + var = s[index + 2 : close_index] + res.append(os.environ.get(var, s[index : close_index + 1])) + index = close_index + 1 + continue + + res.append(c) + else: + res.append(c) + index += 1 + return "".join(res) + + def readFile(self, filename, keep_includes=False, if_not_defined=False): + try: + fileincludes = [] + filedefaults = [] + self.original_filename = filename + comments = [] + with open(filename, encoding="utf-8-sig") as textFileHandle: + for line in textFileHandle: + s = line.strip() + if s == "END": + break + if s.startswith("#"): + comments.append(s[1:].rstrip()) + continue + elif s.startswith("INCLUDE("): + fileincludes.append(s[s.find("(") + 1 : s.rfind(")")]) + elif s.startswith("DEFAULT("): + filedefaults.append(s[s.find("(") + 1 : s.rfind(")")]) + elif s != "": + eq = s.find("=") + if eq >= 0: + key = s[0:eq].strip() + if key in self.params: + if if_not_defined: + continue + raise IniError("Error: duplicate key: " + key + " in " + filename) + value = s[eq + 1 :].strip() + if self.expand_environment_variables: + value = self.expand_placeholders(value) + self.params[key] = value + self.readOrder.append(key) + if len(comments): + self.comments[key] = comments + if not s.startswith("#"): + comments = [] + + if keep_includes: + self.includes += fileincludes + self.defaults += filedefaults + else: + for ffile in fileincludes: + if os.path.isabs(ffile): + self.readFile(ffile, if_not_defined=if_not_defined) + else: + self.readFile( + os.path.join(os.path.dirname(filename), ffile), + if_not_defined=if_not_defined, + ) + for ffile in filedefaults: + if os.path.isabs(ffile): + self.readFile(ffile, if_not_defined=True) + else: + self.readFile( + os.path.join(os.path.dirname(filename), ffile), + if_not_defined=True, + ) + + return self.params + except Exception: + print("Error in " + filename) + raise + + def __str__(self): + return "\n".join(self.fileLines()) + + def saveFile(self, filename=None): + """ + Save to a .ini file. + + :param filename: name of file to save to + """ + + if not filename: + filename = self.original_filename + if not filename: + raise IniError("No filename for iniFile.saveFile()") + with open(filename, "w", encoding="utf-8") as f: + f.write(str(self)) + + def fileLines(self): + def asIniText(value): + if isinstance(value, str): + return value + if isinstance(value, bool): + return str(value)[0] + return str(value) + + parameterLines = [] + for include in self.includes: + parameterLines.append("INCLUDE(" + include + ")") + for default in self.defaults: + parameterLines.append("DEFAULT(" + default + ")") + + keys = list(self.params.keys()) + keys.sort() + + for key in self.readOrder: + if key in keys: + parameterLines.append(key + "=" + asIniText(self.params[key])) + keys.remove(key) + for key in keys: + parameterLines.append(key + "=" + asIniText(self.params[key])) + + return parameterLines + + def replaceTags(self, placeholder, text): + for key in self.params: + self.params[key] = self.params[key].replace(placeholder, text) + return self.params + + def delete_keys(self, keys): + for k in keys: + self.params.pop(k, None) + + def _undefined(self, name): + raise IniError("parameter not defined: " + name) + + def hasKey(self, name): + """ + Test if key name exists. + + :param name: parameter name + :return: True or False test if key name exists + """ + return name in self.params + + def isSet(self, name, allowEmpty=False): + """ + Tests whether value for name is set or is empty. + + :param name: name of parameter + :param allowEmpty: whether to allow empty strings (return True if parameter name exists but is not set, "x = ") + """ + + return name in self.params and (allowEmpty or self.params[name] != "") + + def asType(self, name, tp, default=None, allowEmpty=False): + if self.isSet(name, allowEmpty): + if tp is bool: + return self.bool(name, default) + elif tp is list: + return self.split(name, default) + elif tp is np.ndarray: + return self.ndarray(name, default) + else: + return tp(self.params[name]) + elif default is not None: + return default + else: + self._undefined(name) + + def setAttr(self, name, instance, default=None, allowEmpty=False): + """ + Set attribute of an object to value of parameter, using same type as existing value or default. + + :param name: parameter name + :param instance: instance of an object, so instance.name is the value to set + :param default: default value if instance.name does not exist + :param allowEmpty: whether to allow empty values + """ + default = getattr(instance, name, default) + setattr( + instance, + name, + self.asType(name, type(default), default, allowEmpty=allowEmpty), + ) + + def getAttr(self, instance, name, default=None, comment=None): + val = getattr(instance, name, default) + self.params[name] = val + if comment: + self.comments[name] = comment + + def bool(self, name, default=False): + """ + Get boolean value. + + :param name: parameter name + :param default: default value if not set + """ + if self.isSet(name): + s = self.params[name] + if isinstance(s, bool): + return s + if s[0] == "T": + return True + elif s[0] == "F": + return False + raise IniError("parameter does not have valid T(rue) or F(alse) boolean value: " + name) + elif default is not None: + return default + else: + self._undefined(name) + + def bool_list(self, name, default=None): + """ + Get list of boolean values, e.g. from name = T F T. + + :param name: parameter name + :param default: default value if not set + """ + + if not default: + default = [] + return self.split(name, default, tp=bool) + + def string(self, name, default=None, allowEmpty=True): + """ + Get string value. + + :param name: parameter name + :param default: default value if not set + :param allowEmpty: whether to return empty string if value is empty (otherwise return default) + """ + return self.asType(name, str, default, allowEmpty=allowEmpty) + + def list(self, name, default=None, tp=None): + """ + Get list (from space-separated values). + + :param name: parameter name + :param default: default value + :param tp: type for each member of the list + """ + + if not default: + default = [] + return self.split(name, default, tp) + + def float(self, name, default=None): + """ + Get float value. + + :param name: parameter name + :param default: default value + """ + return self.asType(name, float, default) + + def float_list(self, name, default=None): + """ + Get list of float values. + + :param name: parameter name + :param default: default value if not set + """ + + if not default: + default = [] + return self.split(name, default, tp=float) + + def int(self, name, default=None): + """ + Get int value. + + :param name: parameter name + :param default: default value + """ + + return self.asType(name, int, default) + + def int_list(self, name, default=None): + """ + Get list of int values. + + :param name: parameter name + :param default: default value + """ + + if not default: + default = [] + return self.split(name, default, tp=int) + + def split(self, name, default=None, tp=None): + """ + Gets a list of values, optionally cast to type tp. + + :param name: parameter name + :param default: default value + :param tp: type for each list member + """ + if name in self.params and isinstance(self.params[name], (list, tuple)): + if tp is None: + return self.params[name] + else: + return [tp(x) for x in self.params[name]] + + s = self.string(name, default) + if isinstance(s, str): + if tp is not None: + return [tp(x) for x in s.split()] + return s.split() + else: + return s + + def ndarray(self, name, default=None, tp=np.float64): + """ + Get numpy array of values. + + :param name: parameter name + :param default: default value + :param tp: type for array + """ + return np.array(self.split(name, default, tp=tp)) + + def array_int(self, name, index=1, default=None): + """ + Get one int value, for entries of the form name(index). + + :param name: base parameter name + :param index: index (in brackets) + :param default: default value + """ + return self.int(name + "(%u)" % index, default) + + def array_string(self, name, index=1, default=None): + """ + Get one str value, for entries of the form name(index). + + :param name: base parameter name + :param index: index (in brackets) + :param default: default value + """ + + return self.string(name + "(%u)" % index, default) + + def array_bool(self, name, index=1, default=None): + """ + Get one boolean value, for entries of the form name(index). + + :param name: base parameter name + :param index: index (in brackets) + :param default: default value + """ + + return self.bool(name + "(%u)" % index, default) + + def array_float(self, name, index=1, default=None): + """ + Get one float value, for entries of the form name(index). + + :param name: base parameter name + :param index: index (in brackets) + :param default: default value + """ + + return self.float(name + "(%u)" % index, default) + + def relativeFileName(self, name, default=None): + s = self.string(name, default) + if not os.path.isabs(s) and self.original_filename is not None: + return os.path.join(os.path.dirname(self.original_filename), s) + return s diff --git a/camb/initialpower.py b/camb/initialpower.py index f6a9ee81..f45cb3ab 100644 --- a/camb/initialpower.py +++ b/camb/initialpower.py @@ -1,217 +1,217 @@ -# Initial power spectrum parameters - -from .baseconfig import POINTER, CAMBError, F2003Class, byref, c_double, c_int, fortran_class, np, numpy_1d - -tensor_parameterization_names = ["tensor_param_indeptilt", "tensor_param_rpivot", "tensor_param_AT"] -tensor_param_indeptilt = 1 -tensor_param_rpivot = 2 -tensor_param_AT = 3 - - -class InitialPower(F2003Class): - """ - Abstract base class for initial power spectrum classes - """ - - _fortran_class_module_ = "InitialPower" - - def set_params(self, **kwargs): - pass - - -@fortran_class -class SplinedInitialPower(InitialPower): - """ - Object to store a generic primordial spectrum set from a set of sampled k_i, P(k_i) values. - - See :meth:`.model.CAMBparams.set_initial_power_function` for a convenience constructor function to - set a general interpolated P(k) model from a python function. - """ - - _fortran_class_name_ = "TSplinedInitialPower" - - _fields_ = ( - ("effective_ns_for_nonlinear", c_double, "Effective n_s to use for approximate non-linear correction models"), - ) - - _methods_ = ( - ("HasTensors", [], c_int), - ("SetScalarTable", [POINTER(c_int), numpy_1d, numpy_1d]), - ("SetTensorTable", [POINTER(c_int), numpy_1d, numpy_1d]), - ("SetScalarLogRegular", [POINTER(c_double), POINTER(c_double), POINTER(c_int), numpy_1d]), - ("SetTensorLogRegular", [POINTER(c_double), POINTER(c_double), POINTER(c_int), numpy_1d]), - ) - - def __init__(self, **kwargs): - if kwargs.get("PK") is not None: - self.set_scalar_table(kwargs["ks"], kwargs["PK"]) - ns_eff = kwargs.get("effective_ns_for_nonlinear") - if ns_eff is not None: - self.effective_ns_for_nonlinear = ns_eff - - def __getstate__(self): - raise TypeError("Cannot save class with splines") - - def has_tensors(self): - """ - Is the tensor spectrum set? - - :return: True if tensors - """ - return self.f_HasTensors() != 0 - - def set_scalar_table(self, k, PK): - """ - Set arrays of k and P(k) values for cubic spline interpolation. - Note that using :meth:`set_scalar_log_regular` may be better - (faster, and easier to get fine enough spacing a low k) - - :param k: array of k values (Mpc^{-1}) - :param PK: array of scalar power spectrum values - """ - self.f_SetScalarTable( - byref(c_int(len(k))), np.ascontiguousarray(k, dtype=np.float64), np.ascontiguousarray(PK, dtype=np.float64) - ) - - def set_tensor_table(self, k, PK): - """ - Set arrays of k and P_t(k) values for cubic spline interpolation - - :param k: array of k values (Mpc^{-1}) - :param PK: array of tensor power spectrum values - """ - self.f_SetTensorTable( - byref(c_int(len(k))), np.ascontiguousarray(k, dtype=np.float64), np.ascontiguousarray(PK, dtype=np.float64) - ) - - def set_scalar_log_regular(self, kmin, kmax, PK): - """ - Set log-regular cubic spline interpolation for P(k) - - :param kmin: minimum k value (not minimum log(k)) - :param kmax: maximum k value (inclusive) - :param PK: array of scalar power spectrum values, with PK[0]=P(kmin) and PK[-1]=P(kmax) - """ - self.f_SetScalarLogRegular( - byref(c_double(kmin)), - byref(c_double(kmax)), - byref(c_int(len(PK))), - np.ascontiguousarray(PK, dtype=np.float64), - ) - - def set_tensor_log_regular(self, kmin, kmax, PK): - """ - Set log-regular cubic spline interpolation for tensor spectrum P_t(k) - - :param kmin: minimum k value (not minimum log(k)) - :param kmax: maximum k value (inclusive) - :param PK: array of scalar power spectrum values, with PK[0]=P_t(kmin) and PK[-1]=P_t(kmax) - """ - - self.f_SetTensorLogRegular( - byref(c_double(kmin)), - byref(c_double(kmax)), - byref(c_int(len(PK))), - np.ascontiguousarray(PK, dtype=np.float64), - ) - - -@fortran_class -class InitialPowerLaw(InitialPower): - """ - Object to store parameters for the primordial power spectrum in the standard power law expansion. - - """ - - _fields_ = ( - ("tensor_parameterization", c_int, {"names": tensor_parameterization_names, "start": 1}), - ("ns", c_double), - ("nrun", c_double), - ("nrunrun", c_double), - ("nt", c_double), - ("ntrun", c_double), - ("r", c_double), - ("pivot_scalar", c_double), - ("pivot_tensor", c_double), - ("As", c_double), - ("At", c_double), - ) - - _fortran_class_name_ = "TInitialPowerLaw" - - def __init__(self, **kwargs): - self.set_params(**kwargs) - - def set_params( - self, - As=2e-9, - ns=0.96, - nrun=0, - nrunrun=0.0, - r=0.0, - nt=None, - ntrun=0.0, - pivot_scalar=0.05, - pivot_tensor=0.05, - parameterization="tensor_param_rpivot", - ): - r""" - Set parameters using standard power law parameterization. If nt=None, uses inflation consistency relation. - - :param As: comoving curvature power at k=pivot_scalar (:math:`A_s`) - :param ns: scalar spectral index :math:`n_s` - :param nrun: running of scalar spectral index :math:`d n_s/d \log k` - :param nrunrun: running of running of spectral index, :math:`d^2 n_s/d (\log k)^2` - :param r: tensor to scalar ratio at pivot - :param nt: tensor spectral index :math:`n_t`. If None, set using inflation consistency - :param ntrun: running of tensor spectral index - :param pivot_scalar: pivot scale for scalar spectrum - :param pivot_tensor: pivot scale for tensor spectrum - :param parameterization: See CAMB notes. One of - - tensor_param_indeptilt = 1 - - tensor_param_rpivot = 2 - - tensor_param_AT = 3 - :return: self - """ - - if parameterization not in [ - tensor_param_rpivot, - tensor_param_indeptilt, - "tensor_param_rpivot", - "tensor_param_indeptilt", - ]: - raise CAMBError("Initial power parameterization not supported here") - self.tensor_parameterization = parameterization - self.As = As - self.ns = ns - self.nrun = nrun - self.nrunrun = nrunrun - if nt is None: - # set from inflationary consistency - if ntrun: - raise CAMBError("ntrun set but using inflation consistency (nt=None)") - if self.tensor_parameterization != "tensor_param_rpivot": - raise CAMBError( - "tensor parameterization not tensor_param_rpivot with inflation consistency: " - f"{self.tensor_parameterization}" - ) - if r: - self.nt = -r / 8.0 * (2.0 - ns - r / 8.0) - self.ntrun = r / 8.0 * (r / 8.0 + ns - 1) - else: - self.nt = self.ntrun = 0.0 - else: - self.nt = nt - self.ntrun = ntrun - self.r = r - self.pivot_scalar = pivot_scalar - self.pivot_tensor = pivot_tensor - return self - - def has_tensors(self): - """ - Do these settings have non-zero tensors? - - :return: True if non-zero tensor amplitude - """ - return self.r > 0 +# Initial power spectrum parameters + +from .baseconfig import POINTER, CAMBError, F2003Class, byref, c_double, c_int, fortran_class, np, numpy_1d + +tensor_parameterization_names = ["tensor_param_indeptilt", "tensor_param_rpivot", "tensor_param_AT"] +tensor_param_indeptilt = 1 +tensor_param_rpivot = 2 +tensor_param_AT = 3 + + +class InitialPower(F2003Class): + """ + Abstract base class for initial power spectrum classes + """ + + _fortran_class_module_ = "InitialPower" + + def set_params(self, **kwargs): + pass + + +@fortran_class +class SplinedInitialPower(InitialPower): + """ + Object to store a generic primordial spectrum set from a set of sampled k_i, P(k_i) values. + + See :meth:`.model.CAMBparams.set_initial_power_function` for a convenience constructor function to + set a general interpolated P(k) model from a python function. + """ + + _fortran_class_name_ = "TSplinedInitialPower" + + _fields_ = ( + ("effective_ns_for_nonlinear", c_double, "Effective n_s to use for approximate non-linear correction models"), + ) + + _methods_ = ( + ("HasTensors", [], c_int), + ("SetScalarTable", [POINTER(c_int), numpy_1d, numpy_1d]), + ("SetTensorTable", [POINTER(c_int), numpy_1d, numpy_1d]), + ("SetScalarLogRegular", [POINTER(c_double), POINTER(c_double), POINTER(c_int), numpy_1d]), + ("SetTensorLogRegular", [POINTER(c_double), POINTER(c_double), POINTER(c_int), numpy_1d]), + ) + + def __init__(self, **kwargs): + if kwargs.get("PK") is not None: + self.set_scalar_table(kwargs["ks"], kwargs["PK"]) + ns_eff = kwargs.get("effective_ns_for_nonlinear") + if ns_eff is not None: + self.effective_ns_for_nonlinear = ns_eff + + def __getstate__(self): + raise TypeError("Cannot save class with splines") + + def has_tensors(self): + """ + Is the tensor spectrum set? + + :return: True if tensors + """ + return self.f_HasTensors() != 0 + + def set_scalar_table(self, k, PK): + """ + Set arrays of k and P(k) values for cubic spline interpolation. + Note that using :meth:`set_scalar_log_regular` may be better + (faster, and easier to get fine enough spacing a low k) + + :param k: array of k values (Mpc^{-1}) + :param PK: array of scalar power spectrum values + """ + self.f_SetScalarTable( + byref(c_int(len(k))), np.ascontiguousarray(k, dtype=np.float64), np.ascontiguousarray(PK, dtype=np.float64) + ) + + def set_tensor_table(self, k, PK): + """ + Set arrays of k and P_t(k) values for cubic spline interpolation + + :param k: array of k values (Mpc^{-1}) + :param PK: array of tensor power spectrum values + """ + self.f_SetTensorTable( + byref(c_int(len(k))), np.ascontiguousarray(k, dtype=np.float64), np.ascontiguousarray(PK, dtype=np.float64) + ) + + def set_scalar_log_regular(self, kmin, kmax, PK): + """ + Set log-regular cubic spline interpolation for P(k) + + :param kmin: minimum k value (not minimum log(k)) + :param kmax: maximum k value (inclusive) + :param PK: array of scalar power spectrum values, with PK[0]=P(kmin) and PK[-1]=P(kmax) + """ + self.f_SetScalarLogRegular( + byref(c_double(kmin)), + byref(c_double(kmax)), + byref(c_int(len(PK))), + np.ascontiguousarray(PK, dtype=np.float64), + ) + + def set_tensor_log_regular(self, kmin, kmax, PK): + """ + Set log-regular cubic spline interpolation for tensor spectrum P_t(k) + + :param kmin: minimum k value (not minimum log(k)) + :param kmax: maximum k value (inclusive) + :param PK: array of scalar power spectrum values, with PK[0]=P_t(kmin) and PK[-1]=P_t(kmax) + """ + + self.f_SetTensorLogRegular( + byref(c_double(kmin)), + byref(c_double(kmax)), + byref(c_int(len(PK))), + np.ascontiguousarray(PK, dtype=np.float64), + ) + + +@fortran_class +class InitialPowerLaw(InitialPower): + """ + Object to store parameters for the primordial power spectrum in the standard power law expansion. + + """ + + _fields_ = ( + ("tensor_parameterization", c_int, {"names": tensor_parameterization_names, "start": 1}), + ("ns", c_double), + ("nrun", c_double), + ("nrunrun", c_double), + ("nt", c_double), + ("ntrun", c_double), + ("r", c_double), + ("pivot_scalar", c_double), + ("pivot_tensor", c_double), + ("As", c_double), + ("At", c_double), + ) + + _fortran_class_name_ = "TInitialPowerLaw" + + def __init__(self, **kwargs): + self.set_params(**kwargs) + + def set_params( + self, + As=2e-9, + ns=0.96, + nrun=0, + nrunrun=0.0, + r=0.0, + nt=None, + ntrun=0.0, + pivot_scalar=0.05, + pivot_tensor=0.05, + parameterization="tensor_param_rpivot", + ): + r""" + Set parameters using standard power law parameterization. If nt=None, uses inflation consistency relation. + + :param As: comoving curvature power at k=pivot_scalar (:math:`A_s`) + :param ns: scalar spectral index :math:`n_s` + :param nrun: running of scalar spectral index :math:`d n_s/d \log k` + :param nrunrun: running of running of spectral index, :math:`d^2 n_s/d (\log k)^2` + :param r: tensor to scalar ratio at pivot + :param nt: tensor spectral index :math:`n_t`. If None, set using inflation consistency + :param ntrun: running of tensor spectral index + :param pivot_scalar: pivot scale for scalar spectrum + :param pivot_tensor: pivot scale for tensor spectrum + :param parameterization: See CAMB notes. One of + - tensor_param_indeptilt = 1 + - tensor_param_rpivot = 2 + - tensor_param_AT = 3 + :return: self + """ + + if parameterization not in [ + tensor_param_rpivot, + tensor_param_indeptilt, + "tensor_param_rpivot", + "tensor_param_indeptilt", + ]: + raise CAMBError("Initial power parameterization not supported here") + self.tensor_parameterization = parameterization + self.As = As + self.ns = ns + self.nrun = nrun + self.nrunrun = nrunrun + if nt is None: + # set from inflationary consistency + if ntrun: + raise CAMBError("ntrun set but using inflation consistency (nt=None)") + if self.tensor_parameterization != "tensor_param_rpivot": + raise CAMBError( + "tensor parameterization not tensor_param_rpivot with inflation consistency: " + f"{self.tensor_parameterization}" + ) + if r: + self.nt = -r / 8.0 * (2.0 - ns - r / 8.0) + self.ntrun = r / 8.0 * (r / 8.0 + ns - 1) + else: + self.nt = self.ntrun = 0.0 + else: + self.nt = nt + self.ntrun = ntrun + self.r = r + self.pivot_scalar = pivot_scalar + self.pivot_tensor = pivot_tensor + return self + + def has_tensors(self): + """ + Do these settings have non-zero tensors? + + :return: True if non-zero tensor amplitude + """ + return self.r > 0 diff --git a/camb/model.py b/camb/model.py index 2ebc50a2..98b4881c 100644 --- a/camb/model.py +++ b/camb/model.py @@ -1,1152 +1,1157 @@ -from __future__ import annotations - -import ctypes -import logging -from ctypes import POINTER, byref, c_bool, c_double, c_int, c_void_p -from typing import ClassVar, overload - -from . import _ini, bbn, constants -from . import recombination as recomb -from . import reionization as reion -from .baseconfig import ( - AllocatableArrayDouble, - AllocatableArrayInt, - AllocatableObject, - AllocatableObjectArray, - Array1D, - CAMB_Structure, - CAMBError, - CAMBParamRangeError, - CAMBValueError, - F2003Class, - camblib, - fortran_class, - np, - numpy_1d, - numpy_1d_int, -) -from .dark_energy import DarkEnergyEqnOfState, DarkEnergyModel -from .initialpower import InitialPower, SplinedInitialPower -from .nonlinear import NonLinearModel -from .recombination import RecombinationModel -from .reionization import ReionizationModel -from .sources import SourceWindow - -logger = logging.getLogger(__name__) - -# Union and Optional types are now built-in to Python 3.10+ - -max_nu = 5 -max_transfer_redshifts = 256 -nthermo_derived = 13 -Transfer_kh = 1 -Transfer_cdm = 2 -Transfer_b = 3 -Transfer_g = 4 -Transfer_r = 5 -Transfer_nu = 6 -Transfer_tot = 7 -Transfer_nonu = 8 -Transfer_tot_de = 9 -Transfer_Weyl = 10 -Transfer_Newt_vel_cdm = 11 -Transfer_Newt_vel_baryon = 12 -Transfer_vel_baryon_cdm = 13 -Transfer_max = Transfer_vel_baryon_cdm - -# for 21cm case -Transfer_monopole = 4 -Transfer_vnewt = 5 -Transfer_Tmat = 6 - -NonLinear_none = "NonLinear_none" -NonLinear_pk = "NonLinear_pk" -NonLinear_lens = "NonLinear_lens" -NonLinear_both = "NonLinear_both" -NonLinear_names = [NonLinear_none, NonLinear_pk, NonLinear_lens, NonLinear_both] - -derived_names = [ - "age", - "zstar", - "rstar", - "thetastar", - "DAstar", - "zdrag", - "rdrag", - "kd", - "thetad", - "zeq", - "keq", - "thetaeq", - "thetarseq", -] - -transfer_names = [ - "k/h", - "delta_cdm", - "delta_baryon", - "delta_photon", - "delta_neutrino", - "delta_nu", - "delta_tot", - "delta_nonu", - "delta_tot_de", - "Weyl", - "v_newtonian_cdm", - "v_newtonian_baryon", - "v_baryon_cdm", -] - -evolve_names = transfer_names + [ - "a", - "etak", - "H", - "growth", - "v_photon", - "pi_photon", - "E_2", - "v_neutrino", - "T_source", - "E_source", - "lens_potential_source", -] - -background_names = [ - "x_e", - "opacity", - "visibility", - "cs2b", - "T_b", - "dopacity", - "ddopacity", - "dvisibility", - "ddvisibility", -] -density_names = ["tot", "K", "cdm", "baryon", "photon", "neutrino", "nu", "de"] - -neutrino_hierarchy_normal = "normal" -neutrino_hierarchy_inverted = "inverted" -neutrino_hierarchy_degenerate = "degenerate" -neutrino_hierarchies = [neutrino_hierarchy_normal, neutrino_hierarchy_inverted, neutrino_hierarchy_degenerate] - - -class TransferParams(CAMB_Structure): - """ - Object storing parameters for the matter power spectrum calculation. - - Not intended to be separately instantiated, only used as part of CAMBparams. - """ - - _fields_ = ( - ("high_precision", c_bool, "True for more accuracy"), - ( - "accurate_massive_neutrinos", - c_bool, - "True if you want neutrino transfer functions accurate (false by default)", - ), - ("kmax", c_double, "k_max to output (no h in units)"), - ("k_per_logint", c_int, "number of points per log k interval. If zero, set an irregular optimized spacing"), - ("PK_num_redshifts", c_int, "number of redshifts to calculate"), - ( - "PK_redshifts", - c_double * max_transfer_redshifts, - {"size": "PK_num_redshifts"}, - "redshifts to output for the matter transfer and power", - ), - ) - - -class AccuracyParams(CAMB_Structure): - """ - Structure with parameters governing numerical accuracy. AccuracyBoost will also scale almost all the other - parameters except for lSampleBoost (which is specific to the output interpolation) and lAccuracyBoost - (which is specific to the multipole hierarchy evolution), e.g. setting AccuracyBoost=2, IntTolBoost=1.5, means - that internally the k sampling for integration will be boosted by AccuracyBoost*IntTolBoost = 3. - - Not intended to be separately instantiated, only used as part of CAMBparams. - If you want to set fields with :func:`.camb.set_params`, use 'Accuracy.xxx':yyy in the parameter dictionary. - """ - - _fields_ = ( - ( - "AccuracyBoost", - c_double, - "general accuracy setting effecting everything related to step sizes etc. " - "(including separate settings below except the next two)", - ), - ( - "lSampleBoost", - c_double, - "accuracy for sampling in ell for interpolation for the C_l (if >=50, all ell are calculated)", - ), - ("lAccuracyBoost", c_double, "Boosts number of multipoles integrated in Boltzmann hierarchy"), - ("AccuratePolarization", c_bool, "Do you care about the accuracy of the polarization Cls?"), - ("AccurateBB", c_bool, "Do you care about BB accuracy (e.g. in lensing)"), - ("AccurateReionization", c_bool, "Do you care about percent level accuracy on EE signal from reionization?"), - ("TimeStepBoost", c_double, "Sampling time steps"), - ( - "BackgroundTimeStepBoost", - c_double, - "Number of time steps for background thermal history and source window interpolation", - ), - ("IntTolBoost", c_double, "Tolerances for integrating differential equations"), - ("SourcekAccuracyBoost", c_double, "Accuracy of k sampling for source time integration"), - ("IntkAccuracyBoost", c_double, "Accuracy of k sampling for integration"), - ("TransferkBoost", c_double, "Accuracy of k sampling for transfer functions"), - ("NonFlatIntAccuracyBoost", c_double, "Accuracy of non-flat time integration"), - ("BessIntBoost", c_double, "Accuracy of bessel integration truncation"), - ("LensingBoost", c_double, "Accuracy of the lensing of CMB power spectra"), - ("NonlinSourceBoost", c_double, "Accuracy of steps and kmax used for the non-linear correction"), - ("BesselBoost", c_double, "Accuracy of bessel pre-computation sampling"), - ("LimberBoost", c_double, "Accuracy of Limber approximation use"), - ("SourceLimberBoost", c_double, "Scales when to switch to Limber for source windows"), - ("KmaxBoost", c_double, "Boost max k for source window functions"), - ("neutrino_q_boost", c_double, "Number of momenta integrated for neutrino perturbations"), - ) - - -class SourceTermParams(CAMB_Structure): - """ - Structure with parameters determining how galaxy/lensing/21cm power spectra and transfer functions are calculated. - - Not intended to be separately instantiated, only used as part of CAMBparams. - """ - - _fields_ = ( - ( - "limber_windows", - c_bool, - "Use Limber approximation where appropriate. CMB lensing uses Limber even if limber_window is false, " - + "but method is changed to be consistent with other sources if limber_windows is true", - ), - ( - "limber_phi_lmin", - c_int, - "When limber_windows=True, the minimum L to use Limber approximation for the " - "lensing potential and other sources (which may use higher but not lower)", - ), - ("counts_density", c_bool, "Include the density perturbation source"), - ("counts_redshift", c_bool, "Include redshift distortions"), - ("counts_lensing", c_bool, "Include magnification bias for number counts"), - ("counts_velocity", c_bool, "Non-redshift distortion velocity terms"), - ( - "counts_radial", - c_bool, - "Radial displacement velocity term; does not include time delay; " - "subset of counts_velocity, just 1 / (chi * H) term", - ), - ("counts_timedelay", c_bool, "Include time delay terms * 1 / (H * chi)"), - ("counts_ISW", c_bool, "Include tiny ISW terms"), - ("counts_potential", c_bool, "Include tiny terms in potentials at source"), - ("counts_evolve", c_bool, "Account for source evolution"), - ("line_phot_dipole", c_bool, "Dipole sources for 21cm"), - ("line_phot_quadrupole", c_bool, "Quadrupole sources for 21cm"), - ("line_basic", c_bool, "Include main 21cm monopole density/spin temperature sources"), - ("line_distortions", c_bool, "Redshift distortions for 21cm"), - ("line_extra", c_bool, "Include other sources"), - ("line_reionization", c_bool, "Replace the E modes with 21cm polarization"), - ("use_21cm_mK", c_bool, "Use mK units for 21cm"), - ) - - -class CustomSources(CAMB_Structure): - """ - Structure containing symbolic-compiled custom CMB angular power spectrum source functions. - Don't change this directly, instead call :meth:`.model.CAMBparams.set_custom_scalar_sources`. - """ - - _fields_ = ( - ("num_custom_sources", c_int, "number of sources set"), - ("c_source_func", c_void_p, "Don't directly change this"), - ("custom_source_ell_scales", AllocatableArrayInt, "scaling in L for outputs"), - ) - - -@fortran_class -class CAMBparams(F2003Class): - """ - Object storing the parameters for a CAMB calculation, including cosmological parameters and - settings for what to calculate. When a new object is instantiated, default parameters are set automatically. - - To add a new parameter, add it to the CAMBparams type in model.f90, then edit the _fields_ list in the CAMBparams - class in model.py to add the new parameter in the corresponding location of the member list. After rebuilding the - python version you can then access the parameter by using params.new_parameter_name where params is a CAMBparams - instance. You could also modify the wrapper functions to set the field value less directly. - - You can view the set of underlying parameters used by the Fortran code by printing the CAMBparams instance. - In python, to set cosmology parameters it is usually best to use :meth:`set_cosmology` and - equivalent methods for most other parameters. Alternatively the convenience function :func:`.camb.set_params` - can construct a complete instance from a dictionary of relevant parameters. - You can also save and restore a CAMBparams instance using the repr and eval functions, or pickle it. - - """ - - _fields_ = ( - ("WantCls", c_bool, "Calculate C_L"), - ("WantTransfer", c_bool, "Calculate matter transfer functions and matter power spectrum"), - ("WantScalars", c_bool, "Calculates scalar modes"), - ("WantTensors", c_bool, "Calculate tensor modes"), - ("WantVectors", c_bool, "Calculate vector modes"), - ("WantDerivedParameters", c_bool, "Calculate derived parameters"), - ("Want_cl_2D_array", c_bool, "For the C_L, include NxN matrix of all possible cross-spectra between sources"), - ("Want_CMB", c_bool, "Calculate the temperature and polarization power spectra"), - ("Want_CMB_lensing", c_bool, "Calculate the lensing potential power spectrum"), - ("DoLensing", c_bool, "Include CMB lensing"), - ("NonLinear", c_int, {"names": NonLinear_names}), - ("Transfer", TransferParams), - ("want_zstar", c_bool), - ("want_zdrag", c_bool), - ("min_l", c_int, "l_min for the scalar C_L (1 or 2, L=1 dipoles are Newtonian Gauge)"), - ("max_l", c_int, "l_max for the scalar C_L"), - ("max_l_tensor", c_int, "l_max for the tensor C_L"), - ("max_eta_k", c_double, "Maximum k*eta_0 for scalar C_L, where eta_0 is the conformal time today"), - ("max_eta_k_tensor", c_double, "Maximum k*eta_0 for tensor C_L, where eta_0 is the conformal time today"), - ("ombh2", c_double, "Omega_baryon h^2"), - ("omch2", c_double, "Omega_cdm h^2"), - ("omk", c_double, "Omega_K"), - ("omnuh2", c_double, "Omega_massive_neutrino h^2"), - ("H0", c_double, "Hubble parameter is km/s/Mpc units"), - ("TCMB", c_double, "CMB temperature today in Kelvin"), - ("YHe", c_double, "Helium mass fraction"), - ("num_nu_massless", c_double, "Effective number of massless neutrinos"), - ("num_nu_massive", c_int, "Total physical (integer) number of massive neutrino species"), - ("nu_mass_eigenstates", c_int, "Number of non-degenerate mass eigenstates"), - ( - "share_delta_neff", - c_bool, - "Share the non-integer part of num_nu_massless between the eigenstates. " - "This is not needed or used in the python interface.", - ), - ( - "nu_mass_degeneracies", - c_double * max_nu, - {"size": "nu_mass_eigenstates"}, - "Degeneracy of each distinct eigenstate", - ), - ( - "nu_mass_fractions", - c_double * max_nu, - {"size": "nu_mass_eigenstates"}, - "Mass fraction in each distinct eigenstate", - ), - ( - "nu_mass_numbers", - c_int * max_nu, - {"size": "nu_mass_eigenstates"}, - "Number of physical neutrinos per distinct eigenstate", - ), - ("InitPower", AllocatableObject(InitialPower)), - ("Recomb", AllocatableObject(recomb.RecombinationModel)), - ("Reion", AllocatableObject(reion.ReionizationModel)), - ("DarkEnergy", AllocatableObject(DarkEnergyModel)), - ("NonLinearModel", AllocatableObject(NonLinearModel)), - ("Accuracy", AccuracyParams), - ("SourceTerms", SourceTermParams), - ("z_outputs", AllocatableArrayDouble, "redshifts to always calculate BAO output parameters"), - ( - "scalar_initial_condition", - c_int, - { - "names": [ - "initial_vector", - "initial_adiabatic", - "initial_iso_CDM", - "initial_iso_baryon", - "initial_iso_neutrino", - "initial_iso_neutrino_vel", - ] - }, - ), - ( - "InitialConditionVector", - AllocatableArrayDouble, - "if scalar_initial_condition is initial_vector, the vector of initial condition amplitudes", - ), - ("OutputNormalization", c_int, "If non-zero, multipole to normalize the C_L at"), - ("Alens", c_double, "non-physical scaling amplitude for the CMB lensing spectrum power"), - ("MassiveNuMethod", c_int, {"names": ["Nu_int", "Nu_trunc", "Nu_approx", "Nu_best"]}), - ( - "DoLateRadTruncation", - c_bool, - "If true, use smooth approx to radiation perturbations after decoupling on small" - " scales, saving evolution of irrelevant oscillatory multipole equations", - ), - ( - "Evolve_baryon_cs", - c_bool, - "Evolve a separate equation for the baryon sound speed rather than using background approximation", - ), - ("Evolve_delta_xe", c_bool, "Evolve ionization fraction perturbations"), - ("Evolve_delta_Ts", c_bool, "Evolve the spin temperature perturbation (for 21cm)"), - ("Do21cm", c_bool, "21cm is not yet implemented via the python wrapper"), - ("transfer_21cm_cl", c_bool, "Get 21cm C_L at a given fixed redshift"), - ("Log_lvalues", c_bool, "Use log spacing for sampling in L"), - ( - "use_cl_spline_template", - c_bool, - "When interpolating use a fiducial spectrum shape to define ratio to spline", - ), - ("min_l_logl_sampling", c_int, "Minimum L to use log sampling for L"), - ("SourceWindows", AllocatableObjectArray(SourceWindow)), - ("CustomSources", CustomSources), - ) - - H0: float - SourceWindows: list[SourceWindow] - - _fortran_class_module_ = "model" - - _methods_ = ( - ( - "SetNeutrinoHierarchy", - [POINTER(c_double), POINTER(c_double), POINTER(c_double), POINTER(c_int), POINTER(c_int)], - ), - ("Validate", None, c_int), - ("PrimordialPower", [numpy_1d, numpy_1d, POINTER(c_int), POINTER(c_int)]), - ("SetCustomSourcesFunc", [POINTER(c_int), POINTER(ctypes.c_void_p), numpy_1d_int]), - ) - - def __init__(self, **kwargs): - set_default_params(self) - self.InitPower.set_params() - super().__init__(**kwargs) - - def validate(self): - """ - Do some quick tests for sanity - - :return: True if OK - """ - return self.f_Validate() != 0 - - def write_ini(self, ini_filename, validate=True): - """ - Write the current parameters to a CAMB .ini file. - - :param ini_filename: path to the output .ini file - :param validate: whether to validate the written file - """ - return _ini.write_ini(self, ini_filename, validate=validate) - - def set_accuracy( - self, - AccuracyBoost=1.0, - lSampleBoost=1.0, - lAccuracyBoost=1.0, - DoLateRadTruncation=True, - min_l_logl_sampling=None, - ): - """ - Set parameters determining overall calculation accuracy (large values may give big slow down). - For finer control you can set individual accuracy parameters by changing CAMBParams.Accuracy - (:class:`.model.AccuracyParams`) . - - :param AccuracyBoost: increase AccuracyBoost to decrease integration step size, increase density of k - sampling, etc. - :param lSampleBoost: increase lSampleBoost to increase density of L sampling for CMB - :param lAccuracyBoost: increase lAccuracyBoost to increase the maximum L included in the Boltzmann hierarchies - :param DoLateRadTruncation: If True, use approximation to radiation perturbation evolution at late times - :param min_l_logl_sampling: at L>min_l_logl_sampling uses sparser log sampling for L interpolation; - increase above 5000 for better accuracy at L > 5000 - :return: self - """ - self.Accuracy.lSampleBoost = lSampleBoost - self.Accuracy.AccuracyBoost = AccuracyBoost - self.Accuracy.lAccuracyBoost = lAccuracyBoost - self.DoLateRadTruncation = DoLateRadTruncation - if min_l_logl_sampling: - self.min_l_logl_sampling = min_l_logl_sampling - return self - - def set_initial_power_function( - self, - P_scalar, - P_tensor=None, - kmin=1e-6, - kmax=100.0, - N_min=200, - rtol=5e-5, - effective_ns_for_nonlinear=None, - args=(), - ): - r""" - Set the initial power spectrum from a function P_scalar(k, \*args), and optionally also the tensor spectrum. - The function is called to make a pre-computed array which is then interpolated inside CAMB. The sampling in k - is set automatically so that the spline is accurate, but you may also need to increase other - accuracy parameters. - - :param P_scalar: function returning normalized initial scalar curvature power as function of k (in Mpc^{-1}) - :param P_tensor: optional function returning normalized initial tensor power spectrum - :param kmin: minimum wavenumber to compute - :param kmax: maximum wavenumber to compute - :param N_min: minimum number of spline points for the pre-computation - :param rtol: relative tolerance for deciding how many points are enough - :param effective_ns_for_nonlinear: an effective n_s for use with approximate non-linear corrections - :param args: optional list of arguments passed to P_scalar (and P_tensor) - :return: self - """ - - from scipy.interpolate import InterpolatedUnivariateSpline - - assert N_min > 7 - assert kmin < kmax - # sample function logspace, finely enough that it interpolates accurately - N = N_min - ktest = np.logspace(np.log10(kmin), np.log10(kmax), N // 2) - PK_test = P_scalar(ktest, *args) - while True: - ks = np.logspace(np.log10(kmin), np.log10(kmax), N) - PK_compare = InterpolatedUnivariateSpline(ktest, PK_test)(ks) - PK = P_scalar(ks, *args) - if np.allclose(PK, PK_compare, atol=np.max(PK) * 1e-6, rtol=rtol): - break - N *= 2 - PK_test = PK - ktest = ks - PK_t = None if P_tensor is None else P_tensor(ks, *args) - self.set_initial_power_table(ks, PK, PK_t, effective_ns_for_nonlinear) - return self - - def set_initial_power_table(self, k, pk=None, pk_tensor=None, effective_ns_for_nonlinear=None): - """ - Set a general initial power spectrum from tabulated values. It's up to you to ensure the sampling - of the k values is high enough that it can be interpolated accurately. - - :param k: array of k values (Mpc^{-1}) - :param pk: array of primordial curvature perturbation power spectrum values P(k_i) - :param pk_tensor: array of tensor spectrum values - :param effective_ns_for_nonlinear: an effective n_s for use with approximate non-linear corrections - """ - self.InitPower = SplinedInitialPower() - initpower = self.InitPower - if effective_ns_for_nonlinear is not None: - initpower.effective_ns_for_nonlinear = effective_ns_for_nonlinear - if pk is None: - pk = np.empty(0) - elif len(k) != len(pk): - raise CAMBValueError("k and P(k) arrays must be same size") - if pk_tensor is not None: - if len(k) != len(pk_tensor): - raise CAMBValueError("k and P_tensor(k) arrays must be same size") - initpower.set_tensor_table(k, pk_tensor) - initpower.set_scalar_table(k, pk) - return self - - def set_initial_power(self, initial_power_params): - """ - Set the InitialPower primordial power spectrum parameters - - :param initial_power_params: :class:`.initialpower.InitialPowerLaw` - or :class:`.initialpower.SplinedInitialPower` instance - :return: self - """ - self.InitPower = initial_power_params - return self - - def set_H0_for_theta( - self, theta, cosmomc_approx=False, theta_H0_range=(10, 100), est_H0=67.0, iteration_threshold=8, setter_H0=None - ): - r""" - Set H0 to give a specified value of the acoustic angular scale parameter theta. - - :param theta: value of :math:`r_s/D_M` at redshift :math:`z_\star` - :param cosmomc_approx: if true, use approximate fitting formula for :math:`z_\star`, - if false do full numerical calculation - :param theta_H0_range: min, max interval to search for H0 (in km/s/Mpc) - :param est_H0: an initial guess for H0 in km/s/Mpc, used in the case cosmomc_approx=False. - :param iteration_threshold: difference in H0 from est_H0 for which to iterate, - used for cosmomc_approx=False to correct for small changes in zstar when H0 changes - :param setter_H0: if specified, a function to call to set H0 for each iteration to find thetstar. It should be - a function(pars: CAMBParams, H0: float). Not normally needed, but can be used e.g. when DE model needs to be - changed for each H0 because it depends explicitly on e.g. Omega_m. - """ - - if not (0.001 < theta < 0.1): - raise CAMBParamRangeError("theta looks wrong (parameter is just theta, not 100*theta)") - - try: - from scipy.optimize import brentq - except ImportError: - raise CAMBError("You need SciPy to set cosmomc_theta.") - - from . import camb - - if setter_H0: - _set_H0 = setter_H0 - else: - - def _set_H0(params, H0): - params.H0 = H0 - - data = camb.CAMBdata() - if not cosmomc_approx: - zstar = c_double() - _set_H0(self, est_H0) - data.calc_background_no_thermo(self) - # get_zstar initializes the recombination model - zstar = data.f_get_zstar(byref(zstar)) - - def f(H0): - _set_H0(self, H0) - data.calc_background_no_thermo(self) - if cosmomc_approx: - theta_test = data.cosmomc_theta() - else: - rs = data.sound_horizon(zstar) - theta_test = rs / (data.angular_diameter_distance(zstar) * (1 + zstar)) - return theta_test - theta - - try: - # noinspection PyTypeChecker - self.H0 = brentq(f, theta_H0_range[0], theta_H0_range[1], rtol=5e-5) # type: ignore - if not cosmomc_approx and abs(self.H0 - est_H0) > iteration_threshold: - # iterate with recalculation of recombination and zstar - self.set_H0_for_theta( - theta, - theta_H0_range=theta_H0_range, - est_H0=self.H0, - iteration_threshold=iteration_threshold, - setter_H0=setter_H0, - ) - except ValueError: - raise CAMBParamRangeError("No solution for H0 inside of theta_H0_range") - - def set_cosmology( - self, - H0: float | None = None, - ombh2=0.022, - omch2=0.12, - omk=0.0, - cosmomc_theta: float | None = None, - thetastar: float | None = None, - neutrino_hierarchy: str | int = "degenerate", - num_massive_neutrinos=1, - mnu=0.06, - nnu=constants.default_nnu, - YHe: float | None = None, - meffsterile=0.0, - standard_neutrino_neff=constants.default_nnu, - TCMB=constants.COBE_CMBTemp, - tau: float | None = None, - zrei: float | None = None, - Alens=1.0, - bbn_predictor: None | str | bbn.BBNPredictor = None, - theta_H0_range=(10, 100), - setter_H0=None, - ): - r""" - Sets cosmological parameters in terms of physical densities and parameters (e.g. as used in Planck analyses). - Default settings give a single distinct neutrino mass eigenstate, by default one neutrino with mnu = 0.06eV. - Set the neutrino_hierarchy parameter to normal or inverted to use a two-eigenstate model that is a good - approximation to the known mass splittings seen in oscillation measurements. - For more fine-grained control can set the neutrino parameters directly rather than using this function. - - Instead of setting the Hubble parameter directly, you can instead set the acoustic scale parameter - (cosmomc_theta, which is based on a fitting formula for simple models, or thetastar, which is numerically - calculated more generally). Note that you must have already set the dark energy model, you can't use - set_cosmology with theta and then change the background evolution (which would change theta at the calculated - H0 value). Likewise, the dark energy model cannot depend explicitly on H0 unless you provide a custom - setter_H0 function to update the model for each H0 iteration used to search for thetastar. - - If in doubt, print CAMBparams after setting parameters to see the underlying values that have been set. - - :param H0: Hubble parameter today in km/s/Mpc. Can leave unset and instead set thetastar or cosmomc_theta - (which solves for the required H0). - :param ombh2: physical density in baryons - :param omch2: physical density in cold dark matter - :param omk: Omega_K curvature parameter - :param cosmomc_theta: The approximate CosmoMC theta parameter :math:`\theta_{\rm MC}`. The angular - diameter distance is calculated numerically, but the redshift :math:`z_\star` - is calculated using an approximate (quite accurate but non-general) fitting formula. - Leave unset to use H0 or thetastar. - :param thetastar: The angular acoustic scale parameter :math:`\theta_\star = r_s(z_*)/D_M(z_*)`, defined as - the ratio of the photon-baryon sound horizon :math:`r_s` to the angular diameter - distance :math:`D_M`, where both quantities are evaluated at :math:`z_*`, the redshift at - which the optical depth (excluding reionization) is unity. Leave unset to use H0 or cosmomc_theta. - :param neutrino_hierarchy: 'degenerate', 'normal', or 'inverted' (1 or 2 eigenstate approximation) - :param num_massive_neutrinos: number of massive neutrinos. If meffsterile is set, this is the number of - massive active neutrinos. - :param mnu: sum of neutrino masses (in eV). Omega_nu is calculated approximately from this assuming neutrinos - non-relativistic today; i.e. here is defined as a direct proxy for Omega_nu. Internally the actual - physical mass is calculated from the Omega_nu accounting for small mass-dependent velocity corrections - but neglecting spectral distortions to the neutrino distribution. - Set the neutrino field values directly if you need finer control or more complex neutrino models. - :param nnu: N_eff, effective relativistic degrees of freedom - :param YHe: Helium mass fraction. If None, set from BBN consistency. - :param meffsterile: effective mass of sterile neutrinos (set along with nnu greater than the standard value). - Defined as in the Planck papers. You do not need to also change num_massive_neutrinos. - :param standard_neutrino_neff: default value for N_eff in standard cosmology (non-integer to allow for partial - heating of neutrinos at electron-positron annihilation and QED effects) - :param TCMB: CMB temperature (in Kelvin) - :param tau: optical depth; if None and zrei is None, current Reion settings are not changed - :param zrei: reionization mid-point optical depth (set tau=None to use this) - :param Alens: (non-physical) scaling of the lensing potential compared to prediction - :param bbn_predictor: :class:`.bbn.BBNPredictor` instance used to get YHe from BBN consistency if YHe is None, - or name of a BBN predictor class, or file name of an interpolation table - :param theta_H0_range: if thetastar or cosmomc_theta is specified, the min, max interval of H0 values to map to; - if H0 is outside this range it will raise an exception. - :param setter_H0: if specified, a function to call to set H0 for each iteration to find thetastar. It should be - a function(pars: CAMBParams, H0: float). Not normally needed, but can be used e.g. when DE model needs to be - changed for each H0 because it depends explicitly on H0 - """ - - if YHe is None: - # use BBN prediction - if isinstance(bbn_predictor, str): - self.bbn_predictor = bbn.get_predictor(bbn_predictor) - else: - self.bbn_predictor = bbn_predictor or bbn.get_predictor() - YHe = self.bbn_predictor.Y_He(ombh2 * (constants.COBE_CMBTemp / TCMB) ** 3, nnu - standard_neutrino_neff) - self.YHe = YHe - self.TCMB = TCMB - self.ombh2 = ombh2 - self.omch2 = omch2 - self.Alens = Alens - - neutrino_mass_fac = constants.neutrino_mass_fac * (constants.COBE_CMBTemp / TCMB) ** 3 - - if not isinstance(neutrino_hierarchy, str): - neutrino_hierarchy = neutrino_hierarchies[neutrino_hierarchy - 1] - - if nnu >= standard_neutrino_neff or neutrino_hierarchy != neutrino_hierarchy_degenerate: - omnuh2 = mnu / neutrino_mass_fac * (standard_neutrino_neff / 3) ** 0.75 - else: - omnuh2 = mnu / neutrino_mass_fac * (nnu / 3.0) ** 0.75 - omnuh2_sterile = meffsterile / neutrino_mass_fac - if omnuh2_sterile > 0 and nnu < standard_neutrino_neff: - raise CAMBError(f"sterile neutrino mass required Neff> {constants.default_nnu:.3g}") - if omnuh2 and not num_massive_neutrinos: - raise CAMBError("non-zero mnu with zero num_massive_neutrinos") - - omnuh2 = omnuh2 + omnuh2_sterile - self.omnuh2 = omnuh2 - self.omk = omk - assert num_massive_neutrinos == int(num_massive_neutrinos) - self.f_SetNeutrinoHierarchy( - byref(c_double(omnuh2)), - byref(c_double(omnuh2_sterile)), - byref(c_double(nnu)), - byref(c_int(neutrino_hierarchies.index(neutrino_hierarchy) + 1)), - byref(c_int(int(num_massive_neutrinos))), - ) - - if cosmomc_theta or thetastar: - if H0 is not None: - raise CAMBError("Set H0=None when setting theta.") - if cosmomc_theta and thetastar: - raise CAMBError("Cannot set both cosmomc_theta and thetastar") - - self.set_H0_for_theta( - cosmomc_theta or thetastar, - cosmomc_approx=cosmomc_theta is not None, - theta_H0_range=theta_H0_range, - setter_H0=setter_H0, - ) - else: - if H0 is None: - raise CAMBError("Must set H0, cosmomc_theta or thetastar") - if H0 < 1: - raise CAMBValueError("H0 is the value in km/s/Mpc, your value looks very small") - self.H0 = H0 - - if tau is not None: - if zrei is not None: - raise CAMBError("Cannot set both tau and zrei") - self.Reion.set_tau(tau) - elif zrei is not None: - self.Reion.set_zrei(zrei) - - return self - - @property - def h(self): - return self.H0 / 100 - - @h.setter - def h(self, value): - self.H0 = value * 100 - - @property - def omegab(self): - return self.ombh2 / (self.H0 / 100) ** 2 - - @property - def omegac(self): - return self.omch2 / (self.H0 / 100) ** 2 - - @property - def omeganu(self): - return self.omnuh2 / (self.H0 / 100) ** 2 - - @property - def omegam(self): - return (self.ombh2 + self.omch2 + self.omnuh2) / (self.H0 / 100) ** 2 - - @property - def N_eff(self): - """ - :return: Effective number of degrees of freedom in relativistic species at early times. - """ - if self.share_delta_neff: - return self.num_nu_massless + self.num_nu_massive - else: - return sum(self.nu_mass_degeneracies[: self.nu_mass_eigenstates]) + self.num_nu_massless - - @property - def lmax(self): - return self.max_l - - def set_classes( - self, - dark_energy_model=None, - initial_power_model=None, - non_linear_model=None, - recombination_model=None, - reionization_model=None, - ): - """ - Change the classes used to implement parts of the model. - - :param dark_energy_model: 'fluid', 'ppf', or name of a DarkEnergyModel class - :param initial_power_model: name of an InitialPower class - :param non_linear_model: name of a NonLinearModel class - :param recombination_model: name of RecombinationModel class - :param reionization_model: name of a ReionizationModel class - """ - if dark_energy_model: - self.DarkEnergy = self.make_class_named(dark_energy_model, DarkEnergyModel) - if initial_power_model: - self.InitPower = self.make_class_named(initial_power_model, InitialPower) - if non_linear_model: - self.NonLinearModel = self.make_class_named(non_linear_model, NonLinearModel) - if recombination_model: - self.Recomb = self.make_class_named(recombination_model, RecombinationModel) - if reionization_model: - self.Reion = self.make_class_named(reionization_model, ReionizationModel) - - def set_dark_energy( - self, - w=-1.0, - cs2=1.0, - wa=0, - use_tabulated_w=False, - wde_a_array=None, - wde_w_array=None, - dark_energy_model="fluid", - ): - r""" - Set dark energy parameters (use set_dark_energy_w_a to set w(a) from numerical table instead) - To use a custom dark energy model, assign the class instance to the DarkEnergy field instead. - - :param w: :math:`w\equiv p_{\rm de}/\rho_{\rm de}`, assumed constant - :param wa: evolution of w (for dark_energy_model=ppf) - :param cs2: rest-frame sound speed squared of dark energy fluid - :param use_tabulated_w: whether use interpolated w - :param wde_a_array: array of scale factors - :param wde_w_array: array of w(a) - :param dark_energy_model: model to use ('fluid' or 'ppf'), default is 'fluid' - :return: self - """ - - de = self.make_class_named(dark_energy_model, DarkEnergyEqnOfState) - de.set_params( - w=w, wa=wa, cs2=cs2, use_tabulated_w=use_tabulated_w, wde_a_array=wde_a_array, wde_w_array=wde_w_array - ) - self.DarkEnergy = de - return self - - def set_dark_energy_w_a(self, a, w, dark_energy_model="fluid"): - """ - Set the dark energy equation of state from tabulated values (which are cubic spline interpolated). - - :param a: array of sampled a = 1/(1+z) values - :param w: array of w(a) - :param dark_energy_model: model to use ('fluid' or 'ppf'), default is 'fluid' - :return: self - """ - if dark_energy_model == "fluid" and np.any(w < -1): - raise CAMBError("fluid dark energy model does not support w crossing -1") - self.DarkEnergy = self.make_class_named(dark_energy_model, DarkEnergyEqnOfState) - # Note that assigning to allocatable fields makes deep copies of the object - self.DarkEnergy.set_w_a_table(a, w) - return self - - def get_zre(self): - return self.Reion.get_zre(self) - - # alias consistent with input parameter name - get_zrei = get_zre - - def get_Y_p(self, ombh2=None, delta_neff=None): - r""" - Get BBN helium nucleon fraction (NOT the same as the mass fraction Y_He) by interpolation using the - :class:`.bbn.BBNPredictor` instance passed to :meth:`set_cosmology` - (or the default one, if `Y_He` has not been set). - - :param ombh2: :math:`\Omega_b h^2` (default: value passed to :meth:`set_cosmology`) - :param delta_neff: additional :math:`N_{\rm eff}` relative to standard value (of 3.044) - (default: from values passed to :meth:`set_cosmology`) - :return: :math:`Y_p^{\rm BBN}` helium nucleon fraction predicted by BBN. - """ - try: - ombh2 = ombh2 if ombh2 is not None else self.ombh2 - delta_neff = delta_neff if delta_neff is not None else self.N_eff - constants.default_nnu - return self.bbn_predictor.Y_p(ombh2, delta_neff) - except AttributeError: - raise CAMBError("Not able to compute Y_p: not using an interpolation table for BBN abundances.") - - def get_DH(self, ombh2=None, delta_neff=None): - r""" - Get deuterium ration D/H by interpolation using the - :class:`.bbn.BBNPredictor` instance passed to :meth:`set_cosmology` - (or the default one, if `Y_He` has not been set). - - :param ombh2: :math:`\Omega_b h^2` (default: value passed to :meth:`set_cosmology`) - :param delta_neff: additional :math:`N_{\rm eff}` relative to standard value (of 3.044) - (default: from values passed to :meth:`set_cosmology`) - :return: BBN helium nucleon fraction D/H - """ - try: - ombh2 = ombh2 if ombh2 is not None else self.ombh2 - delta_neff = delta_neff if delta_neff is not None else self.N_eff - constants.default_nnu - return self.bbn_predictor.DH(ombh2, delta_neff) - except AttributeError: - raise CAMBError("Not able to compute DH: not using an interpolation table for BBN abundances.") - - def set_matter_power( - self, - redshifts=(0.0,), - kmax=1.2, - k_per_logint=None, - nonlinear=None, - accurate_massive_neutrino_transfers=False, - silent=False, - ): - """ - Set parameters for calculating matter power spectra and transfer functions. - - :param redshifts: array of redshifts to calculate - :param kmax: maximum k to calculate (where k is just k, not k/h) - :param k_per_logint: minimum number of k steps per log k. Set to zero to use default optimized spacing. - :param nonlinear: if None, uses existing setting, otherwise boolean for whether to use non-linear matter power. - :param accurate_massive_neutrino_transfers: if you want the massive neutrino transfers accurately - :param silent: if True, don't give warnings about sort order - :return: self - """ - if not len(redshifts): - raise CAMBError("set_matter_power redshifts list is empty") - - self.WantTransfer = True - self.Transfer.high_precision = True - self.Transfer.accurate_massive_neutrinos = accurate_massive_neutrino_transfers - self.Transfer.kmax = kmax - zs = sorted(redshifts, reverse=True) - if nonlinear is not None: - if nonlinear: - if self.NonLinear in [NonLinear_lens, NonLinear_both]: - self.NonLinear = NonLinear_both - else: - self.NonLinear = NonLinear_pk - if not silent and (kmax < 5 or kmax < 20 and np.max(zs) > 4): - logger.warning(f"Using kmax={kmax} with Halofit non-linear models may give inaccurate results") - else: - if self.NonLinear in [NonLinear_lens, NonLinear_both]: - self.NonLinear = NonLinear_lens - else: - self.NonLinear = NonLinear_none - self.Transfer.k_per_logint = k_per_logint if k_per_logint else 0 - if not silent and np.any(np.array(zs) - np.array(redshifts) != 0): - print("Note: redshifts have been re-sorted (earliest first)") - if len(redshifts) > max_transfer_redshifts: - raise CAMBError(f"You can have at most {max_transfer_redshifts} redshifts") - self.Transfer.PK_redshifts = zs - return self - - def set_nonlinear_lensing(self, nonlinear): - """ - Settings for whether or not to use non-linear corrections for the CMB lensing potential. - Note that set_for_lmax also sets lensing to be non-linear if lens_potential_accuracy>0 - - :param nonlinear: true to use non-linear corrections - """ - if nonlinear: - if self.NonLinear in [NonLinear_pk, NonLinear_both]: - self.NonLinear = NonLinear_both - else: - self.NonLinear = NonLinear_lens - else: - if self.NonLinear in [NonLinear_pk, NonLinear_both]: - self.NonLinear = NonLinear_pk - else: - self.NonLinear = NonLinear_none - - def set_for_lmax( - self, - lmax, - max_eta_k=None, - lens_potential_accuracy=0, - lens_margin=150, - k_eta_fac=2.5, - lens_k_eta_reference=18000.0, - nonlinear=None, - ): - r""" - Set parameters to get CMB power spectra accurate to specific a l_lmax. - Note this does not fix the actual output L range, spectra may be calculated above l_max - (but may not be accurate there). To fix the l_max for output arrays use the optional input argument - to :meth:`.results.CAMBdata.get_cmb_power_spectra` etc. - - :param lmax: :math:`\ell_{\rm max}` you want - :param max_eta_k: maximum value of :math:`k \eta_0\approx k\chi_*` to use, which indirectly sets k_max. - If None, sensible value set automatically. - :param lens_potential_accuracy: Set to 1 or higher if you want to get the lensing potential accurate - (1 is only Planck-level accuracy) - :param lens_margin: the :math:`\Delta \ell_{\rm max}` to use to ensure lensed :math:`C_\ell` are correct - at :math:`\ell_{\rm max}` - :param k_eta_fac: k_eta_fac default factor for setting max_eta_k = k_eta_fac*lmax if max_eta_k=None - :param lens_k_eta_reference: value of max_eta_k to use when lens_potential_accuracy>0; use - k_eta_max = lens_k_eta_reference*lens_potential_accuracy - :param nonlinear: use non-linear power spectrum; if None, sets nonlinear if lens_potential_accuracy>0 otherwise - preserves current setting - :return: self - """ - if self.DoLensing: - self.max_l = lmax + lens_margin - else: - self.max_l = lmax - self.max_eta_k = max_eta_k or self.max_l * k_eta_fac - if lens_potential_accuracy: - self.set_nonlinear_lensing(nonlinear is not False) - self.max_eta_k = max(self.max_eta_k, lens_k_eta_reference * lens_potential_accuracy) - elif nonlinear is not None: - self.set_nonlinear_lensing(nonlinear) - return self - - @overload - def scalar_power(self, k: float) -> float: ... - - @overload - def scalar_power(self, k: Array1D) -> np.ndarray: ... - - def scalar_power(self, k): - r""" - Get the primordial scalar curvature power spectrum at :math:`k` - - :param k: wavenumber :math:`k` (in :math:`{\rm Mpc}^{-1}` units) - :return: power spectrum at :math:`k` - """ - return self.primordial_power(k, 0) - - @overload - def tensor_power(self, k: float) -> float: ... - - @overload - def tensor_power(self, k: Array1D) -> np.ndarray: ... - - def tensor_power(self, k): - r""" - Get the primordial tensor curvature power spectrum at :math:`k` - - :param k: wavenumber :math:`k` (in :math:`{\rm Mpc}^{-1}` units) - :return: tensor power spectrum at :math:`k` - """ - - return self.primordial_power(k, 2) - - def primordial_power(self, k, ix): - karr = np.ascontiguousarray([k] if np.isscalar(k) else k, dtype=np.float64) - n = karr.shape[0] - powers = np.empty(n) - self.f_PrimordialPower(karr, powers, byref(c_int(n)), byref(c_int(ix))) - if np.isscalar(k): - return powers[0] - else: - return powers - - _custom_source_name_dict: ClassVar[dict] = {} - - def set_custom_scalar_sources( - self, custom_sources, source_names=None, source_ell_scales=None, frame="CDM", code_path=None - ): - r""" - Set custom sources for angular power spectrum using camb.symbolic sympy expressions. - - :param custom_sources: list of sympy expressions for the angular power spectrum sources - :param source_names: optional list of string names for the sources - :param source_ell_scales: list or dictionary of scalings for each source name, where for integer entry n, - the source for multipole :math:`\ell` is scaled by :math:`\sqrt{(\ell+n)!/(\ell-n)!}`, - i.e. :math:`n=2` for a new polarization-like source. - :param frame: if the source is not gauge invariant, frame in which to interpret result - :param code_path: optional path for output of source code for CAMB f90 source function - """ - - from . import symbolic - - if isinstance(custom_sources, dict): - assert not source_names - if source_ell_scales and not isinstance(source_ell_scales, dict): - raise CAMBValueError("source_ell_scales must be a dictionary if custom_sources is") - lst = [] - source_names = [] - for name in custom_sources: - source_names.append(name) - lst.append(custom_sources[name]) - custom_sources = lst - elif not isinstance(custom_sources, (list, tuple)): - custom_sources = [custom_sources] - if source_names: - source_names = [source_names] - custom_source_names = source_names or ["C%s" % (i + 1) for i in range(len(custom_sources))] - if len(custom_source_names) != len(custom_sources): - raise CAMBValueError("Number of custom source names does not match number of sources") - scales = np.zeros(len(custom_sources), dtype=np.int32) - if source_ell_scales: - if isinstance(source_ell_scales, dict): - if set(source_ell_scales.keys()) - set(custom_source_names): - raise CAMBValueError("scale dict key not in source names list") - for i, name in enumerate(custom_source_names): - if name in source_ell_scales: - scales[i] = source_ell_scales[name] - else: - scales[:] = source_ell_scales - - _current_source_func = symbolic.compile_sympy_to_camb_source_func( - custom_sources, frame=frame, code_path=code_path - ) - - custom_source_func = ctypes.cast(_current_source_func, c_void_p) - self._custom_source_name_dict[custom_source_func.value] = custom_source_names - self.f_SetCustomSourcesFunc(byref(c_int(len(custom_sources))), byref(custom_source_func), scales) - - def get_custom_source_names(self): - if self.CustomSources.num_custom_sources: - return self._custom_source_name_dict[self.CustomSources.c_source_func] - else: - return [] - - def clear_custom_scalar_sources(self): - self.f_SetCustomSourcesFunc(byref(c_int(0)), byref(ctypes.c_void_p(0)), np.zeros(0, dtype=np.int32)) - - def diff(self, params): - """ - Print differences between this set of parameters and params - - :param params: another CAMBparams instance - """ - p1 = str(params) - p2 = str(self) - for line1, line2 in zip(p1.split("\n"), p2.split("\n")): - if line1 != line2: - print(line1, " <-> ", line2) - - -def set_default_params(P): - """ - Set default values for all parameters - - :param P: :class:`.model.CAMBparams` - :return: P - """ - assert isinstance(P, CAMBparams) - camblib.__camb_MOD_camb_setdefparams(byref(P)) - return P +from __future__ import annotations + +import ctypes +import logging +from ctypes import POINTER, byref, c_bool, c_double, c_int, c_void_p +from typing import ClassVar, overload + +from . import _ini, bbn, constants +from . import recombination as recomb +from . import reionization as reion +from .baseconfig import ( + AllocatableArrayDouble, + AllocatableArrayInt, + AllocatableObject, + AllocatableObjectArray, + Array1D, + CAMB_Structure, + CAMBError, + CAMBParamRangeError, + CAMBValueError, + F2003Class, + camblib, + fortran_class, + np, + numpy_1d, + numpy_1d_int, +) +from .dark_energy import DarkEnergyEqnOfState, DarkEnergyModel +from .initialpower import InitialPower, SplinedInitialPower +from .nonlinear import NonLinearModel +from .recombination import RecombinationModel +from .reionization import ReionizationModel +from .sources import SourceWindow + +logger = logging.getLogger(__name__) + +# Union and Optional types are now built-in to Python 3.10+ + +max_nu = 5 +max_transfer_redshifts = 256 +nthermo_derived = 13 +Transfer_kh = 1 +Transfer_cdm = 2 +Transfer_b = 3 +Transfer_g = 4 +Transfer_r = 5 +Transfer_nu = 6 +Transfer_tot = 7 +Transfer_nonu = 8 +Transfer_tot_de = 9 +Transfer_Weyl = 10 +Transfer_Newt_vel_cdm = 11 +Transfer_Newt_vel_baryon = 12 +Transfer_vel_baryon_cdm = 13 +Transfer_max = Transfer_vel_baryon_cdm + +# for 21cm case +Transfer_monopole = 4 +Transfer_vnewt = 5 +Transfer_Tmat = 6 + +NonLinear_none = "NonLinear_none" +NonLinear_pk = "NonLinear_pk" +NonLinear_lens = "NonLinear_lens" +NonLinear_both = "NonLinear_both" +NonLinear_names = [NonLinear_none, NonLinear_pk, NonLinear_lens, NonLinear_both] + +derived_names = [ + "age", + "zstar", + "rstar", + "thetastar", + "DAstar", + "zdrag", + "rdrag", + "kd", + "thetad", + "zeq", + "keq", + "thetaeq", + "thetarseq", +] + +transfer_names = [ + "k/h", + "delta_cdm", + "delta_baryon", + "delta_photon", + "delta_neutrino", + "delta_nu", + "delta_tot", + "delta_nonu", + "delta_tot_de", + "Weyl", + "v_newtonian_cdm", + "v_newtonian_baryon", + "v_baryon_cdm", +] + +evolve_names = transfer_names + [ + "a", + "etak", + "H", + "growth", + "v_photon", + "pi_photon", + "E_2", + "v_neutrino", + "T_source", + "E_source", + "lens_potential_source", +] + +background_names = [ + "x_e", + "opacity", + "visibility", + "cs2b", + "T_b", + "dopacity", + "ddopacity", + "dvisibility", + "ddvisibility", +] +density_names = ["tot", "K", "cdm", "baryon", "photon", "neutrino", "nu", "de"] + +neutrino_hierarchy_normal = "normal" +neutrino_hierarchy_inverted = "inverted" +neutrino_hierarchy_degenerate = "degenerate" +neutrino_hierarchies = [neutrino_hierarchy_normal, neutrino_hierarchy_inverted, neutrino_hierarchy_degenerate] + + +class TransferParams(CAMB_Structure): + """ + Object storing parameters for the matter power spectrum calculation. + + Not intended to be separately instantiated, only used as part of CAMBparams. + """ + + _fields_ = ( + ("high_precision", c_bool, "True for more accuracy"), + ( + "accurate_massive_neutrinos", + c_bool, + "True if you want neutrino transfer functions accurate (false by default)", + ), + ("kmax", c_double, "k_max to output (no h in units)"), + ("k_per_logint", c_int, "number of points per log k interval. If zero, set an irregular optimized spacing"), + ("PK_num_redshifts", c_int, "number of redshifts to calculate"), + ( + "PK_redshifts", + c_double * max_transfer_redshifts, + {"size": "PK_num_redshifts"}, + "redshifts to output for the matter transfer and power", + ), + ) + + +class AccuracyParams(CAMB_Structure): + """ + Structure with parameters governing numerical accuracy. AccuracyBoost will also scale almost all the other + parameters except for lSampleBoost (which is specific to the output interpolation) and lAccuracyBoost + (which is specific to the multipole hierarchy evolution), e.g. setting AccuracyBoost=2, IntTolBoost=1.5, means + that internally the k sampling for integration will be boosted by AccuracyBoost*IntTolBoost = 3. + + Not intended to be separately instantiated, only used as part of CAMBparams. + If you want to set fields with :func:`.camb.set_params`, use 'Accuracy.xxx':yyy in the parameter dictionary. + """ + + _fields_ = ( + ( + "AccuracyBoost", + c_double, + "general accuracy setting effecting everything related to step sizes etc. " + "(including separate settings below except the next two)", + ), + ( + "lSampleBoost", + c_double, + "accuracy for sampling in ell for interpolation for the C_l (if >=50, all ell are calculated)", + ), + ("lAccuracyBoost", c_double, "Boosts number of multipoles integrated in Boltzmann hierarchy"), + ("AccuratePolarization", c_bool, "Do you care about the accuracy of the polarization Cls?"), + ("AccurateBB", c_bool, "Do you care about BB accuracy (e.g. in lensing)"), + ("AccurateReionization", c_bool, "Do you care about percent level accuracy on EE signal from reionization?"), + ("TimeStepBoost", c_double, "Sampling time steps"), + ( + "BackgroundTimeStepBoost", + c_double, + "Number of time steps for background thermal history and source window interpolation", + ), + ( + "TimeSwitchBoost", + c_double, + "Accuracy for limit/domination approximation switches", + ), + ("IntTolBoost", c_double, "Tolerances for integrating differential equations"), + ("SourcekAccuracyBoost", c_double, "Accuracy of k sampling for source time integration"), + ("IntkAccuracyBoost", c_double, "Accuracy of k sampling for integration"), + ("TransferkBoost", c_double, "Accuracy of k sampling for transfer functions"), + ("NonFlatIntAccuracyBoost", c_double, "Accuracy of non-flat time integration"), + ("BessIntBoost", c_double, "Accuracy of bessel integration truncation"), + ("LensingBoost", c_double, "Accuracy of the lensing of CMB power spectra"), + ("NonlinSourceBoost", c_double, "Accuracy of steps and kmax used for the non-linear correction"), + ("BesselBoost", c_double, "Accuracy of bessel pre-computation sampling"), + ("LimberBoost", c_double, "Accuracy of Limber approximation use"), + ("SourceLimberBoost", c_double, "Scales when to switch to Limber for source windows"), + ("KmaxBoost", c_double, "Boost max k for source window functions"), + ("neutrino_q_boost", c_double, "Number of momenta integrated for neutrino perturbations"), + ) + + +class SourceTermParams(CAMB_Structure): + """ + Structure with parameters determining how galaxy/lensing/21cm power spectra and transfer functions are calculated. + + Not intended to be separately instantiated, only used as part of CAMBparams. + """ + + _fields_ = ( + ( + "limber_windows", + c_bool, + "Use Limber approximation where appropriate. CMB lensing uses Limber even if limber_window is false, " + + "but method is changed to be consistent with other sources if limber_windows is true", + ), + ( + "limber_phi_lmin", + c_int, + "When limber_windows=True, the minimum L to use Limber approximation for the " + "lensing potential and other sources (which may use higher but not lower)", + ), + ("counts_density", c_bool, "Include the density perturbation source"), + ("counts_redshift", c_bool, "Include redshift distortions"), + ("counts_lensing", c_bool, "Include magnification bias for number counts"), + ("counts_velocity", c_bool, "Non-redshift distortion velocity terms"), + ( + "counts_radial", + c_bool, + "Radial displacement velocity term; does not include time delay; " + "subset of counts_velocity, just 1 / (chi * H) term", + ), + ("counts_timedelay", c_bool, "Include time delay terms * 1 / (H * chi)"), + ("counts_ISW", c_bool, "Include tiny ISW terms"), + ("counts_potential", c_bool, "Include tiny terms in potentials at source"), + ("counts_evolve", c_bool, "Account for source evolution"), + ("line_phot_dipole", c_bool, "Dipole sources for 21cm"), + ("line_phot_quadrupole", c_bool, "Quadrupole sources for 21cm"), + ("line_basic", c_bool, "Include main 21cm monopole density/spin temperature sources"), + ("line_distortions", c_bool, "Redshift distortions for 21cm"), + ("line_extra", c_bool, "Include other sources"), + ("line_reionization", c_bool, "Replace the E modes with 21cm polarization"), + ("use_21cm_mK", c_bool, "Use mK units for 21cm"), + ) + + +class CustomSources(CAMB_Structure): + """ + Structure containing symbolic-compiled custom CMB angular power spectrum source functions. + Don't change this directly, instead call :meth:`.model.CAMBparams.set_custom_scalar_sources`. + """ + + _fields_ = ( + ("num_custom_sources", c_int, "number of sources set"), + ("c_source_func", c_void_p, "Don't directly change this"), + ("custom_source_ell_scales", AllocatableArrayInt, "scaling in L for outputs"), + ) + + +@fortran_class +class CAMBparams(F2003Class): + """ + Object storing the parameters for a CAMB calculation, including cosmological parameters and + settings for what to calculate. When a new object is instantiated, default parameters are set automatically. + + To add a new parameter, add it to the CAMBparams type in model.f90, then edit the _fields_ list in the CAMBparams + class in model.py to add the new parameter in the corresponding location of the member list. After rebuilding the + python version you can then access the parameter by using params.new_parameter_name where params is a CAMBparams + instance. You could also modify the wrapper functions to set the field value less directly. + + You can view the set of underlying parameters used by the Fortran code by printing the CAMBparams instance. + In python, to set cosmology parameters it is usually best to use :meth:`set_cosmology` and + equivalent methods for most other parameters. Alternatively the convenience function :func:`.camb.set_params` + can construct a complete instance from a dictionary of relevant parameters. + You can also save and restore a CAMBparams instance using the repr and eval functions, or pickle it. + + """ + + _fields_ = ( + ("WantCls", c_bool, "Calculate C_L"), + ("WantTransfer", c_bool, "Calculate matter transfer functions and matter power spectrum"), + ("WantScalars", c_bool, "Calculates scalar modes"), + ("WantTensors", c_bool, "Calculate tensor modes"), + ("WantVectors", c_bool, "Calculate vector modes"), + ("WantDerivedParameters", c_bool, "Calculate derived parameters"), + ("Want_cl_2D_array", c_bool, "For the C_L, include NxN matrix of all possible cross-spectra between sources"), + ("Want_CMB", c_bool, "Calculate the temperature and polarization power spectra"), + ("Want_CMB_lensing", c_bool, "Calculate the lensing potential power spectrum"), + ("DoLensing", c_bool, "Include CMB lensing"), + ("NonLinear", c_int, {"names": NonLinear_names}), + ("Transfer", TransferParams), + ("want_zstar", c_bool), + ("want_zdrag", c_bool), + ("min_l", c_int, "l_min for the scalar C_L (1 or 2, L=1 dipoles are Newtonian Gauge)"), + ("max_l", c_int, "l_max for the scalar C_L"), + ("max_l_tensor", c_int, "l_max for the tensor C_L"), + ("max_eta_k", c_double, "Maximum k*eta_0 for scalar C_L, where eta_0 is the conformal time today"), + ("max_eta_k_tensor", c_double, "Maximum k*eta_0 for tensor C_L, where eta_0 is the conformal time today"), + ("ombh2", c_double, "Omega_baryon h^2"), + ("omch2", c_double, "Omega_cdm h^2"), + ("omk", c_double, "Omega_K"), + ("omnuh2", c_double, "Omega_massive_neutrino h^2"), + ("H0", c_double, "Hubble parameter is km/s/Mpc units"), + ("TCMB", c_double, "CMB temperature today in Kelvin"), + ("YHe", c_double, "Helium mass fraction"), + ("num_nu_massless", c_double, "Effective number of massless neutrinos"), + ("num_nu_massive", c_int, "Total physical (integer) number of massive neutrino species"), + ("nu_mass_eigenstates", c_int, "Number of non-degenerate mass eigenstates"), + ( + "share_delta_neff", + c_bool, + "Share the non-integer part of num_nu_massless between the eigenstates. " + "This is not needed or used in the python interface.", + ), + ( + "nu_mass_degeneracies", + c_double * max_nu, + {"size": "nu_mass_eigenstates"}, + "Degeneracy of each distinct eigenstate", + ), + ( + "nu_mass_fractions", + c_double * max_nu, + {"size": "nu_mass_eigenstates"}, + "Mass fraction in each distinct eigenstate", + ), + ( + "nu_mass_numbers", + c_int * max_nu, + {"size": "nu_mass_eigenstates"}, + "Number of physical neutrinos per distinct eigenstate", + ), + ("InitPower", AllocatableObject(InitialPower)), + ("Recomb", AllocatableObject(recomb.RecombinationModel)), + ("Reion", AllocatableObject(reion.ReionizationModel)), + ("DarkEnergy", AllocatableObject(DarkEnergyModel)), + ("NonLinearModel", AllocatableObject(NonLinearModel)), + ("Accuracy", AccuracyParams), + ("SourceTerms", SourceTermParams), + ("z_outputs", AllocatableArrayDouble, "redshifts to always calculate BAO output parameters"), + ( + "scalar_initial_condition", + c_int, + { + "names": [ + "initial_vector", + "initial_adiabatic", + "initial_iso_CDM", + "initial_iso_baryon", + "initial_iso_neutrino", + "initial_iso_neutrino_vel", + ] + }, + ), + ( + "InitialConditionVector", + AllocatableArrayDouble, + "if scalar_initial_condition is initial_vector, the vector of initial condition amplitudes", + ), + ("OutputNormalization", c_int, "If non-zero, multipole to normalize the C_L at"), + ("Alens", c_double, "non-physical scaling amplitude for the CMB lensing spectrum power"), + ("MassiveNuMethod", c_int, {"names": ["Nu_int", "Nu_trunc", "Nu_approx", "Nu_best"]}), + ( + "DoLateRadTruncation", + c_bool, + "If true, use smooth approx to radiation perturbations after decoupling on small" + " scales, saving evolution of irrelevant oscillatory multipole equations", + ), + ( + "Evolve_baryon_cs", + c_bool, + "Evolve a separate equation for the baryon sound speed rather than using background approximation", + ), + ("Evolve_delta_xe", c_bool, "Evolve ionization fraction perturbations"), + ("Evolve_delta_Ts", c_bool, "Evolve the spin temperature perturbation (for 21cm)"), + ("Do21cm", c_bool, "21cm is not yet implemented via the python wrapper"), + ("transfer_21cm_cl", c_bool, "Get 21cm C_L at a given fixed redshift"), + ("Log_lvalues", c_bool, "Use log spacing for sampling in L"), + ( + "use_cl_spline_template", + c_bool, + "When interpolating use a fiducial spectrum shape to define ratio to spline", + ), + ("min_l_logl_sampling", c_int, "Minimum L to use log sampling for L"), + ("SourceWindows", AllocatableObjectArray(SourceWindow)), + ("CustomSources", CustomSources), + ) + + H0: float + SourceWindows: list[SourceWindow] + + _fortran_class_module_ = "model" + + _methods_ = ( + ( + "SetNeutrinoHierarchy", + [POINTER(c_double), POINTER(c_double), POINTER(c_double), POINTER(c_int), POINTER(c_int)], + ), + ("Validate", None, c_int), + ("PrimordialPower", [numpy_1d, numpy_1d, POINTER(c_int), POINTER(c_int)]), + ("SetCustomSourcesFunc", [POINTER(c_int), POINTER(ctypes.c_void_p), numpy_1d_int]), + ) + + def __init__(self, **kwargs): + set_default_params(self) + self.InitPower.set_params() + super().__init__(**kwargs) + + def validate(self): + """ + Do some quick tests for sanity + + :return: True if OK + """ + return self.f_Validate() != 0 + + def write_ini(self, ini_filename, validate=True): + """ + Write the current parameters to a CAMB .ini file. + + :param ini_filename: path to the output .ini file + :param validate: whether to validate the written file + """ + return _ini.write_ini(self, ini_filename, validate=validate) + + def set_accuracy( + self, + AccuracyBoost=1.0, + lSampleBoost=1.0, + lAccuracyBoost=1.0, + DoLateRadTruncation=True, + min_l_logl_sampling=None, + ): + """ + Set parameters determining overall calculation accuracy (large values may give big slow down). + For finer control you can set individual accuracy parameters by changing CAMBParams.Accuracy + (:class:`.model.AccuracyParams`) . + + :param AccuracyBoost: increase AccuracyBoost to decrease integration step size, increase density of k + sampling, etc. + :param lSampleBoost: increase lSampleBoost to increase density of L sampling for CMB + :param lAccuracyBoost: increase lAccuracyBoost to increase the maximum L included in the Boltzmann hierarchies + :param DoLateRadTruncation: If True, use approximation to radiation perturbation evolution at late times + :param min_l_logl_sampling: at L>min_l_logl_sampling uses sparser log sampling for L interpolation; + increase above 5000 for better accuracy at L > 5000 + :return: self + """ + self.Accuracy.lSampleBoost = lSampleBoost + self.Accuracy.AccuracyBoost = AccuracyBoost + self.Accuracy.lAccuracyBoost = lAccuracyBoost + self.DoLateRadTruncation = DoLateRadTruncation + if min_l_logl_sampling: + self.min_l_logl_sampling = min_l_logl_sampling + return self + + def set_initial_power_function( + self, + P_scalar, + P_tensor=None, + kmin=1e-6, + kmax=100.0, + N_min=200, + rtol=5e-5, + effective_ns_for_nonlinear=None, + args=(), + ): + r""" + Set the initial power spectrum from a function P_scalar(k, \*args), and optionally also the tensor spectrum. + The function is called to make a pre-computed array which is then interpolated inside CAMB. The sampling in k + is set automatically so that the spline is accurate, but you may also need to increase other + accuracy parameters. + + :param P_scalar: function returning normalized initial scalar curvature power as function of k (in Mpc^{-1}) + :param P_tensor: optional function returning normalized initial tensor power spectrum + :param kmin: minimum wavenumber to compute + :param kmax: maximum wavenumber to compute + :param N_min: minimum number of spline points for the pre-computation + :param rtol: relative tolerance for deciding how many points are enough + :param effective_ns_for_nonlinear: an effective n_s for use with approximate non-linear corrections + :param args: optional list of arguments passed to P_scalar (and P_tensor) + :return: self + """ + + from scipy.interpolate import InterpolatedUnivariateSpline + + assert N_min > 7 + assert kmin < kmax + # sample function logspace, finely enough that it interpolates accurately + N = N_min + ktest = np.logspace(np.log10(kmin), np.log10(kmax), N // 2) + PK_test = P_scalar(ktest, *args) + while True: + ks = np.logspace(np.log10(kmin), np.log10(kmax), N) + PK_compare = InterpolatedUnivariateSpline(ktest, PK_test)(ks) + PK = P_scalar(ks, *args) + if np.allclose(PK, PK_compare, atol=np.max(PK) * 1e-6, rtol=rtol): + break + N *= 2 + PK_test = PK + ktest = ks + PK_t = None if P_tensor is None else P_tensor(ks, *args) + self.set_initial_power_table(ks, PK, PK_t, effective_ns_for_nonlinear) + return self + + def set_initial_power_table(self, k, pk=None, pk_tensor=None, effective_ns_for_nonlinear=None): + """ + Set a general initial power spectrum from tabulated values. It's up to you to ensure the sampling + of the k values is high enough that it can be interpolated accurately. + + :param k: array of k values (Mpc^{-1}) + :param pk: array of primordial curvature perturbation power spectrum values P(k_i) + :param pk_tensor: array of tensor spectrum values + :param effective_ns_for_nonlinear: an effective n_s for use with approximate non-linear corrections + """ + self.InitPower = SplinedInitialPower() + initpower = self.InitPower + if effective_ns_for_nonlinear is not None: + initpower.effective_ns_for_nonlinear = effective_ns_for_nonlinear + if pk is None: + pk = np.empty(0) + elif len(k) != len(pk): + raise CAMBValueError("k and P(k) arrays must be same size") + if pk_tensor is not None: + if len(k) != len(pk_tensor): + raise CAMBValueError("k and P_tensor(k) arrays must be same size") + initpower.set_tensor_table(k, pk_tensor) + initpower.set_scalar_table(k, pk) + return self + + def set_initial_power(self, initial_power_params): + """ + Set the InitialPower primordial power spectrum parameters + + :param initial_power_params: :class:`.initialpower.InitialPowerLaw` + or :class:`.initialpower.SplinedInitialPower` instance + :return: self + """ + self.InitPower = initial_power_params + return self + + def set_H0_for_theta( + self, theta, cosmomc_approx=False, theta_H0_range=(10, 100), est_H0=67.0, iteration_threshold=8, setter_H0=None + ): + r""" + Set H0 to give a specified value of the acoustic angular scale parameter theta. + + :param theta: value of :math:`r_s/D_M` at redshift :math:`z_\star` + :param cosmomc_approx: if true, use approximate fitting formula for :math:`z_\star`, + if false do full numerical calculation + :param theta_H0_range: min, max interval to search for H0 (in km/s/Mpc) + :param est_H0: an initial guess for H0 in km/s/Mpc, used in the case cosmomc_approx=False. + :param iteration_threshold: difference in H0 from est_H0 for which to iterate, + used for cosmomc_approx=False to correct for small changes in zstar when H0 changes + :param setter_H0: if specified, a function to call to set H0 for each iteration to find thetstar. It should be + a function(pars: CAMBParams, H0: float). Not normally needed, but can be used e.g. when DE model needs to be + changed for each H0 because it depends explicitly on e.g. Omega_m. + """ + + if not (0.001 < theta < 0.1): + raise CAMBParamRangeError("theta looks wrong (parameter is just theta, not 100*theta)") + + try: + from scipy.optimize import brentq + except ImportError: + raise CAMBError("You need SciPy to set cosmomc_theta.") + + from . import camb + + if setter_H0: + _set_H0 = setter_H0 + else: + + def _set_H0(params, H0): + params.H0 = H0 + + data = camb.CAMBdata() + if not cosmomc_approx: + zstar = c_double() + _set_H0(self, est_H0) + data.calc_background_no_thermo(self) + # get_zstar initializes the recombination model + zstar = data.f_get_zstar(byref(zstar)) + + def f(H0): + _set_H0(self, H0) + data.calc_background_no_thermo(self) + if cosmomc_approx: + theta_test = data.cosmomc_theta() + else: + rs = data.sound_horizon(zstar) + theta_test = rs / (data.angular_diameter_distance(zstar) * (1 + zstar)) + return theta_test - theta + + try: + # noinspection PyTypeChecker + self.H0 = brentq(f, theta_H0_range[0], theta_H0_range[1], rtol=5e-5) # type: ignore + if not cosmomc_approx and abs(self.H0 - est_H0) > iteration_threshold: + # iterate with recalculation of recombination and zstar + self.set_H0_for_theta( + theta, + theta_H0_range=theta_H0_range, + est_H0=self.H0, + iteration_threshold=iteration_threshold, + setter_H0=setter_H0, + ) + except ValueError: + raise CAMBParamRangeError("No solution for H0 inside of theta_H0_range") + + def set_cosmology( + self, + H0: float | None = None, + ombh2=0.022, + omch2=0.12, + omk=0.0, + cosmomc_theta: float | None = None, + thetastar: float | None = None, + neutrino_hierarchy: str | int = "degenerate", + num_massive_neutrinos=1, + mnu=0.06, + nnu=constants.default_nnu, + YHe: float | None = None, + meffsterile=0.0, + standard_neutrino_neff=constants.default_nnu, + TCMB=constants.COBE_CMBTemp, + tau: float | None = None, + zrei: float | None = None, + Alens=1.0, + bbn_predictor: None | str | bbn.BBNPredictor = None, + theta_H0_range=(10, 100), + setter_H0=None, + ): + r""" + Sets cosmological parameters in terms of physical densities and parameters (e.g. as used in Planck analyses). + Default settings give a single distinct neutrino mass eigenstate, by default one neutrino with mnu = 0.06eV. + Set the neutrino_hierarchy parameter to normal or inverted to use a two-eigenstate model that is a good + approximation to the known mass splittings seen in oscillation measurements. + For more fine-grained control can set the neutrino parameters directly rather than using this function. + + Instead of setting the Hubble parameter directly, you can instead set the acoustic scale parameter + (cosmomc_theta, which is based on a fitting formula for simple models, or thetastar, which is numerically + calculated more generally). Note that you must have already set the dark energy model, you can't use + set_cosmology with theta and then change the background evolution (which would change theta at the calculated + H0 value). Likewise, the dark energy model cannot depend explicitly on H0 unless you provide a custom + setter_H0 function to update the model for each H0 iteration used to search for thetastar. + + If in doubt, print CAMBparams after setting parameters to see the underlying values that have been set. + + :param H0: Hubble parameter today in km/s/Mpc. Can leave unset and instead set thetastar or cosmomc_theta + (which solves for the required H0). + :param ombh2: physical density in baryons + :param omch2: physical density in cold dark matter + :param omk: Omega_K curvature parameter + :param cosmomc_theta: The approximate CosmoMC theta parameter :math:`\theta_{\rm MC}`. The angular + diameter distance is calculated numerically, but the redshift :math:`z_\star` + is calculated using an approximate (quite accurate but non-general) fitting formula. + Leave unset to use H0 or thetastar. + :param thetastar: The angular acoustic scale parameter :math:`\theta_\star = r_s(z_*)/D_M(z_*)`, defined as + the ratio of the photon-baryon sound horizon :math:`r_s` to the angular diameter + distance :math:`D_M`, where both quantities are evaluated at :math:`z_*`, the redshift at + which the optical depth (excluding reionization) is unity. Leave unset to use H0 or cosmomc_theta. + :param neutrino_hierarchy: 'degenerate', 'normal', or 'inverted' (1 or 2 eigenstate approximation) + :param num_massive_neutrinos: number of massive neutrinos. If meffsterile is set, this is the number of + massive active neutrinos. + :param mnu: sum of neutrino masses (in eV). Omega_nu is calculated approximately from this assuming neutrinos + non-relativistic today; i.e. here is defined as a direct proxy for Omega_nu. Internally the actual + physical mass is calculated from the Omega_nu accounting for small mass-dependent velocity corrections + but neglecting spectral distortions to the neutrino distribution. + Set the neutrino field values directly if you need finer control or more complex neutrino models. + :param nnu: N_eff, effective relativistic degrees of freedom + :param YHe: Helium mass fraction. If None, set from BBN consistency. + :param meffsterile: effective mass of sterile neutrinos (set along with nnu greater than the standard value). + Defined as in the Planck papers. You do not need to also change num_massive_neutrinos. + :param standard_neutrino_neff: default value for N_eff in standard cosmology (non-integer to allow for partial + heating of neutrinos at electron-positron annihilation and QED effects) + :param TCMB: CMB temperature (in Kelvin) + :param tau: optical depth; if None and zrei is None, current Reion settings are not changed + :param zrei: reionization mid-point optical depth (set tau=None to use this) + :param Alens: (non-physical) scaling of the lensing potential compared to prediction + :param bbn_predictor: :class:`.bbn.BBNPredictor` instance used to get YHe from BBN consistency if YHe is None, + or name of a BBN predictor class, or file name of an interpolation table + :param theta_H0_range: if thetastar or cosmomc_theta is specified, the min, max interval of H0 values to map to; + if H0 is outside this range it will raise an exception. + :param setter_H0: if specified, a function to call to set H0 for each iteration to find thetastar. It should be + a function(pars: CAMBParams, H0: float). Not normally needed, but can be used e.g. when DE model needs to be + changed for each H0 because it depends explicitly on H0 + """ + + if YHe is None: + # use BBN prediction + if isinstance(bbn_predictor, str): + self.bbn_predictor = bbn.get_predictor(bbn_predictor) + else: + self.bbn_predictor = bbn_predictor or bbn.get_predictor() + YHe = self.bbn_predictor.Y_He(ombh2 * (constants.COBE_CMBTemp / TCMB) ** 3, nnu - standard_neutrino_neff) + self.YHe = YHe + self.TCMB = TCMB + self.ombh2 = ombh2 + self.omch2 = omch2 + self.Alens = Alens + + neutrino_mass_fac = constants.neutrino_mass_fac * (constants.COBE_CMBTemp / TCMB) ** 3 + + if not isinstance(neutrino_hierarchy, str): + neutrino_hierarchy = neutrino_hierarchies[neutrino_hierarchy - 1] + + if nnu >= standard_neutrino_neff or neutrino_hierarchy != neutrino_hierarchy_degenerate: + omnuh2 = mnu / neutrino_mass_fac * (standard_neutrino_neff / 3) ** 0.75 + else: + omnuh2 = mnu / neutrino_mass_fac * (nnu / 3.0) ** 0.75 + omnuh2_sterile = meffsterile / neutrino_mass_fac + if omnuh2_sterile > 0 and nnu < standard_neutrino_neff: + raise CAMBError(f"sterile neutrino mass required Neff> {constants.default_nnu:.3g}") + if omnuh2 and not num_massive_neutrinos: + raise CAMBError("non-zero mnu with zero num_massive_neutrinos") + + omnuh2 = omnuh2 + omnuh2_sterile + self.omnuh2 = omnuh2 + self.omk = omk + assert num_massive_neutrinos == int(num_massive_neutrinos) + self.f_SetNeutrinoHierarchy( + byref(c_double(omnuh2)), + byref(c_double(omnuh2_sterile)), + byref(c_double(nnu)), + byref(c_int(neutrino_hierarchies.index(neutrino_hierarchy) + 1)), + byref(c_int(int(num_massive_neutrinos))), + ) + + if cosmomc_theta or thetastar: + if H0 is not None: + raise CAMBError("Set H0=None when setting theta.") + if cosmomc_theta and thetastar: + raise CAMBError("Cannot set both cosmomc_theta and thetastar") + + self.set_H0_for_theta( + cosmomc_theta or thetastar, + cosmomc_approx=cosmomc_theta is not None, + theta_H0_range=theta_H0_range, + setter_H0=setter_H0, + ) + else: + if H0 is None: + raise CAMBError("Must set H0, cosmomc_theta or thetastar") + if H0 < 1: + raise CAMBValueError("H0 is the value in km/s/Mpc, your value looks very small") + self.H0 = H0 + + if tau is not None: + if zrei is not None: + raise CAMBError("Cannot set both tau and zrei") + self.Reion.set_tau(tau) + elif zrei is not None: + self.Reion.set_zrei(zrei) + + return self + + @property + def h(self): + return self.H0 / 100 + + @h.setter + def h(self, value): + self.H0 = value * 100 + + @property + def omegab(self): + return self.ombh2 / (self.H0 / 100) ** 2 + + @property + def omegac(self): + return self.omch2 / (self.H0 / 100) ** 2 + + @property + def omeganu(self): + return self.omnuh2 / (self.H0 / 100) ** 2 + + @property + def omegam(self): + return (self.ombh2 + self.omch2 + self.omnuh2) / (self.H0 / 100) ** 2 + + @property + def N_eff(self): + """ + :return: Effective number of degrees of freedom in relativistic species at early times. + """ + if self.share_delta_neff: + return self.num_nu_massless + self.num_nu_massive + else: + return sum(self.nu_mass_degeneracies[: self.nu_mass_eigenstates]) + self.num_nu_massless + + @property + def lmax(self): + return self.max_l + + def set_classes( + self, + dark_energy_model=None, + initial_power_model=None, + non_linear_model=None, + recombination_model=None, + reionization_model=None, + ): + """ + Change the classes used to implement parts of the model. + + :param dark_energy_model: 'fluid', 'ppf', or name of a DarkEnergyModel class + :param initial_power_model: name of an InitialPower class + :param non_linear_model: name of a NonLinearModel class + :param recombination_model: name of RecombinationModel class + :param reionization_model: name of a ReionizationModel class + """ + if dark_energy_model: + self.DarkEnergy = self.make_class_named(dark_energy_model, DarkEnergyModel) + if initial_power_model: + self.InitPower = self.make_class_named(initial_power_model, InitialPower) + if non_linear_model: + self.NonLinearModel = self.make_class_named(non_linear_model, NonLinearModel) + if recombination_model: + self.Recomb = self.make_class_named(recombination_model, RecombinationModel) + if reionization_model: + self.Reion = self.make_class_named(reionization_model, ReionizationModel) + + def set_dark_energy( + self, + w=-1.0, + cs2=1.0, + wa=0, + use_tabulated_w=False, + wde_a_array=None, + wde_w_array=None, + dark_energy_model="fluid", + ): + r""" + Set dark energy parameters (use set_dark_energy_w_a to set w(a) from numerical table instead) + To use a custom dark energy model, assign the class instance to the DarkEnergy field instead. + + :param w: :math:`w\equiv p_{\rm de}/\rho_{\rm de}`, assumed constant + :param wa: evolution of w (for dark_energy_model=ppf) + :param cs2: rest-frame sound speed squared of dark energy fluid + :param use_tabulated_w: whether use interpolated w + :param wde_a_array: array of scale factors + :param wde_w_array: array of w(a) + :param dark_energy_model: model to use ('fluid' or 'ppf'), default is 'fluid' + :return: self + """ + + de = self.make_class_named(dark_energy_model, DarkEnergyEqnOfState) + de.set_params( + w=w, wa=wa, cs2=cs2, use_tabulated_w=use_tabulated_w, wde_a_array=wde_a_array, wde_w_array=wde_w_array + ) + self.DarkEnergy = de + return self + + def set_dark_energy_w_a(self, a, w, dark_energy_model="fluid"): + """ + Set the dark energy equation of state from tabulated values (which are cubic spline interpolated). + + :param a: array of sampled a = 1/(1+z) values + :param w: array of w(a) + :param dark_energy_model: model to use ('fluid' or 'ppf'), default is 'fluid' + :return: self + """ + if dark_energy_model == "fluid" and np.any(w < -1): + raise CAMBError("fluid dark energy model does not support w crossing -1") + self.DarkEnergy = self.make_class_named(dark_energy_model, DarkEnergyEqnOfState) + # Note that assigning to allocatable fields makes deep copies of the object + self.DarkEnergy.set_w_a_table(a, w) + return self + + def get_zre(self): + return self.Reion.get_zre(self) + + # alias consistent with input parameter name + get_zrei = get_zre + + def get_Y_p(self, ombh2=None, delta_neff=None): + r""" + Get BBN helium nucleon fraction (NOT the same as the mass fraction Y_He) by interpolation using the + :class:`.bbn.BBNPredictor` instance passed to :meth:`set_cosmology` + (or the default one, if `Y_He` has not been set). + + :param ombh2: :math:`\Omega_b h^2` (default: value passed to :meth:`set_cosmology`) + :param delta_neff: additional :math:`N_{\rm eff}` relative to standard value (of 3.044) + (default: from values passed to :meth:`set_cosmology`) + :return: :math:`Y_p^{\rm BBN}` helium nucleon fraction predicted by BBN. + """ + try: + ombh2 = ombh2 if ombh2 is not None else self.ombh2 + delta_neff = delta_neff if delta_neff is not None else self.N_eff - constants.default_nnu + return self.bbn_predictor.Y_p(ombh2, delta_neff) + except AttributeError: + raise CAMBError("Not able to compute Y_p: not using an interpolation table for BBN abundances.") + + def get_DH(self, ombh2=None, delta_neff=None): + r""" + Get deuterium ration D/H by interpolation using the + :class:`.bbn.BBNPredictor` instance passed to :meth:`set_cosmology` + (or the default one, if `Y_He` has not been set). + + :param ombh2: :math:`\Omega_b h^2` (default: value passed to :meth:`set_cosmology`) + :param delta_neff: additional :math:`N_{\rm eff}` relative to standard value (of 3.044) + (default: from values passed to :meth:`set_cosmology`) + :return: BBN helium nucleon fraction D/H + """ + try: + ombh2 = ombh2 if ombh2 is not None else self.ombh2 + delta_neff = delta_neff if delta_neff is not None else self.N_eff - constants.default_nnu + return self.bbn_predictor.DH(ombh2, delta_neff) + except AttributeError: + raise CAMBError("Not able to compute DH: not using an interpolation table for BBN abundances.") + + def set_matter_power( + self, + redshifts=(0.0,), + kmax=1.2, + k_per_logint=None, + nonlinear=None, + accurate_massive_neutrino_transfers=False, + silent=False, + ): + """ + Set parameters for calculating matter power spectra and transfer functions. + + :param redshifts: array of redshifts to calculate + :param kmax: maximum k to calculate (where k is just k, not k/h) + :param k_per_logint: minimum number of k steps per log k. Set to zero to use default optimized spacing. + :param nonlinear: if None, uses existing setting, otherwise boolean for whether to use non-linear matter power. + :param accurate_massive_neutrino_transfers: if you want the massive neutrino transfers accurately + :param silent: if True, don't give warnings about sort order + :return: self + """ + if not len(redshifts): + raise CAMBError("set_matter_power redshifts list is empty") + + self.WantTransfer = True + self.Transfer.high_precision = True + self.Transfer.accurate_massive_neutrinos = accurate_massive_neutrino_transfers + self.Transfer.kmax = kmax + zs = sorted(redshifts, reverse=True) + if nonlinear is not None: + if nonlinear: + if self.NonLinear in [NonLinear_lens, NonLinear_both]: + self.NonLinear = NonLinear_both + else: + self.NonLinear = NonLinear_pk + if not silent and (kmax < 5 or kmax < 20 and np.max(zs) > 4): + logger.warning(f"Using kmax={kmax} with Halofit non-linear models may give inaccurate results") + else: + if self.NonLinear in [NonLinear_lens, NonLinear_both]: + self.NonLinear = NonLinear_lens + else: + self.NonLinear = NonLinear_none + self.Transfer.k_per_logint = k_per_logint if k_per_logint else 0 + if not silent and np.any(np.array(zs) - np.array(redshifts) != 0): + print("Note: redshifts have been re-sorted (earliest first)") + if len(redshifts) > max_transfer_redshifts: + raise CAMBError(f"You can have at most {max_transfer_redshifts} redshifts") + self.Transfer.PK_redshifts = zs + return self + + def set_nonlinear_lensing(self, nonlinear): + """ + Settings for whether or not to use non-linear corrections for the CMB lensing potential. + Note that set_for_lmax also sets lensing to be non-linear if lens_potential_accuracy>0 + + :param nonlinear: true to use non-linear corrections + """ + if nonlinear: + if self.NonLinear in [NonLinear_pk, NonLinear_both]: + self.NonLinear = NonLinear_both + else: + self.NonLinear = NonLinear_lens + else: + if self.NonLinear in [NonLinear_pk, NonLinear_both]: + self.NonLinear = NonLinear_pk + else: + self.NonLinear = NonLinear_none + + def set_for_lmax( + self, + lmax, + max_eta_k=None, + lens_potential_accuracy=0, + lens_margin=150, + k_eta_fac=2.5, + lens_k_eta_reference=18000.0, + nonlinear=None, + ): + r""" + Set parameters to get CMB power spectra accurate to specific a l_lmax. + Note this does not fix the actual output L range, spectra may be calculated above l_max + (but may not be accurate there). To fix the l_max for output arrays use the optional input argument + to :meth:`.results.CAMBdata.get_cmb_power_spectra` etc. + + :param lmax: :math:`\ell_{\rm max}` you want + :param max_eta_k: maximum value of :math:`k \eta_0\approx k\chi_*` to use, which indirectly sets k_max. + If None, sensible value set automatically. + :param lens_potential_accuracy: Set to 1 or higher if you want to get the lensing potential accurate + (1 is only Planck-level accuracy) + :param lens_margin: the :math:`\Delta \ell_{\rm max}` to use to ensure lensed :math:`C_\ell` are correct + at :math:`\ell_{\rm max}` + :param k_eta_fac: k_eta_fac default factor for setting max_eta_k = k_eta_fac*lmax if max_eta_k=None + :param lens_k_eta_reference: value of max_eta_k to use when lens_potential_accuracy>0; use + k_eta_max = lens_k_eta_reference*lens_potential_accuracy + :param nonlinear: use non-linear power spectrum; if None, sets nonlinear if lens_potential_accuracy>0 otherwise + preserves current setting + :return: self + """ + if self.DoLensing: + self.max_l = lmax + lens_margin + else: + self.max_l = lmax + self.max_eta_k = max_eta_k or self.max_l * k_eta_fac + if lens_potential_accuracy: + self.set_nonlinear_lensing(nonlinear is not False) + self.max_eta_k = max(self.max_eta_k, lens_k_eta_reference * lens_potential_accuracy) + elif nonlinear is not None: + self.set_nonlinear_lensing(nonlinear) + return self + + @overload + def scalar_power(self, k: float) -> float: ... + + @overload + def scalar_power(self, k: Array1D) -> np.ndarray: ... + + def scalar_power(self, k): + r""" + Get the primordial scalar curvature power spectrum at :math:`k` + + :param k: wavenumber :math:`k` (in :math:`{\rm Mpc}^{-1}` units) + :return: power spectrum at :math:`k` + """ + return self.primordial_power(k, 0) + + @overload + def tensor_power(self, k: float) -> float: ... + + @overload + def tensor_power(self, k: Array1D) -> np.ndarray: ... + + def tensor_power(self, k): + r""" + Get the primordial tensor curvature power spectrum at :math:`k` + + :param k: wavenumber :math:`k` (in :math:`{\rm Mpc}^{-1}` units) + :return: tensor power spectrum at :math:`k` + """ + + return self.primordial_power(k, 2) + + def primordial_power(self, k, ix): + karr = np.ascontiguousarray([k] if np.isscalar(k) else k, dtype=np.float64) + n = karr.shape[0] + powers = np.empty(n) + self.f_PrimordialPower(karr, powers, byref(c_int(n)), byref(c_int(ix))) + if np.isscalar(k): + return powers[0] + else: + return powers + + _custom_source_name_dict: ClassVar[dict] = {} + + def set_custom_scalar_sources( + self, custom_sources, source_names=None, source_ell_scales=None, frame="CDM", code_path=None + ): + r""" + Set custom sources for angular power spectrum using camb.symbolic sympy expressions. + + :param custom_sources: list of sympy expressions for the angular power spectrum sources + :param source_names: optional list of string names for the sources + :param source_ell_scales: list or dictionary of scalings for each source name, where for integer entry n, + the source for multipole :math:`\ell` is scaled by :math:`\sqrt{(\ell+n)!/(\ell-n)!}`, + i.e. :math:`n=2` for a new polarization-like source. + :param frame: if the source is not gauge invariant, frame in which to interpret result + :param code_path: optional path for output of source code for CAMB f90 source function + """ + + from . import symbolic + + if isinstance(custom_sources, dict): + assert not source_names + if source_ell_scales and not isinstance(source_ell_scales, dict): + raise CAMBValueError("source_ell_scales must be a dictionary if custom_sources is") + lst = [] + source_names = [] + for name in custom_sources: + source_names.append(name) + lst.append(custom_sources[name]) + custom_sources = lst + elif not isinstance(custom_sources, (list, tuple)): + custom_sources = [custom_sources] + if source_names: + source_names = [source_names] + custom_source_names = source_names or ["C%s" % (i + 1) for i in range(len(custom_sources))] + if len(custom_source_names) != len(custom_sources): + raise CAMBValueError("Number of custom source names does not match number of sources") + scales = np.zeros(len(custom_sources), dtype=np.int32) + if source_ell_scales: + if isinstance(source_ell_scales, dict): + if set(source_ell_scales.keys()) - set(custom_source_names): + raise CAMBValueError("scale dict key not in source names list") + for i, name in enumerate(custom_source_names): + if name in source_ell_scales: + scales[i] = source_ell_scales[name] + else: + scales[:] = source_ell_scales + + _current_source_func = symbolic.compile_sympy_to_camb_source_func( + custom_sources, frame=frame, code_path=code_path + ) + + custom_source_func = ctypes.cast(_current_source_func, c_void_p) + self._custom_source_name_dict[custom_source_func.value] = custom_source_names + self.f_SetCustomSourcesFunc(byref(c_int(len(custom_sources))), byref(custom_source_func), scales) + + def get_custom_source_names(self): + if self.CustomSources.num_custom_sources: + return self._custom_source_name_dict[self.CustomSources.c_source_func] + else: + return [] + + def clear_custom_scalar_sources(self): + self.f_SetCustomSourcesFunc(byref(c_int(0)), byref(ctypes.c_void_p(0)), np.zeros(0, dtype=np.int32)) + + def diff(self, params): + """ + Print differences between this set of parameters and params + + :param params: another CAMBparams instance + """ + p1 = str(params) + p2 = str(self) + for line1, line2 in zip(p1.split("\n"), p2.split("\n")): + if line1 != line2: + print(line1, " <-> ", line2) + + +def set_default_params(P): + """ + Set default values for all parameters + + :param P: :class:`.model.CAMBparams` + :return: P + """ + assert isinstance(P, CAMBparams) + camblib.__camb_MOD_camb_setdefparams(byref(P)) + return P diff --git a/camb/recombination.py b/camb/recombination.py index 8f004e3a..85efb98c 100644 --- a/camb/recombination.py +++ b/camb/recombination.py @@ -37,6 +37,9 @@ class Recfast(RecombinationModel): ("wGauss1", c_double), ("wGauss2", c_double), ("Nz", c_int), + ("use_rosenbrock", c_bool), + ("rosenbrock_handoff_xH", c_double), + ("rosenbrock_tol", c_double), ) _fortran_class_module_ = "Recombination" diff --git a/camb/tests/camb_test.py b/camb/tests/camb_test.py index 2c58fb12..e245fe5a 100644 --- a/camb/tests/camb_test.py +++ b/camb/tests/camb_test.py @@ -1,961 +1,1025 @@ -import os -import pickle -import platform -import subprocess -import sys -import tempfile -import unittest - -import numpy as np - -try: - import camb -except ImportError: - sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) - import camb -from camb import bbn, correlations, dark_energy, initialpower, model -from camb.baseconfig import CAMBError, CAMBParamRangeError, CAMBValueError - -fast = "ci fast" in os.getenv("GITHUB_ACTIONS", "") - - -class CambTest(unittest.TestCase): - def testAssigments(self): - ini = os.path.join(os.path.dirname(__file__), "..", "inifiles", "planck_2018.ini") - if os.path.exists(ini): - pars = camb.read_ini(ini) - self.assertTrue(np.abs(camb.get_background(pars).cosmomc_theta() * 100 / 1.040909 - 1) < 2e-5) - pars = camb.CAMBparams() - pars.set_cosmology(H0=68.5, ombh2=0.022, mnu=0, omch2=0.1) - self.assertAlmostEqual(pars.omegam, (0.022 + 0.1) / 0.685**2) - with self.assertRaises(AttributeError): - # noinspection PyPropertyAccess - pars.omegam = 1 - pars.InitPower.set_params(ns=0.01) - data = camb.CAMBdata() - data.Params = pars - self.assertEqual(data.Params.InitPower.ns, pars.InitPower.ns) - d = dark_energy.DarkEnergyFluid(w=-0.95) - pars.DarkEnergy = d - self.assertEqual(pars.DarkEnergy.w, -0.95) - pars.DarkEnergy = dark_energy.AxionEffectiveFluid(w_n=0.4) - data.Params = pars - self.assertEqual(pars.DarkEnergy.w_n, 0.4) - pars.z_outputs = [0.1, 0.4] - self.assertEqual(pars.z_outputs[1], 0.4) - pars.z_outputs[0] = 0.3 - self.assertEqual(pars.z_outputs[0], 0.3) - pars.z_outputs = pars.z_outputs - pars.z_outputs = [] - pars.z_outputs = None - # noinspection PyTypeChecker - self.assertFalse(len(pars.z_outputs)) - with self.assertRaises(TypeError): - pars.DarkEnergy = initialpower.InitialPowerLaw() - pars.NonLinear = model.NonLinear_both - printstr = str(pars) - self.assertTrue("Want_CMB_lensing = True" in printstr and "NonLinear = NonLinear_both" in printstr) - pars.NonLinear = model.NonLinear_lens - self.assertTrue(pars.NonLinear == model.NonLinear_lens) - with self.assertRaises(ValueError): - pars.NonLinear = 4 - pars.nu_mass_degeneracies = np.zeros(3) - self.assertTrue(len(pars.nu_mass_degeneracies) == 3) - pars.nu_mass_degeneracies = [1, 2, 3] - self.assertTrue(pars.nu_mass_degeneracies[1] == 2) - pars.nu_mass_degeneracies[1] = 5 - self.assertTrue(pars.nu_mass_degeneracies[1] == 5) - with self.assertRaises(CAMBParamRangeError): - pars.nu_mass_degeneracies = np.zeros(7) - pars.nu_mass_eigenstates = 0 - self.assertFalse(len(pars.nu_mass_degeneracies[:1])) - pars = camb.set_params(**{"InitPower.ns": 1.2, "WantTransfer": True}) - self.assertEqual(pars.InitPower.ns, 1.2) - self.assertTrue(pars.WantTransfer) - pars.DarkEnergy = None - pars = camb.set_params(**{"H0": 67, "ombh2": 0.002, "r": 0.1, "Accuracy.AccurateBB": True}) - self.assertEqual(pars.Accuracy.AccurateBB, True) - - from camb.sources import GaussianSourceWindow - - pars = camb.CAMBparams() - pars.SourceWindows = [GaussianSourceWindow(), GaussianSourceWindow(redshift=1)] - self.assertEqual(pars.SourceWindows[1].redshift, 1) - pars.SourceWindows[0].redshift = 2 - self.assertEqual(pars.SourceWindows[0].redshift, 2) - self.assertTrue(len(pars.SourceWindows) == 2) - pars.SourceWindows[0] = GaussianSourceWindow(redshift=3) - self.assertEqual(pars.SourceWindows[0].redshift, 3) - self.assertTrue("redshift = 3.0" in str(pars)) - pars.SourceWindows = pars.SourceWindows[0:1] - self.assertTrue(len(pars.SourceWindows) == 1) - pars.SourceWindows = [] - self.assertTrue(len(pars.SourceWindows) == 0) - params = camb.get_valid_numerical_params() - self.assertEqual( - params, - { - "ombh2", - "deltazrei", - "omnuh2", - "tau", - "omk", - "zrei", - "thetastar", - "nrunrun", - "meffsterile", - "nnu", - "ntrun", - "HMCode_A_baryon", - "HMCode_eta_baryon", - "HMCode_logT_AGN", - "cosmomc_theta", - "YHe", - "wa", - "cs2", - "H0", - "mnu", - "Alens", - "TCMB", - "ns", - "nrun", - "As", - "nt", - "r", - "w", - "omch2", - "max_zrei", - "wde_a_array", - "wde_w_array", - }, - ) - params2 = camb.get_valid_numerical_params(dark_energy_model="AxionEffectiveFluid") - self.assertEqual(params2.difference(params), {"fde_zc", "w_n", "zc", "theta_i"}) - pars = camb.set_params( - H0=67, - ombh2=0.022, - omch2=0.12, - dark_energy_model="AxionEffectiveFluid", - w_n=0.4, - fde_zc=0.05, - zc=4000, - ) - self.assertIsInstance(pars.DarkEnergy, dark_energy.AxionEffectiveFluid) - self.assertEqual(pars.DarkEnergy.w_n, 0.4) - self.assertEqual(pars.DarkEnergy.fde_zc, 0.05) - self.assertEqual(pars.DarkEnergy.zc, 4000) - - def testWriteIniRoundTrip(self): - script = os.path.abspath( - os.path.join(os.path.dirname(__file__), "..", "..", "fortran", "tests", "CAMB_test_files.py") - ) - script_dir = os.path.dirname(script) - repo_root = os.path.abspath(os.path.join(script_dir, "..", "..")) - fortran_dir = os.path.join(repo_root, "fortran") - base_settings = os.path.join(repo_root, "inifiles", "params.ini") - - with tempfile.TemporaryDirectory() as ini_dir: - subprocess.run( - [ - sys.executable, - script, - ini_dir, - "--make_ini", - "--no_run_test", - "--base_settings", - base_settings, - ], - check=True, - cwd=fortran_dir, - ) - ini_files = sorted( - os.path.join(ini_dir, filename) - for filename in os.listdir(ini_dir) - if filename.endswith(".ini") and filename.startswith("params_") - ) - self.assertTrue(ini_files) - - cwd = os.getcwd() - os.chdir(fortran_dir) - try: - for ini_file in ini_files: - with self.subTest(ini=os.path.basename(ini_file)): - pars = camb.read_ini(ini_file) - written_ini = os.path.join(ini_dir, "written_" + os.path.basename(ini_file)) - camb.write_ini(pars, written_ini) - self.assertTrue(os.path.exists(written_ini)) - finally: - os.chdir(cwd) - - def testWriteIniFromPythonParams(self): - pars = camb.CAMBparams() - pars.set_cosmology(H0=67, ombh2=0.0224, omch2=0.119, tau=0.054, mnu=0.06) - pars.set_dark_energy(w=-0.95, wa=0.15, dark_energy_model="ppf") - pars.InitPower.set_params(As=2.1e-9, ns=0.965, nrun=0.01, r=0.03, nt=0.0) - pars.set_matter_power(redshifts=[0.0, 0.5, 1.0], kmax=2.0, accurate_massive_neutrino_transfers=True) - pars.set_for_lmax(2200, lens_potential_accuracy=1) - pars.WantTensors = True - pars.NonLinearModel.set_params(halofit_version="mead2020_feedback", HMCode_logT_AGN=7.7) - pars.Alens = 0.95 - - with tempfile.TemporaryDirectory() as temp_dir: - ini_file = os.path.join(temp_dir, "python_params.ini") - pars.write_ini(ini_file) - self.assertTrue(os.path.exists(ini_file)) - - def testBackground(self): - pars = camb.CAMBparams() - pars.set_cosmology(H0=68.5, ombh2=0.022, omch2=0.122, YHe=0.2453, mnu=0.07, omk=0) - zre = camb.get_zre_from_tau(pars, 0.06) - age = camb.get_age(pars) - self.assertAlmostEqual(zre, 8.39, 2) - self.assertAlmostEqual(age, 13.65, 2) - - data = camb.CAMBdata() - bao = data.get_BAO([0.57, 0.27], pars) - - data = camb.CAMBdata() - data.calc_background(pars) - - DA = data.angular_diameter_distance(0.57) - H = data.hubble_parameter(0.27) - self.assertAlmostEqual(DA, bao[0][2], 3) - self.assertAlmostEqual(H, bao[1][1], 3) - - age2 = data.physical_time(0) - self.assertAlmostEqual(age, age2, 4) - - data.comoving_radial_distance(0.48) - t0 = data.conformal_time(0) - self.assertAlmostEqual(t0, data.tau0) - t1 = data.conformal_time(11.5) - t2 = data.comoving_radial_distance(11.5) - self.assertAlmostEqual(t2, t0 - t1, 2) - self.assertAlmostEqual(t1, 4200.809, 2) - chistar = data.conformal_time(0) - data.tau_maxvis - chis = np.linspace(0, chistar, 197) - zs = data.redshift_at_comoving_radial_distance(chis) - chitest = data.comoving_radial_distance(zs) - self.assertTrue(np.sum((chitest - chis) ** 2) < 1e-3) - - theta = data.cosmomc_theta() - self.assertAlmostEqual(theta, 0.0104759965, 5) - - derived = data.get_derived_params() - self.assertAlmostEqual(derived["age"], age, 2) - self.assertAlmostEqual(derived["rdrag"], 146.986, 2) - self.assertAlmostEqual(derived["rstar"], data.sound_horizon(derived["zstar"]), 2) - - # Test BBN consistency, base_plikHM_TT_lowTEB best fit model - pars.set_cosmology(H0=67.31, ombh2=0.022242, omch2=0.11977, mnu=0.06, omk=0) - self.assertAlmostEqual(pars.YHe, 0.2458, 5) - data.calc_background(pars) - self.assertAlmostEqual(data.cosmomc_theta(), 0.0104090741, 7) - self.assertAlmostEqual(data.get_derived_params()["kd"], 0.14055, 4) - - pars.set_cosmology( - H0=67.31, ombh2=0.022242, omch2=0.11977, mnu=0.06, omk=0, bbn_predictor=bbn.BBN_table_interpolator() - ) - self.assertAlmostEqual(pars.YHe, 0.2458, 5) - self.assertAlmostEqual(pars.get_Y_p(), bbn.BBN_table_interpolator().Y_p(0.022242, 0), 5) - - # test massive sterile models as in Planck papers - pars.set_cosmology(H0=68.0, ombh2=0.022305, omch2=0.11873, mnu=0.06, nnu=3.073, omk=0, meffsterile=0.013) - self.assertAlmostEqual(pars.omnuh2, 0.00078, 5) - self.assertAlmostEqual(pars.YHe, 0.246218, 5) - self.assertAlmostEqual(pars.N_eff, 3.073, 4) - - data.calc_background(pars) - self.assertAlmostEqual(data.get_derived_params()["age"], 13.773, 2) - self.assertAlmostEqual(data.cosmomc_theta(), 0.0104103, 6) - - # test dark energy - pars.set_cosmology(H0=68.26, ombh2=0.022271, omch2=0.11914, mnu=0.06, omk=0) - pars.set_dark_energy(w=-1.0226, dark_energy_model="fluid") - - data.calc_background(pars) - self.assertAlmostEqual(data.get_derived_params()["age"], 13.789, 2) - scal = data.luminosity_distance(1.4) - vec = data.luminosity_distance([1.2, 1.4, 0.1, 1.9]) - self.assertAlmostEqual(scal, vec[1], 5) - - pars.set_dark_energy() # re-set defaults - - # test theta - pars.set_cosmology(cosmomc_theta=0.0104085, ombh2=0.022271, omch2=0.11914, mnu=0.06, omk=0) - self.assertAlmostEqual(pars.H0, 67.537, 2) - with self.assertRaises(CAMBParamRangeError): - pars.set_cosmology(cosmomc_theta=0.0204085, ombh2=0.022271, omch2=0.11914, mnu=0.06, omk=0) - pars = camb.set_params(cosmomc_theta=0.0104077, ombh2=0.022, omch2=0.122, w=-0.95) - self.assertAlmostEqual(camb.get_background(pars, no_thermo=True).cosmomc_theta(), 0.0104077, 7) - - pars = camb.set_params(thetastar=0.010311, ombh2=0.022, omch2=0.122) - self.assertAlmostEqual(camb.get_background(pars).get_derived_params()["thetastar"] / 100, 0.010311, 7) - pars = camb.set_params(thetastar=0.010311, ombh2=0.022, omch2=0.122, omk=-0.05) - self.assertAlmostEqual(camb.get_background(pars).get_derived_params()["thetastar"] / 100, 0.010311, 7) - self.assertAlmostEqual(pars.H0, 49.70624, places=3) - - pars = camb.set_params( - cosmomc_theta=0.0104077, ombh2=0.022, omch2=0.122, w=-0.95, wa=0, dark_energy_model="ppf" - ) - self.assertAlmostEqual(camb.get_background(pars, no_thermo=True).cosmomc_theta(), 0.0104077, 7) - - pars = camb.set_params( - cosmomc_theta=0.0104077, - ombh2=0.022, - omch2=0.122, - w=-0.95, - dark_energy_model="DarkEnergyFluid", - initial_power_model="InitialPowerLaw", - ) - self.assertAlmostEqual(camb.get_background(pars, no_thermo=True).cosmomc_theta(), 0.0104077, 7) - - with self.assertRaises(CAMBValueError): - camb.set_params(dark_energy_model="InitialPowerLaw") - data.calc_background(pars) - h2 = (data.Params.H0 / 100) ** 2 - self.assertAlmostEqual(data.get_Omega("baryon"), data.Params.ombh2 / h2, 7) - self.assertAlmostEqual(data.get_Omega("nu"), data.Params.omnuh2 / h2, 7) - self.assertAlmostEqual( - data.get_Omega("photon") - + data.get_Omega("neutrino") - + data.get_Omega("de") - + (pars.ombh2 + pars.omch2 + pars.omnuh2) / h2 - + pars.omk, - 1, - 8, - ) - pars.set_cosmology(H0=67, mnu=1, neutrino_hierarchy="normal") - data.calc_background(pars) - h2 = (pars.H0 / 100) ** 2 - self.assertAlmostEqual( - data.get_Omega("photon") - + data.get_Omega("neutrino") - + data.get_Omega("de") - + (pars.ombh2 + pars.omch2 + pars.omnuh2) / h2 - + pars.omk, - 1, - 8, - ) - redshifts = np.array([0.005, 0.01, 0.3, 0.9342, 4, 27, 321.5, 932, 1049, 1092, 2580, 1e4, 2.1e7]) - self.assertTrue( - np.allclose(data.redshift_at_conformal_time(data.conformal_time(redshifts)), redshifts, rtol=1e-7) - ) - pars.set_dark_energy(w=-1.8) - data.calc_background(pars) - self.assertTrue( - np.allclose(data.redshift_at_conformal_time(data.conformal_time(redshifts)), redshifts, rtol=1e-7) - ) - pars.set_cosmology(cosmomc_theta=0.0104085) - data.calc_background(pars) - self.assertAlmostEqual(data.cosmomc_theta(), 0.0104085) - derived = data.get_derived_params() - pars.Accuracy.BackgroundTimeStepBoost = 2 - data.calc_background(pars) - derived2 = data.get_derived_params() - self.assertAlmostEqual(derived["thetastar"], derived2["thetastar"], places=5) - pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.11, neutrino_hierarchy="inverted") - self.assertEqual(pars.num_nu_massive, 3) - self.assertEqual(pars.nu_mass_numbers[1], 1) - self.assertEqual(pars.nu_mass_eigenstates, 2) - self.assertAlmostEqual(pars.nu_mass_fractions[0], 0.915197, places=4) - - pars = camb.CAMBparams() - pars.set_cosmology(H0=68.5, ombh2=0.022, omch2=0.122, YHe=0.2453, mnu=0.07, omk=0, zrei=zre) - results = camb.get_background(pars) - self.assertEqual(results.Params.Reion.redshift, zre) - - pars = camb.CAMBparams() - pars.set_cosmology(H0=68.5, ombh2=0.022, omch2=0.122, YHe=0.2453, mnu=0.07, omk=-0.05) - data = camb.get_background(pars) - delta2 = ( - data.curvature_radius - / (1 + 0.25) - * ( - np.sin( - (data.comoving_radial_distance(0.25) - data.comoving_radial_distance(0.05)) / data.curvature_radius - ) - ) - ) - np.testing.assert_allclose(delta2, data.angular_diameter_distance2(0.05, 0.25)) - dists = data.angular_diameter_distance2([0.3, 0.05, 0.25], [1, 0.25, 0.05]) - self.assertAlmostEqual(delta2, dists[1]) - self.assertEqual(0, dists[2]) - - self.assertEqual(data.physical_time(0.4), data.physical_time([0.2, 0.4])[1]) - d = data.conformal_time_a1_a2(0, 0.5) + data.conformal_time_a1_a2(0.5, 1) - self.assertAlmostEqual(d, data.conformal_time_a1_a2(0, 1)) - self.assertAlmostEqual(d, sum(data.conformal_time_a1_a2([0, 0.5], [0.5, 1]))) - - def testErrors(self): - redshifts = np.logspace(-1, np.log10(1089)) - pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, redshifts=redshifts, kmax=0.1) - - results = camb.get_background(pars) - with self.assertRaises(CAMBError): - results.get_matter_power_interpolator() - - def testEvolution(self): - redshifts = [0.4, 31.5] - pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, redshifts=redshifts, kmax=0.1) - pars.WantCls = False - - # check transfer function routines and evolution code agree - # Note transfer function redshifts are re-sorted in outputs - data = camb.get_transfer_functions(pars) - mtrans = data.get_matter_transfer_data() - transfer_k = mtrans.transfer_z("delta_cdm", z_index=1) - transfer_k2 = mtrans.transfer_z("delta_baryon", z_index=0) - - kh = mtrans.transfer_z("k/h", z_index=1) - ev = data.get_redshift_evolution( - mtrans.q, redshifts, ["delta_baryon", "delta_cdm", "delta_photon"], lAccuracyBoost=1 - ) - self.assertTrue(np.all(np.abs(transfer_k * kh**2 * (pars.H0 / 100) ** 2 / ev[:, 0, 1] - 1) < 1e-3)) - ix = 1 - self.assertAlmostEqual(transfer_k2[ix] * kh[ix] ** 2 * (pars.H0 / 100) ** 2, ev[ix, 1, 0], 4) - - def testInstances(self): - pars = camb.set_params( - H0=69.1, ombh2=0.032, omch2=0.122, As=3e-9, ns=0.91, omk=0.013, redshifts=[0.0], kmax=0.5 - ) - data = camb.get_background(pars) - res1 = data.angular_diameter_distance(0.7) - drag1 = data.get_derived_params()["rdrag"] - pars2 = camb.set_params(H0=65, ombh2=0.022, omch2=0.122, As=3e-9, ns=0.91) - data2 = camb.get_background(pars2) - res2 = data2.angular_diameter_distance(1.7) - drag2 = data2.get_derived_params()["rdrag"] - self.assertAlmostEqual(res1, data.angular_diameter_distance(0.7)) - self.assertAlmostEqual(res2, data2.angular_diameter_distance(1.7)) - self.assertAlmostEqual(drag1, data.get_derived_params()["rdrag"]) - self.assertEqual(pars2.InitPower.ns, data2.Params.InitPower.ns) - data2.calc_background(pars) - self.assertEqual(pars.InitPower.ns, data2.Params.InitPower.ns) - self.assertAlmostEqual(res1, data2.angular_diameter_distance(0.7)) - data3 = camb.get_results(pars2) - cl3 = data3.get_lensed_scalar_cls(1000) - self.assertAlmostEqual(res2, data3.angular_diameter_distance(1.7)) - self.assertAlmostEqual(drag2, data3.get_derived_params()["rdrag"], places=3) - self.assertAlmostEqual(drag1, data.get_derived_params()["rdrag"], places=3) - pars.set_for_lmax(3000, lens_potential_accuracy=1) - camb.get_results(pars) - del data3 - data4 = camb.get_results(pars2) - cl4 = data4.get_lensed_scalar_cls(1000) - self.assertTrue(np.allclose(cl4, cl3)) - - def testPowers(self): - pars = camb.CAMBparams() - pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.07, omk=0) - pars.set_dark_energy() # re-set defaults - pars.InitPower.set_params(ns=0.965, As=2e-9) - pars.NonLinearModel.set_params(halofit_version="takahashi") - - self.assertAlmostEqual(pars.scalar_power(1), 1.801e-9, 4) - self.assertAlmostEqual(pars.scalar_power([1, 1.5])[0], 1.801e-9, 4) - - pars.set_matter_power(nonlinear=True) - self.assertEqual(pars.NonLinear, model.NonLinear_pk) - pars.set_matter_power(redshifts=[0.0, 0.17, 3.1], silent=True, nonlinear=False) - data = camb.get_results(pars) - - kh, z, pk = data.get_matter_power_spectrum(1e-4, 1, 20) - - kh2, z2, pk2 = data.get_linear_matter_power_spectrum() - - s8 = data.get_sigma8() - self.assertAlmostEqual(s8[0], 0.24686, 3) - self.assertAlmostEqual(s8[2], 0.80044, 3) - fs8 = data.get_fsigma8() - self.assertAlmostEqual(fs8[0], 0.2431, 3) - self.assertAlmostEqual(fs8[2], 0.424712, 3) - - pars.NonLinear = model.NonLinear_both - - data.calc_power_spectra(pars) - kh3, z3, pk3 = data.get_matter_power_spectrum(1e-4, 1, 20) - self.assertAlmostEqual(pk[-1][-3], 51.924, 2) - self.assertAlmostEqual(pk3[-1][-3], 57.723, 2) - self.assertAlmostEqual(pk2[-2][-4], 56.454, 2) - camb.set_feedback_level(0) - - PKnonlin = camb.get_matter_power_interpolator(pars, nonlinear=True) - pars.set_matter_power( - redshifts=[0, 0.09, 0.15, 0.42, 0.76, 1.5, 2.3, 5.5, 8.9], silent=True, kmax=10, k_per_logint=5 - ) - pars.NonLinear = model.NonLinear_both - results = camb.get_results(pars) - kh, z, pk = results.get_nonlinear_matter_power_spectrum() - pk_interp = PKnonlin.P(z, kh) - self.assertTrue(np.sum((pk / pk_interp - 1) ** 2) < 0.005) - PKnonlin2 = results.get_matter_power_interpolator(nonlinear=True, extrap_kmax=500) - pk_interp2 = PKnonlin2.P(z, kh) - self.assertTrue(np.sum((pk_interp / pk_interp2 - 1) ** 2) < 0.005) - - pars.NonLinearModel.set_params(halofit_version="mead") - _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) - self.assertAlmostEqual(pk[0][160], 814.9, delta=0.5) - - pars.NonLinearModel.set_params(halofit_version="mead2016") - _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) - self.assertAlmostEqual(pk[0][160], 814.9, delta=0.5) - - pars.NonLinearModel.set_params(halofit_version="mead2015") - _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) - self.assertAlmostEqual(pk[0][160], 791.3, delta=0.5) - - pars.NonLinearModel.set_params(halofit_version="mead2020") - _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) - self.assertAlmostEqual(pk[0][160], 815.8, delta=0.5) - - pars.NonLinearModel.set_params(halofit_version="mead2020_feedback") - _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) - self.assertAlmostEqual(pk[0][160], 799.0, delta=0.5) - - lmax = 4000 - pars.set_for_lmax(lmax) - cls = data.get_cmb_power_spectra(pars) - data.get_total_cls(2000) - cls_unlensed = data.get_unlensed_scalar_cls(2500) - data.get_tensor_cls(2000) - cls_lensed = data.get_lensed_scalar_cls(3000) - data.get_lens_potential_cls(2000) - - cls_lensed2 = data.get_lensed_cls_with_spectrum(data.get_lens_potential_cls()[:, 0], lmax=3000) - np.testing.assert_allclose(cls_lensed2[2:, :], cls_lensed[2:, :], rtol=1e-4) - cls_lensed2 = data.get_partially_lensed_cls(1, lmax=3000) - np.testing.assert_allclose(cls_lensed2[2:, :], cls_lensed[2:, :], rtol=1e-4) - cls_lensed2 = data.get_partially_lensed_cls(0, lmax=2500) - np.testing.assert_allclose(cls_lensed2[2:, :], cls_unlensed[2:, :], rtol=1e-4) - - # check lensed CL against python; will only agree well for high lmax as python has no extrapolation template - cls_lensed2 = correlations.lensed_cls(cls["unlensed_scalar"], cls["lens_potential"][:, 0], delta_cls=False) - np.testing.assert_allclose(cls_lensed2[2:2000, 2], cls_lensed[2:2000, 2], rtol=1e-3) - np.testing.assert_allclose(cls_lensed2[2:2000, 1], cls_lensed[2:2000, 1], rtol=1e-3) - np.testing.assert_allclose(cls_lensed2[2:2000, 0], cls_lensed[2:2000, 0], rtol=1e-3) - self.assertTrue( - np.all( - np.abs( - (cls_lensed2[2:3000, 3] - cls_lensed[2:3000, 3]) - / np.sqrt(cls_lensed2[2:3000, 0] * cls_lensed2[2:3000, 1]) - ) - < 1e-4 - ) - ) - - corr, xvals, weights = correlations.gauss_legendre_correlation(cls["lensed_scalar"]) - clout = correlations.corr2cl(corr, xvals, weights, 2500) - self.assertTrue(np.all(np.abs(clout[2:2300, 2] / cls["lensed_scalar"][2:2300, 2] - 1) < 1e-3)) - - pars = camb.CAMBparams() - pars.set_cosmology(H0=78, YHe=0.22) - pars.set_for_lmax(2000, lens_potential_accuracy=1) - pars.WantTensors = True - results = camb.get_transfer_functions(pars) - from camb import initialpower - - cls = [] - for r in [0, 0.2, 0.4]: - inflation_params = initialpower.InitialPowerLaw() - inflation_params.set_params(ns=0.96, r=r, nt=0) - results.power_spectra_from_transfer(inflation_params, silent=True) - cls += [results.get_total_cls(CMB_unit="muK")] - self.assertTrue(np.allclose((cls[1] - cls[0])[2:300, 2] * 2, (cls[2] - cls[0])[2:300, 2], rtol=1e-3)) - - # Check generating tensors and scalars together - pars = camb.CAMBparams() - pars.set_cosmology(H0=67) - lmax = 2000 - pars.set_for_lmax(lmax, lens_potential_accuracy=1) - pars.InitPower.set_params(ns=0.96, r=0) - pars.WantTensors = False - results = camb.get_results(pars) - cl1 = results.get_total_cls(lmax, CMB_unit="muK") - pars.InitPower.set_params(ns=0.96, r=0.1, nt=0) - pars.WantTensors = True - results = camb.get_results(pars) - cl2 = results.get_lensed_scalar_cls(lmax, CMB_unit="muK") - ctensor2 = results.get_tensor_cls(lmax, CMB_unit="muK") - results = camb.get_transfer_functions(pars) - results.Params.InitPower.set_params(ns=1.1, r=1) - inflation_params = initialpower.InitialPowerLaw() - inflation_params.set_params(ns=0.96, r=0.05, nt=0) - results.power_spectra_from_transfer(inflation_params, silent=True) - cl3 = results.get_lensed_scalar_cls(lmax, CMB_unit="muK") - ctensor3 = results.get_tensor_cls(lmax, CMB_unit="muK") - self.assertTrue(np.allclose(ctensor2, ctensor3 * 2, rtol=1e-4)) - self.assertTrue(np.allclose(cl1, cl2, rtol=1e-4)) - # These are identical because all scalar spectra were identical (non-linear corrections change it otherwise) - self.assertTrue(np.allclose(cl1, cl3, rtol=1e-4)) - - pars = camb.CAMBparams() - pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.07, omk=0) - pars.set_for_lmax(2500) - pars.min_l = 2 - res = camb.get_results(pars) - cls = res.get_lensed_scalar_cls(2000) - pars.min_l = 1 - res = camb.get_results(pars) - cls2 = res.get_lensed_scalar_cls(2000) - np.testing.assert_allclose(cls[2:, 0:2], cls2[2:, 0:2], rtol=1e-4) - self.assertAlmostEqual(cls2[1, 0], 1.30388e-10, places=13) - self.assertAlmostEqual(cls[1, 0], 0) - - def testSave(self): - pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, redshifts=[0.4, 31.5], kmax=0.1) - pars.set_dark_energy(w=-0.7, wa=0.2, dark_energy_model="ppf") - from camb.sources import GaussianSourceWindow - - pars.SourceWindows = [GaussianSourceWindow(), GaussianSourceWindow(redshift=1)] - s = repr(pars) - pars2 = eval(s) - assert repr(pars2) == s - assert "DarkEnergyPPF" in str(pars2) - b = pickle.dumps(pars) - pars2 = pickle.loads(b) - assert repr(pars2) == s - pars2.InitPower = initialpower.SplinedInitialPower() - with self.assertRaises(TypeError): - repr(pars2) - - def testSigmaR(self): - pars = camb.CAMBparams() - pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.07, omk=0) - pars.InitPower.set_params(ns=0.965, As=2e-9) - pars.set_matter_power(nonlinear=False) - results = camb.get_results(pars) - sigma8 = results.get_sigma8_0() - self.assertAlmostEqual(sigma8, results.get_sigmaR(8)[-1], places=3) - self.assertAlmostEqual(sigma8, results.get_sigmaR(np.array([8]), z_indices=-1)[-1], places=3) - self.assertAlmostEqual(results.get_sigmaR(8)[-1], results.get_sigmaR(8, z_indices=-1)) - pars.set_matter_power(nonlinear=False, k_per_logint=0, kmax=2) - - results = camb.get_results(pars) - P, z, k = results.get_matter_power_interpolator( - nonlinear=False, hubble_units=False, k_hunit=False, return_z_k=True, extrap_kmax=100, silent=True - ) - truth = 0.800679 # from high kmax, high accuracy boost - self.assertTrue(abs(results.get_sigmaR(8)[-1] / sigma8 - 1) < 1e-3) - - def get_sigma(_ks, dlogk): - x = _ks * 8 / (pars.H0 / 100) - w = (3 * (np.sin(x) - x * np.cos(x)) / x**3) ** 2 - w[x < 1e-2] = 1 - x[x < 1e-2] ** 2 / 2 - Ps = P.P(0, _ks) * _ks**3 / (2 * np.pi**2) - return np.sqrt(np.dot(w, Ps * dlogk)) - - logk = np.arange(np.log(1e-5), np.log(20.0), 1.0 / 100) - ks = np.exp(logk) - py_sigma = get_sigma(ks, logk[1] - logk[0]) - self.assertAlmostEqual(py_sigma, truth, places=3) - # no interpolation - logk = np.log(k) - diffs = (logk[2:] - logk[:-2]) / 2 - ks = k[1:-1] - py_sigma2 = get_sigma(ks, diffs) - self.assertAlmostEqual(py_sigma2, truth, places=3) - self.assertTrue(abs(results.get_sigmaR(8)[-1] / truth - 1) < 1e-4) - self.assertTrue(abs(results.get_sigmaR(np.array([8]), z_indices=-1)[-1] / truth - 1) < 1e-4) - pars.set_matter_power(nonlinear=False, k_per_logint=0, kmax=1.2, redshifts=np.arange(0, 10, 2)) - results = camb.get_results(pars) - sigmas = results.get_sigmaR(np.arange(1, 20, 1), hubble_units=False, z_indices=None) - pars.Accuracy.AccuracyBoost = 2 - results = camb.get_results(pars) - sigmas2 = results.get_sigmaR(np.arange(1, 20, 1), hubble_units=False, z_indices=None) - self.assertTrue(np.all(np.abs(sigmas / sigmas2 - 1) < 1e-3)) - pars.Accuracy.AccuracyBoost = 1 - pars.set_matter_power(nonlinear=False, k_per_logint=100, kmax=10, redshifts=np.arange(0, 10, 2)) - results = camb.get_results(pars) - sigmas2 = results.get_sigmaR(np.arange(1, 20, 1), hubble_units=False, z_indices=None) - self.assertAlmostEqual(sigmas2[4, 2], 1.77346, places=3) - self.assertTrue(np.all(np.abs(sigmas[:, 1:] / sigmas2[:, 1:] - 1) < 1e-3)) - self.assertTrue(np.all(np.abs(sigmas[:, 0] / sigmas2[:, 0] - 1) < 2e-3)) - - def testTimeTransfers(self): - from camb import initialpower - - pars = camb.set_params(H0=69, YHe=0.22, lmax=2000, lens_potential_accuracy=1, ns=0.96, As=2.5e-9) - results1 = camb.get_results(pars) - cl1 = results1.get_total_cls() - - pars = camb.set_params(H0=69, YHe=0.22, lmax=2000, lens_potential_accuracy=1) - results = camb.get_transfer_functions(pars, only_time_sources=True) - inflation_params = initialpower.InitialPowerLaw() - inflation_params.set_params(ns=0.96, As=2.5e-9) - results.power_spectra_from_transfer(inflation_params) - cl2 = results.get_total_cls() - np.testing.assert_allclose(cl1, cl2, rtol=1e-4) - inflation_params.set_params(ns=0.96, As=1.9e-9) - results.power_spectra_from_transfer(inflation_params) - inflation_params.set_params(ns=0.96, As=2.5e-9) - results.power_spectra_from_transfer(inflation_params) - cl2 = results.get_total_cls() - np.testing.assert_allclose(cl1, cl2, rtol=1e-4) - - pars = camb.CAMBparams() - pars.set_cosmology(H0=78, YHe=0.22) - pars.set_for_lmax(2000, lens_potential_accuracy=1) - pars.WantTensors = True - results = camb.get_transfer_functions(pars, only_time_sources=True) - cls = [] - for r in [0, 0.2, 0.4]: - inflation_params = initialpower.InitialPowerLaw() - inflation_params.set_params(ns=0.96, r=r, nt=0) - results.power_spectra_from_transfer(inflation_params) - cls += [results.get_total_cls(CMB_unit="muK")] - self.assertTrue(np.allclose((cls[1] - cls[0])[2:300, 2] * 2, (cls[2] - cls[0])[2:300, 2], rtol=1e-3)) - - def testDarkEnergy(self): - pars = camb.CAMBparams() - pars.set_cosmology(H0=71) - pars.InitPower.set_params(ns=0.965, r=0) - for m in ["fluid", "ppf"]: - pars.set_dark_energy(w=-0.7, wa=0.2, dark_energy_model=m) - C1 = camb.get_results(pars).get_cmb_power_spectra() - a = np.logspace(-5, 0, 1000) - w = -0.7 + 0.2 * (1 - a) - pars2 = pars.copy() - pars2.set_dark_energy_w_a(a, w, dark_energy_model=m) - C2 = camb.get_results(pars2).get_cmb_power_spectra() - for f in ["lens_potential", "lensed_scalar"]: - self.assertTrue(np.allclose(C1[f][2:, 0], C2[f][2:, 0])) - pars3 = pars2.copy() - self.assertAlmostEqual(-0.7, pars3.DarkEnergy.w) - - def testInitialPower(self): - pars = camb.CAMBparams() - pars.set_cosmology(H0=67) - import ctypes - - P = camb.InitialPowerLaw() - P2 = ctypes.pointer(P) - self.assertEqual(P.As, pars.InitPower.As) - As = 1.8e-9 - ns = 0.8 - P.set_params(As=As, ns=ns) - self.assertEqual(P.As, As) - self.assertEqual(P2.contents.As, As) - - pars2 = camb.CAMBparams() - pars2.set_cosmology(H0=67) - pars2.InitPower.set_params(As=1.7e-9, ns=ns) - self.assertEqual(pars2.InitPower.As, 1.7e-9) - pars.set_initial_power(pars2.InitPower) - self.assertEqual(pars.InitPower.As, 1.7e-9) - pars.set_initial_power(P) - self.assertEqual(pars.InitPower.As, As) - - ks = np.logspace(-5.5, 2, 1000) - pk = (ks / P.pivot_scalar) ** (ns - 1) * As - pars2.set_initial_power_table(ks, pk) - self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) - sp = camb.SplinedInitialPower(ks=ks, PK=pk) - pars2.set_initial_power(sp) - self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) - self.assertFalse(sp.has_tensors()) - self.assertFalse(pars2.InitPower.has_tensors()) - - sp = camb.SplinedInitialPower() - sp.set_scalar_log_regular(10 ** (-5.5), 10.0**2, pk) - pars2.set_initial_power(sp) - self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) - - sp.set_tensor_log_regular(10 ** (-5.5), 10.0**2, pk) - pars2.set_initial_power(sp) - self.assertAlmostEqual(pars2.tensor_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) - self.assertTrue(sp.has_tensors()) - sp.set_tensor_table([], []) - self.assertFalse(sp.has_tensors()) - pars2.set_initial_power(sp) - - results = camb.get_results(pars2) - cl = results.get_lensed_scalar_cls(CMB_unit="muK") - pars.InitPower.set_params(As=As, ns=ns) - results2 = camb.get_results(pars) - cl2 = results2.get_lensed_scalar_cls(CMB_unit="muK") - self.assertTrue(np.allclose(cl, cl2, rtol=1e-4)) - P = camb.InitialPowerLaw(As=2.1e-9, ns=0.9) - pars2.set_initial_power(P) - pars.InitPower.set_params(As=2.1e-9, ns=0.9) - self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) - - def PK(k, A, n): - return A * (k / 0.05) ** (n - 1) * (1 + 0.1 * np.sin(10 * k)) - - pars.set_initial_power_function(PK, args=(3e-9, 0.95)) - P = pars.scalar_power(ks) - np.testing.assert_almost_equal(P, PK(ks, 3e-9, 0.95), decimal=4) - - # noinspection PyTypeChecker - def testSources(self): - from camb.sources import GaussianSourceWindow, SplinedSourceWindow - - pars = camb.CAMBparams() - pars.set_cosmology(H0=64, mnu=0) - pars.set_for_lmax(1200) - pars.Want_CMB = False - pars.SourceWindows = [ - GaussianSourceWindow(redshift=0.17, source_type="counts", bias=1.2, sigma=0.04, dlog10Ndm=-0.2), - GaussianSourceWindow(redshift=0.5, source_type="lensing", sigma=0.07, dlog10Ndm=0), - ] - pars.SourceTerms.limber_windows = True - results = camb.get_results(pars) - cls = results.get_source_cls_dict() - zs = np.arange(0, 0.5, 0.02) - W = np.exp(-((zs - 0.17) ** 2) / 2 / 0.04**2) / np.sqrt(2 * np.pi) / 0.04 - - ks = np.logspace(-4, 3, 50) - bias_kz = 1.2 * np.ones((len(ks), len(zs))) - test_windows = [ - SplinedSourceWindow(bias=1.2, dlog10Ndm=-0.2, z=zs, W=W), - SplinedSourceWindow(bias_z=1.2 * np.ones_like(zs), dlog10Ndm=-0.2, z=zs, W=W), - SplinedSourceWindow(k_bias=ks, bias_kz=bias_kz, dlog10Ndm=-0.2, z=zs, W=W), - ] - for window in test_windows: - pars.SourceWindows[0] = window - results = camb.get_results(pars) - cls2 = results.get_source_cls_dict() - self.assertTrue(np.allclose(cls2["W1xW1"][2:1200], cls["W1xW1"][2:1200], rtol=1e-3)) - - pars.SourceWindows = [GaussianSourceWindow(redshift=1089, source_type="lensing", sigma=30)] - results = camb.get_results(pars) - cls = results.get_source_cls_dict() - PP = cls["PxP"] - ls = np.arange(0, PP.shape[0]) - self.assertTrue(np.allclose(PP / 4 * (ls * (ls + 1)), cls["W1xW1"], rtol=1e-3)) - self.assertTrue(np.allclose(PP / 2 * np.sqrt(ls * (ls + 1)), cls["PxW1"], rtol=1e-3)) - # test something sharp with redshift distortions (tricky..) - from scipy import signal - - zs = np.arange(1.9689, 2.1057, (2.1057 - 1.9689) / 2000) - W = signal.windows.tukey(len(zs), alpha=0.1) - pars = camb.CAMBparams() - pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) - pars.InitPower.set_params(As=2e-9, ns=0.965) - pars.set_for_lmax(4000) - pars.SourceWindows = [SplinedSourceWindow(z=zs, W=W, source_type="counts")] - pars.SourceTerms.counts_redshift = True - results = camb.get_results(pars) - cls = results.get_source_cls_dict() - self.assertAlmostEqual(np.sum(cls["PxW1"][10:3000:20]), 0.00020001, places=5) - self.assertAlmostEqual(np.sum(cls["W1xW1"][10:3000:20]), 2.26413, places=3) - self.assertAlmostEqual(np.sum(cls["W1xW1"][10]), 0.0001097, places=6) - - def testSymbolic(self): - if fast: - return - import camb.symbolic as s - - monopole_source, ISW, doppler, quadrupole_source = s.get_scalar_temperature_sources() - temp_source = monopole_source + ISW + doppler + quadrupole_source - - pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, omk=0.1) - data = camb.get_background(pars) - tau = np.linspace(1, 1200, 300) - ks = [0.001, 0.05, 1] - monopole2 = s.make_frame_invariant(s.newtonian_gauge(monopole_source), "Newtonian") - Delta_c_N = s.make_frame_invariant(s.Delta_c, "Newtonian") - Delta_c_N2 = s.make_frame_invariant(s.synchronous_gauge(Delta_c_N), "CDM") - ev = data.get_time_evolution( - ks, - tau, - ["delta_photon", s.Delta_g, Delta_c_N, Delta_c_N2, monopole_source, monopole2, temp_source, "T_source"], - ) - self.assertTrue(np.allclose(ev[:, :, 0], ev[:, :, 1])) - self.assertTrue(np.allclose(ev[:, :, 2], ev[:, :, 3])) - self.assertTrue(np.allclose(ev[:, :, 4], ev[:, :, 5])) - self.assertTrue(np.allclose(ev[:, :, 6], ev[:, :, 7])) - - pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95) - pars.set_accuracy(lSampleBoost=2) - try: - pars.set_custom_scalar_sources( - [monopole_source + ISW + doppler + quadrupole_source, s.scalar_E_source], - source_names=["T2", "E2"], - source_ell_scales={"E2": 2}, - ) - data = camb.get_results(pars) - dic = data.get_cmb_unlensed_scalar_array_dict(CMB_unit="muK") - self.assertTrue(np.all(np.abs(dic["T2xT2"][2:2000] / dic["TxT"][2:2000] - 1) < 1e-3)) - self.assertTrue(np.all(np.abs(dic["TxT2"][2:2000] / dic["TxT"][2:2000] - 1) < 1e-3)) - # default interpolation errors much worse for E - self.assertTrue(np.all(np.abs(dic["E2xE2"][10:2000] / dic["ExE"][10:2000] - 1) < 2e-3)) - self.assertTrue(np.all(np.abs(dic["E2xE"][10:2000] / dic["ExE"][10:2000] - 1) < 2e-3)) - dic1 = data.get_cmb_power_spectra(CMB_unit="muK") - self.assertTrue(np.allclose(dic1["unlensed_scalar"][2:2000, 1], dic["ExE"][2:2000])) - finally: - pars.set_accuracy(lSampleBoost=1) - - s.internal_consistency_checks() - - def test_mathutils(self): - from camb.mathutils import chi_squared, pcl_coupling_matrix, scalar_coupling_matrix, threej_coupling - - cinv = np.linalg.inv(np.array([[1.2, 3], [3, 18.2]])) - vec = np.array([0.5, 5.0]) - self.assertAlmostEqual(chi_squared(cinv, vec), cinv.dot(vec).dot(vec)) - W = np.zeros(100) - W[0] = 1 - lmax = len(W) - Xi = threej_coupling(W, lmax) - np.testing.assert_allclose(np.diag(Xi) * (2 * np.arange(lmax + 1) + 1), np.ones(lmax + 1)) - Xis = threej_coupling(W, lmax, pol=True) - np.testing.assert_allclose(np.diag(Xis[0]) * (2 * np.arange(lmax + 1) + 1), np.ones(lmax + 1)) - P = W * 4 * np.pi - M = scalar_coupling_matrix(P, lmax) - np.testing.assert_allclose(M, np.eye(lmax + 1)) - M = pcl_coupling_matrix(P, lmax) - np.testing.assert_allclose(M, np.eye(lmax + 1)) - - def test_extra_EmissionAnglePostBorn(self): - if fast: - return - from camb import emission_angle, postborn - - pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, tau=0.055) - BB = emission_angle.get_emission_delay_BB(pars, lmax=3500) - self.assertAlmostEqual(BB(80) * 2 * np.pi / 80 / 81.0, 1.1e-10, delta=1e-11) # type: ignore - - Bom = postborn.get_field_rotation_BB(pars, lmax=3500) - self.assertAlmostEqual(Bom(100) * 2 * np.pi / 100 / 101.0, 1.65e-11, delta=1e-12) # type: ignore - - def test_memory(self): - if platform.system() != "Windows": - import gc - import resource - - last_usage = -1 - for i in range(3): - pars = camb.CAMBparams() - pars.set_cosmology(H0=70, ombh2=0.022, omch2=0.12, mnu=0.06, omk=0, tau=0.17) - results = camb.get_results(pars) - del pars, results - gc.collect() - usage = round(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0, 1) - if 0 < last_usage != usage: - print(f"Memory usage: {usage:2.2f} KB vs {last_usage:2.2f} KB") - raise Exception("Apparent memory leak") - last_usage = usage - - camb.free_global_memory() - - def test_quintessence(self): - n = 3 - # set zc and fde_zc - pars = camb.set_params( - ombh2=0.022, - omch2=0.122, - thetastar=0.01044341764253, - dark_energy_model="EarlyQuintessence", - m=8e-53, - f=0.05, - n=n, - theta_i=3.1, - use_zc=True, - zc=1e4, - fde_zc=0.1, - ) - camb.get_background(pars) - results = camb.get_results(pars) - self.assertAlmostEqual(results.get_derived_params()["thetastar"], 1.044341764253, delta=1e-5) +import os +import pickle +import platform +import subprocess +import sys +import tempfile +import unittest + +import numpy as np + +try: + import camb +except ImportError: + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + import camb +from camb import bbn, correlations, dark_energy, initialpower, model, recombination +from camb.baseconfig import CAMBError, CAMBParamRangeError, CAMBValueError + +fast = "ci fast" in os.getenv("GITHUB_ACTIONS", "") + + +class CambTest(unittest.TestCase): + def testAssigments(self): + ini = os.path.join(os.path.dirname(__file__), "..", "inifiles", "planck_2018.ini") + if os.path.exists(ini): + pars = camb.read_ini(ini) + self.assertTrue(np.abs(camb.get_background(pars).cosmomc_theta() * 100 / 1.040909 - 1) < 2e-5) + pars = camb.CAMBparams() + pars.set_cosmology(H0=68.5, ombh2=0.022, mnu=0, omch2=0.1) + self.assertAlmostEqual(pars.omegam, (0.022 + 0.1) / 0.685**2) + with self.assertRaises(AttributeError): + # noinspection PyPropertyAccess + pars.omegam = 1 + pars.InitPower.set_params(ns=0.01) + data = camb.CAMBdata() + data.Params = pars + self.assertEqual(data.Params.InitPower.ns, pars.InitPower.ns) + d = dark_energy.DarkEnergyFluid(w=-0.95) + pars.DarkEnergy = d + self.assertEqual(pars.DarkEnergy.w, -0.95) + pars.DarkEnergy = dark_energy.AxionEffectiveFluid(w_n=0.4) + data.Params = pars + self.assertEqual(pars.DarkEnergy.w_n, 0.4) + pars.z_outputs = [0.1, 0.4] + self.assertEqual(pars.z_outputs[1], 0.4) + pars.z_outputs[0] = 0.3 + self.assertEqual(pars.z_outputs[0], 0.3) + pars.z_outputs = pars.z_outputs + pars.z_outputs = [] + pars.z_outputs = None + # noinspection PyTypeChecker + self.assertFalse(len(pars.z_outputs)) + with self.assertRaises(TypeError): + pars.DarkEnergy = initialpower.InitialPowerLaw() + pars.NonLinear = model.NonLinear_both + printstr = str(pars) + self.assertTrue("Want_CMB_lensing = True" in printstr and "NonLinear = NonLinear_both" in printstr) + pars.NonLinear = model.NonLinear_lens + self.assertTrue(pars.NonLinear == model.NonLinear_lens) + with self.assertRaises(ValueError): + pars.NonLinear = 4 + pars.nu_mass_degeneracies = np.zeros(3) + self.assertTrue(len(pars.nu_mass_degeneracies) == 3) + pars.nu_mass_degeneracies = [1, 2, 3] + self.assertTrue(pars.nu_mass_degeneracies[1] == 2) + pars.nu_mass_degeneracies[1] = 5 + self.assertTrue(pars.nu_mass_degeneracies[1] == 5) + with self.assertRaises(CAMBParamRangeError): + pars.nu_mass_degeneracies = np.zeros(7) + pars.nu_mass_eigenstates = 0 + self.assertFalse(len(pars.nu_mass_degeneracies[:1])) + pars = camb.set_params(**{"InitPower.ns": 1.2, "WantTransfer": True}) + self.assertEqual(pars.InitPower.ns, 1.2) + self.assertTrue(pars.WantTransfer) + pars.DarkEnergy = None + pars = camb.set_params(**{"H0": 67, "ombh2": 0.002, "r": 0.1, "Accuracy.AccurateBB": True}) + self.assertEqual(pars.Accuracy.AccurateBB, True) + + from camb.sources import GaussianSourceWindow + + pars = camb.CAMBparams() + pars.SourceWindows = [GaussianSourceWindow(), GaussianSourceWindow(redshift=1)] + self.assertEqual(pars.SourceWindows[1].redshift, 1) + pars.SourceWindows[0].redshift = 2 + self.assertEqual(pars.SourceWindows[0].redshift, 2) + self.assertTrue(len(pars.SourceWindows) == 2) + pars.SourceWindows[0] = GaussianSourceWindow(redshift=3) + self.assertEqual(pars.SourceWindows[0].redshift, 3) + self.assertTrue("redshift = 3.0" in str(pars)) + pars.SourceWindows = pars.SourceWindows[0:1] + self.assertTrue(len(pars.SourceWindows) == 1) + pars.SourceWindows = [] + self.assertTrue(len(pars.SourceWindows) == 0) + params = camb.get_valid_numerical_params() + self.assertEqual( + params, + { + "ombh2", + "deltazrei", + "omnuh2", + "tau", + "omk", + "zrei", + "thetastar", + "nrunrun", + "meffsterile", + "nnu", + "ntrun", + "HMCode_A_baryon", + "HMCode_eta_baryon", + "HMCode_logT_AGN", + "cosmomc_theta", + "YHe", + "wa", + "cs2", + "H0", + "mnu", + "Alens", + "TCMB", + "ns", + "nrun", + "As", + "nt", + "r", + "w", + "omch2", + "max_zrei", + "wde_a_array", + "wde_w_array", + }, + ) + params2 = camb.get_valid_numerical_params(dark_energy_model="AxionEffectiveFluid") + self.assertEqual(params2.difference(params), {"fde_zc", "w_n", "zc", "theta_i"}) + pars = camb.set_params( + H0=67, + ombh2=0.022, + omch2=0.12, + dark_energy_model="AxionEffectiveFluid", + w_n=0.4, + fde_zc=0.05, + zc=4000, + ) + self.assertIsInstance(pars.DarkEnergy, dark_energy.AxionEffectiveFluid) + self.assertEqual(pars.DarkEnergy.w_n, 0.4) + self.assertEqual(pars.DarkEnergy.fde_zc, 0.05) + self.assertEqual(pars.DarkEnergy.zc, 4000) + + def testWriteIniRoundTrip(self): + script = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "fortran", "tests", "CAMB_test_files.py") + ) + script_dir = os.path.dirname(script) + repo_root = os.path.abspath(os.path.join(script_dir, "..", "..")) + fortran_dir = os.path.join(repo_root, "fortran") + base_settings = os.path.join(repo_root, "inifiles", "params.ini") + + with tempfile.TemporaryDirectory() as ini_dir: + subprocess.run( + [ + sys.executable, + script, + ini_dir, + "--make_ini", + "--no_run_test", + "--base_settings", + base_settings, + ], + check=True, + cwd=fortran_dir, + ) + ini_files = sorted( + os.path.join(ini_dir, filename) + for filename in os.listdir(ini_dir) + if filename.endswith(".ini") and filename.startswith("params_") + ) + self.assertTrue(ini_files) + + cwd = os.getcwd() + os.chdir(fortran_dir) + try: + for ini_file in ini_files: + with self.subTest(ini=os.path.basename(ini_file)): + pars = camb.read_ini(ini_file) + written_ini = os.path.join(ini_dir, "written_" + os.path.basename(ini_file)) + camb.write_ini(pars, written_ini) + self.assertTrue(os.path.exists(written_ini)) + finally: + os.chdir(cwd) + + def testWriteIniFromPythonParams(self): + pars = camb.CAMBparams() + pars.set_cosmology(H0=67, ombh2=0.0224, omch2=0.119, tau=0.054, mnu=0.06) + pars.set_dark_energy(w=-0.95, wa=0.15, dark_energy_model="ppf") + pars.InitPower.set_params(As=2.1e-9, ns=0.965, nrun=0.01, r=0.03, nt=0.0) + pars.set_matter_power(redshifts=[0.0, 0.5, 1.0], kmax=2.0, accurate_massive_neutrino_transfers=True) + pars.set_for_lmax(2200, lens_potential_accuracy=1) + pars.WantTensors = True + pars.NonLinearModel.set_params(halofit_version="mead2020_feedback", HMCode_logT_AGN=7.7) + pars.Alens = 0.95 + + with tempfile.TemporaryDirectory() as temp_dir: + ini_file = os.path.join(temp_dir, "python_params.ini") + pars.write_ini(ini_file) + self.assertTrue(os.path.exists(ini_file)) + + def testBackground(self): + pars = camb.CAMBparams() + pars.set_cosmology(H0=68.5, ombh2=0.022, omch2=0.122, YHe=0.2453, mnu=0.07, omk=0) + zre = camb.get_zre_from_tau(pars, 0.06) + age = camb.get_age(pars) + self.assertAlmostEqual(zre, 8.39, 2) + self.assertAlmostEqual(age, 13.65, 2) + + data = camb.CAMBdata() + bao = data.get_BAO([0.57, 0.27], pars) + + data = camb.CAMBdata() + data.calc_background(pars) + + DA = data.angular_diameter_distance(0.57) + H = data.hubble_parameter(0.27) + self.assertAlmostEqual(DA, bao[0][2], 3) + self.assertAlmostEqual(H, bao[1][1], 3) + + age2 = data.physical_time(0) + self.assertAlmostEqual(age, age2, 4) + + data.comoving_radial_distance(0.48) + t0 = data.conformal_time(0) + self.assertAlmostEqual(t0, data.tau0) + t1 = data.conformal_time(11.5) + t2 = data.comoving_radial_distance(11.5) + self.assertAlmostEqual(t2, t0 - t1, 2) + self.assertAlmostEqual(t1, 4200.809, 2) + chistar = data.conformal_time(0) - data.tau_maxvis + chis = np.linspace(0, chistar, 197) + zs = data.redshift_at_comoving_radial_distance(chis) + chitest = data.comoving_radial_distance(zs) + self.assertTrue(np.sum((chitest - chis) ** 2) < 1e-3) + + theta = data.cosmomc_theta() + self.assertAlmostEqual(theta, 0.0104759965, 5) + + derived = data.get_derived_params() + self.assertAlmostEqual(derived["age"], age, 2) + self.assertAlmostEqual(derived["rdrag"], 146.986, 2) + self.assertAlmostEqual(derived["rstar"], data.sound_horizon(derived["zstar"]), 2) + + # Test BBN consistency, base_plikHM_TT_lowTEB best fit model + pars.set_cosmology(H0=67.31, ombh2=0.022242, omch2=0.11977, mnu=0.06, omk=0) + self.assertAlmostEqual(pars.YHe, 0.2458, 5) + data.calc_background(pars) + self.assertAlmostEqual(data.cosmomc_theta(), 0.0104090741, 7) + self.assertAlmostEqual(data.get_derived_params()["kd"], 0.14055, 4) + + pars.set_cosmology( + H0=67.31, ombh2=0.022242, omch2=0.11977, mnu=0.06, omk=0, bbn_predictor=bbn.BBN_table_interpolator() + ) + self.assertAlmostEqual(pars.YHe, 0.2458, 5) + self.assertAlmostEqual(pars.get_Y_p(), bbn.BBN_table_interpolator().Y_p(0.022242, 0), 5) + + # test massive sterile models as in Planck papers + pars.set_cosmology(H0=68.0, ombh2=0.022305, omch2=0.11873, mnu=0.06, nnu=3.073, omk=0, meffsterile=0.013) + self.assertAlmostEqual(pars.omnuh2, 0.00078, 5) + self.assertAlmostEqual(pars.YHe, 0.246218, 5) + self.assertAlmostEqual(pars.N_eff, 3.073, 4) + + data.calc_background(pars) + self.assertAlmostEqual(data.get_derived_params()["age"], 13.773, 2) + self.assertAlmostEqual(data.cosmomc_theta(), 0.0104103, 6) + + # test dark energy + pars.set_cosmology(H0=68.26, ombh2=0.022271, omch2=0.11914, mnu=0.06, omk=0) + pars.set_dark_energy(w=-1.0226, dark_energy_model="fluid") + + data.calc_background(pars) + self.assertAlmostEqual(data.get_derived_params()["age"], 13.789, 2) + scal = data.luminosity_distance(1.4) + vec = data.luminosity_distance([1.2, 1.4, 0.1, 1.9]) + self.assertAlmostEqual(scal, vec[1], 5) + + pars.set_dark_energy() # re-set defaults + + # test theta + pars.set_cosmology(cosmomc_theta=0.0104085, ombh2=0.022271, omch2=0.11914, mnu=0.06, omk=0) + self.assertAlmostEqual(pars.H0, 67.537, 2) + with self.assertRaises(CAMBParamRangeError): + pars.set_cosmology(cosmomc_theta=0.0204085, ombh2=0.022271, omch2=0.11914, mnu=0.06, omk=0) + pars = camb.set_params(cosmomc_theta=0.0104077, ombh2=0.022, omch2=0.122, w=-0.95) + self.assertAlmostEqual(camb.get_background(pars, no_thermo=True).cosmomc_theta(), 0.0104077, 7) + + pars = camb.set_params(thetastar=0.010311, ombh2=0.022, omch2=0.122) + self.assertAlmostEqual(camb.get_background(pars).get_derived_params()["thetastar"] / 100, 0.010311, 7) + pars = camb.set_params(thetastar=0.010311, ombh2=0.022, omch2=0.122, omk=-0.05) + self.assertAlmostEqual(camb.get_background(pars).get_derived_params()["thetastar"] / 100, 0.010311, 7) + self.assertAlmostEqual(pars.H0, 49.70624, places=3) + + pars = camb.set_params( + cosmomc_theta=0.0104077, ombh2=0.022, omch2=0.122, w=-0.95, wa=0, dark_energy_model="ppf" + ) + self.assertAlmostEqual(camb.get_background(pars, no_thermo=True).cosmomc_theta(), 0.0104077, 7) + + pars = camb.set_params( + cosmomc_theta=0.0104077, + ombh2=0.022, + omch2=0.122, + w=-0.95, + dark_energy_model="DarkEnergyFluid", + initial_power_model="InitialPowerLaw", + ) + self.assertAlmostEqual(camb.get_background(pars, no_thermo=True).cosmomc_theta(), 0.0104077, 7) + + with self.assertRaises(CAMBValueError): + camb.set_params(dark_energy_model="InitialPowerLaw") + data.calc_background(pars) + h2 = (data.Params.H0 / 100) ** 2 + self.assertAlmostEqual(data.get_Omega("baryon"), data.Params.ombh2 / h2, 7) + self.assertAlmostEqual(data.get_Omega("nu"), data.Params.omnuh2 / h2, 7) + self.assertAlmostEqual( + data.get_Omega("photon") + + data.get_Omega("neutrino") + + data.get_Omega("de") + + (pars.ombh2 + pars.omch2 + pars.omnuh2) / h2 + + pars.omk, + 1, + 8, + ) + pars.set_cosmology(H0=67, mnu=1, neutrino_hierarchy="normal") + data.calc_background(pars) + h2 = (pars.H0 / 100) ** 2 + self.assertAlmostEqual( + data.get_Omega("photon") + + data.get_Omega("neutrino") + + data.get_Omega("de") + + (pars.ombh2 + pars.omch2 + pars.omnuh2) / h2 + + pars.omk, + 1, + 8, + ) + redshifts = np.array([0.005, 0.01, 0.3, 0.9342, 4, 27, 321.5, 932, 1049, 1092, 2580, 1e4, 2.1e7]) + self.assertTrue( + np.allclose(data.redshift_at_conformal_time(data.conformal_time(redshifts)), redshifts, rtol=1e-7) + ) + pars.set_dark_energy(w=-1.8) + data.calc_background(pars) + self.assertTrue( + np.allclose(data.redshift_at_conformal_time(data.conformal_time(redshifts)), redshifts, rtol=1e-7) + ) + pars.set_cosmology(cosmomc_theta=0.0104085) + data.calc_background(pars) + self.assertAlmostEqual(data.cosmomc_theta(), 0.0104085) + derived = data.get_derived_params() + pars.Accuracy.BackgroundTimeStepBoost = 2 + data.calc_background(pars) + derived2 = data.get_derived_params() + self.assertAlmostEqual(derived["thetastar"], derived2["thetastar"], places=5) + pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.11, neutrino_hierarchy="inverted") + self.assertEqual(pars.num_nu_massive, 3) + self.assertEqual(pars.nu_mass_numbers[1], 1) + self.assertEqual(pars.nu_mass_eigenstates, 2) + self.assertAlmostEqual(pars.nu_mass_fractions[0], 0.915197, places=4) + + pars = camb.CAMBparams() + pars.set_cosmology(H0=68.5, ombh2=0.022, omch2=0.122, YHe=0.2453, mnu=0.07, omk=0, zrei=zre) + results = camb.get_background(pars) + self.assertEqual(results.Params.Reion.redshift, zre) + + pars = camb.CAMBparams() + pars.set_cosmology(H0=68.5, ombh2=0.022, omch2=0.122, YHe=0.2453, mnu=0.07, omk=-0.05) + data = camb.get_background(pars) + delta2 = ( + data.curvature_radius + / (1 + 0.25) + * ( + np.sin( + (data.comoving_radial_distance(0.25) - data.comoving_radial_distance(0.05)) / data.curvature_radius + ) + ) + ) + np.testing.assert_allclose(delta2, data.angular_diameter_distance2(0.05, 0.25)) + dists = data.angular_diameter_distance2([0.3, 0.05, 0.25], [1, 0.25, 0.05]) + self.assertAlmostEqual(delta2, dists[1]) + self.assertEqual(0, dists[2]) + + self.assertEqual(data.physical_time(0.4), data.physical_time([0.2, 0.4])[1]) + d = data.conformal_time_a1_a2(0, 0.5) + data.conformal_time_a1_a2(0.5, 1) + self.assertAlmostEqual(d, data.conformal_time_a1_a2(0, 1)) + self.assertAlmostEqual(d, sum(data.conformal_time_a1_a2([0, 0.5], [0.5, 1]))) + + def testRecfastRosenbrockAgreement(self): + redshifts = np.geomspace(1.0, 3001.0, 400) - 1.0 + + def make_pars(nz, use_rosenbrock=False, handoff=0.985): + pars = camb.CAMBparams() + pars.set_cosmology(H0=67.4, ombh2=0.02237, omch2=0.12, mnu=0.06, tau=0.054, YHe=0.2453) + pars.InitPower.set_params(As=2.1e-9, ns=0.965) + rec = recombination.Recfast() + rec.Nz = nz + rec.use_rosenbrock = use_rosenbrock + rec.rosenbrock_handoff_xH = handoff + rec.rosenbrock_tol = 3e-4 + pars.Recomb = rec + return pars + + for nz, handoff in [(2048, 0.976), (10000, 0.985)]: + with self.subTest(nz=nz, handoff=handoff): + base = camb.get_background(make_pars(nz)) + ros = camb.get_background(make_pars(nz, use_rosenbrock=True, handoff=handoff)) + + base_hist = base.get_background_redshift_evolution(redshifts, vars=["x_e", "T_b"], format="array") + ros_hist = ros.get_background_redshift_evolution(redshifts, vars=["x_e", "T_b"], format="array") + + xe_denom = np.maximum(np.maximum(np.abs(base_hist[:, 0]), np.abs(ros_hist[:, 0])), 1e-12) + tb_denom = np.maximum(np.maximum(np.abs(base_hist[:, 1]), np.abs(ros_hist[:, 1])), 1e-12) + xe_rel = np.max(np.abs(base_hist[:, 0] - ros_hist[:, 0]) / xe_denom) + tb_rel = np.max(np.abs(base_hist[:, 1] - ros_hist[:, 1]) / tb_denom) + + base_derived = base.get_derived_params() + ros_derived = ros.get_derived_params() + theta_rel = abs(ros_derived["thetastar"] / base_derived["thetastar"] - 1.0) + zstar_rel = abs(ros_derived["zstar"] / base_derived["zstar"] - 1.0) + + self.assertLess(xe_rel, 1e-3) + self.assertLess(tb_rel, 1e-6) + self.assertLess(theta_rel, 1e-6) + self.assertLess(zstar_rel, 1e-6) + + nz = 2048 + delta_z = 1.0e4 / nz + nodes = 1.0e4 - np.arange(1, nz + 1, dtype=np.float64) * delta_z + base_nodes = camb.get_background(make_pars(nz)).get_background_redshift_evolution( + nodes, vars=["x_e"], format="array" + )[:, 0] + snap_index = np.flatnonzero(base_nodes < 0.976)[0] + upper_x = base_nodes[snap_index - 1] + lower_x = base_nodes[snap_index] + handoff_a = lower_x + 0.25 * (upper_x - lower_x) + handoff_b = lower_x + 0.75 * (upper_x - lower_x) + ros_a = camb.get_background(make_pars(nz, use_rosenbrock=True, handoff=handoff_a)) + ros_b = camb.get_background(make_pars(nz, use_rosenbrock=True, handoff=handoff_b)) + hist_a = ros_a.get_background_redshift_evolution(redshifts, vars=["x_e", "T_b"], format="array") + hist_b = ros_b.get_background_redshift_evolution(redshifts, vars=["x_e", "T_b"], format="array") + self.assertLess(np.max(np.abs(hist_a - hist_b)), 1e-15) + + with tempfile.TemporaryDirectory() as temp_dir: + ini_path = os.path.join(temp_dir, "recfast_rosenbrock.ini") + make_pars(2048, use_rosenbrock=True, handoff=0.985).write_ini(ini_path) + roundtrip = camb.read_ini(ini_path) + self.assertTrue(roundtrip.Recomb.use_rosenbrock) + self.assertEqual(roundtrip.Recomb.Nz, 2048) + self.assertAlmostEqual(roundtrip.Recomb.rosenbrock_handoff_xH, 0.985) + self.assertAlmostEqual(roundtrip.Recomb.rosenbrock_tol, 3e-4) + + def testErrors(self): + redshifts = np.logspace(-1, np.log10(1089)) + pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, redshifts=redshifts, kmax=0.1) + + results = camb.get_background(pars) + with self.assertRaises(CAMBError): + results.get_matter_power_interpolator() + + def testEvolution(self): + redshifts = [0.4, 31.5] + pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, redshifts=redshifts, kmax=0.1) + pars.WantCls = False + + # check transfer function routines and evolution code agree + # Note transfer function redshifts are re-sorted in outputs + data = camb.get_transfer_functions(pars) + mtrans = data.get_matter_transfer_data() + transfer_k = mtrans.transfer_z("delta_cdm", z_index=1) + transfer_k2 = mtrans.transfer_z("delta_baryon", z_index=0) + + kh = mtrans.transfer_z("k/h", z_index=1) + ev = data.get_redshift_evolution( + mtrans.q, redshifts, ["delta_baryon", "delta_cdm", "delta_photon"], lAccuracyBoost=1 + ) + self.assertTrue(np.all(np.abs(transfer_k * kh**2 * (pars.H0 / 100) ** 2 / ev[:, 0, 1] - 1) < 1e-3)) + ix = 1 + self.assertAlmostEqual(transfer_k2[ix] * kh[ix] ** 2 * (pars.H0 / 100) ** 2, ev[ix, 1, 0], 4) + + def testInstances(self): + pars = camb.set_params( + H0=69.1, ombh2=0.032, omch2=0.122, As=3e-9, ns=0.91, omk=0.013, redshifts=[0.0], kmax=0.5 + ) + data = camb.get_background(pars) + res1 = data.angular_diameter_distance(0.7) + drag1 = data.get_derived_params()["rdrag"] + pars2 = camb.set_params(H0=65, ombh2=0.022, omch2=0.122, As=3e-9, ns=0.91) + data2 = camb.get_background(pars2) + res2 = data2.angular_diameter_distance(1.7) + drag2 = data2.get_derived_params()["rdrag"] + self.assertAlmostEqual(res1, data.angular_diameter_distance(0.7)) + self.assertAlmostEqual(res2, data2.angular_diameter_distance(1.7)) + self.assertAlmostEqual(drag1, data.get_derived_params()["rdrag"]) + self.assertEqual(pars2.InitPower.ns, data2.Params.InitPower.ns) + data2.calc_background(pars) + self.assertEqual(pars.InitPower.ns, data2.Params.InitPower.ns) + self.assertAlmostEqual(res1, data2.angular_diameter_distance(0.7)) + data3 = camb.get_results(pars2) + cl3 = data3.get_lensed_scalar_cls(1000) + self.assertAlmostEqual(res2, data3.angular_diameter_distance(1.7)) + self.assertAlmostEqual(drag2, data3.get_derived_params()["rdrag"], places=3) + self.assertAlmostEqual(drag1, data.get_derived_params()["rdrag"], places=3) + pars.set_for_lmax(3000, lens_potential_accuracy=1) + camb.get_results(pars) + del data3 + data4 = camb.get_results(pars2) + cl4 = data4.get_lensed_scalar_cls(1000) + self.assertTrue(np.allclose(cl4, cl3)) + + def testPowers(self): + pars = camb.CAMBparams() + pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.07, omk=0) + pars.set_dark_energy() # re-set defaults + pars.InitPower.set_params(ns=0.965, As=2e-9) + pars.NonLinearModel.set_params(halofit_version="takahashi") + + self.assertAlmostEqual(pars.scalar_power(1), 1.801e-9, 4) + self.assertAlmostEqual(pars.scalar_power([1, 1.5])[0], 1.801e-9, 4) + + pars.set_matter_power(nonlinear=True) + self.assertEqual(pars.NonLinear, model.NonLinear_pk) + pars.set_matter_power(redshifts=[0.0, 0.17, 3.1], silent=True, nonlinear=False) + data = camb.get_results(pars) + + kh, z, pk = data.get_matter_power_spectrum(1e-4, 1, 20) + + kh2, z2, pk2 = data.get_linear_matter_power_spectrum() + + s8 = data.get_sigma8() + self.assertAlmostEqual(s8[0], 0.24686, 3) + self.assertAlmostEqual(s8[2], 0.80044, 3) + fs8 = data.get_fsigma8() + self.assertAlmostEqual(fs8[0], 0.2431, 3) + self.assertAlmostEqual(fs8[2], 0.424712, 3) + + pars.NonLinear = model.NonLinear_both + + data.calc_power_spectra(pars) + kh3, z3, pk3 = data.get_matter_power_spectrum(1e-4, 1, 20) + self.assertAlmostEqual(pk[-1][-3], 51.924, 2) + self.assertAlmostEqual(pk3[-1][-3], 57.723, 2) + self.assertAlmostEqual(pk2[-2][-4], 56.454, 2) + camb.set_feedback_level(0) + + PKnonlin = camb.get_matter_power_interpolator(pars, nonlinear=True) + pars.set_matter_power( + redshifts=[0, 0.09, 0.15, 0.42, 0.76, 1.5, 2.3, 5.5, 8.9], silent=True, kmax=10, k_per_logint=5 + ) + pars.NonLinear = model.NonLinear_both + results = camb.get_results(pars) + kh, z, pk = results.get_nonlinear_matter_power_spectrum() + pk_interp = PKnonlin.P(z, kh) + self.assertTrue(np.sum((pk / pk_interp - 1) ** 2) < 0.005) + PKnonlin2 = results.get_matter_power_interpolator(nonlinear=True, extrap_kmax=500) + pk_interp2 = PKnonlin2.P(z, kh) + self.assertTrue(np.sum((pk_interp / pk_interp2 - 1) ** 2) < 0.005) + + pars.NonLinearModel.set_params(halofit_version="mead") + _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) + self.assertAlmostEqual(pk[0][160], 814.9, delta=0.5) + + pars.NonLinearModel.set_params(halofit_version="mead2016") + _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) + self.assertAlmostEqual(pk[0][160], 814.9, delta=0.5) + + pars.NonLinearModel.set_params(halofit_version="mead2015") + _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) + self.assertAlmostEqual(pk[0][160], 791.3, delta=0.5) + + pars.NonLinearModel.set_params(halofit_version="mead2020") + _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) + self.assertAlmostEqual(pk[0][160], 815.8, delta=0.5) + + pars.NonLinearModel.set_params(halofit_version="mead2020_feedback") + _, _, pk = results.get_nonlinear_matter_power_spectrum(params=pars) + self.assertAlmostEqual(pk[0][160], 799.0, delta=0.5) + + lmax = 4000 + pars.set_for_lmax(lmax) + cls = data.get_cmb_power_spectra(pars) + data.get_total_cls(2000) + cls_unlensed = data.get_unlensed_scalar_cls(2500) + data.get_tensor_cls(2000) + cls_lensed = data.get_lensed_scalar_cls(3000) + data.get_lens_potential_cls(2000) + + cls_lensed2 = data.get_lensed_cls_with_spectrum(data.get_lens_potential_cls()[:, 0], lmax=3000) + np.testing.assert_allclose(cls_lensed2[2:, :], cls_lensed[2:, :], rtol=1e-4) + cls_lensed2 = data.get_partially_lensed_cls(1, lmax=3000) + np.testing.assert_allclose(cls_lensed2[2:, :], cls_lensed[2:, :], rtol=1e-4) + cls_lensed2 = data.get_partially_lensed_cls(0, lmax=2500) + np.testing.assert_allclose(cls_lensed2[2:, :], cls_unlensed[2:, :], rtol=1e-4) + + # check lensed CL against python; will only agree well for high lmax as python has no extrapolation template + cls_lensed2 = correlations.lensed_cls(cls["unlensed_scalar"], cls["lens_potential"][:, 0], delta_cls=False) + np.testing.assert_allclose(cls_lensed2[2:2000, 2], cls_lensed[2:2000, 2], rtol=1e-3) + np.testing.assert_allclose(cls_lensed2[2:2000, 1], cls_lensed[2:2000, 1], rtol=1e-3) + np.testing.assert_allclose(cls_lensed2[2:2000, 0], cls_lensed[2:2000, 0], rtol=1e-3) + self.assertTrue( + np.all( + np.abs( + (cls_lensed2[2:3000, 3] - cls_lensed[2:3000, 3]) + / np.sqrt(cls_lensed2[2:3000, 0] * cls_lensed2[2:3000, 1]) + ) + < 1e-4 + ) + ) + + corr, xvals, weights = correlations.gauss_legendre_correlation(cls["lensed_scalar"]) + clout = correlations.corr2cl(corr, xvals, weights, 2500) + self.assertTrue(np.all(np.abs(clout[2:2300, 2] / cls["lensed_scalar"][2:2300, 2] - 1) < 1e-3)) + + pars = camb.CAMBparams() + pars.set_cosmology(H0=78, YHe=0.22) + pars.set_for_lmax(2000, lens_potential_accuracy=1) + pars.WantTensors = True + results = camb.get_transfer_functions(pars) + from camb import initialpower + + cls = [] + for r in [0, 0.2, 0.4]: + inflation_params = initialpower.InitialPowerLaw() + inflation_params.set_params(ns=0.96, r=r, nt=0) + results.power_spectra_from_transfer(inflation_params, silent=True) + cls += [results.get_total_cls(CMB_unit="muK")] + self.assertTrue(np.allclose((cls[1] - cls[0])[2:300, 2] * 2, (cls[2] - cls[0])[2:300, 2], rtol=1e-3)) + + # Check generating tensors and scalars together + pars = camb.CAMBparams() + pars.set_cosmology(H0=67) + lmax = 2000 + pars.set_for_lmax(lmax, lens_potential_accuracy=1) + pars.InitPower.set_params(ns=0.96, r=0) + pars.WantTensors = False + results = camb.get_results(pars) + cl1 = results.get_total_cls(lmax, CMB_unit="muK") + pars.InitPower.set_params(ns=0.96, r=0.1, nt=0) + pars.WantTensors = True + results = camb.get_results(pars) + cl2 = results.get_lensed_scalar_cls(lmax, CMB_unit="muK") + ctensor2 = results.get_tensor_cls(lmax, CMB_unit="muK") + results = camb.get_transfer_functions(pars) + results.Params.InitPower.set_params(ns=1.1, r=1) + inflation_params = initialpower.InitialPowerLaw() + inflation_params.set_params(ns=0.96, r=0.05, nt=0) + results.power_spectra_from_transfer(inflation_params, silent=True) + cl3 = results.get_lensed_scalar_cls(lmax, CMB_unit="muK") + ctensor3 = results.get_tensor_cls(lmax, CMB_unit="muK") + self.assertTrue(np.allclose(ctensor2, ctensor3 * 2, rtol=1e-4)) + self.assertTrue(np.allclose(cl1, cl2, rtol=1e-4)) + # These are identical because all scalar spectra were identical (non-linear corrections change it otherwise) + self.assertTrue(np.allclose(cl1, cl3, rtol=1e-4)) + + pars = camb.CAMBparams() + pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.07, omk=0) + pars.set_for_lmax(2500) + pars.min_l = 2 + res = camb.get_results(pars) + cls = res.get_lensed_scalar_cls(2000) + pars.min_l = 1 + res = camb.get_results(pars) + cls2 = res.get_lensed_scalar_cls(2000) + np.testing.assert_allclose(cls[2:, 0:2], cls2[2:, 0:2], rtol=1e-4) + np.testing.assert_allclose(cls2[1, 0], 1.303942e-10, rtol=3e-3) + self.assertAlmostEqual(cls[1, 0], 0) + + def testSave(self): + pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, redshifts=[0.4, 31.5], kmax=0.1) + pars.set_dark_energy(w=-0.7, wa=0.2, dark_energy_model="ppf") + from camb.sources import GaussianSourceWindow + + pars.SourceWindows = [GaussianSourceWindow(), GaussianSourceWindow(redshift=1)] + s = repr(pars) + pars2 = eval(s) + assert repr(pars2) == s + assert "DarkEnergyPPF" in str(pars2) + b = pickle.dumps(pars) + pars2 = pickle.loads(b) + assert repr(pars2) == s + pars2.InitPower = initialpower.SplinedInitialPower() + with self.assertRaises(TypeError): + repr(pars2) + + def testSigmaR(self): + pars = camb.CAMBparams() + pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122, mnu=0.07, omk=0) + pars.InitPower.set_params(ns=0.965, As=2e-9) + pars.set_matter_power(nonlinear=False) + results = camb.get_results(pars) + sigma8 = results.get_sigma8_0() + self.assertAlmostEqual(sigma8, results.get_sigmaR(8)[-1], places=3) + self.assertAlmostEqual(sigma8, results.get_sigmaR(np.array([8]), z_indices=-1)[-1], places=3) + self.assertAlmostEqual(results.get_sigmaR(8)[-1], results.get_sigmaR(8, z_indices=-1)) + pars.set_matter_power(nonlinear=False, k_per_logint=0, kmax=2) + + results = camb.get_results(pars) + P, z, k = results.get_matter_power_interpolator( + nonlinear=False, hubble_units=False, k_hunit=False, return_z_k=True, extrap_kmax=100, silent=True + ) + truth = 0.800679 # from high kmax, high accuracy boost + self.assertTrue(abs(results.get_sigmaR(8)[-1] / sigma8 - 1) < 1e-3) + + def get_sigma(_ks, dlogk): + x = _ks * 8 / (pars.H0 / 100) + w = (3 * (np.sin(x) - x * np.cos(x)) / x**3) ** 2 + w[x < 1e-2] = 1 - x[x < 1e-2] ** 2 / 2 + Ps = P.P(0, _ks) * _ks**3 / (2 * np.pi**2) + return np.sqrt(np.dot(w, Ps * dlogk)) + + logk = np.arange(np.log(1e-5), np.log(20.0), 1.0 / 100) + ks = np.exp(logk) + py_sigma = get_sigma(ks, logk[1] - logk[0]) + self.assertAlmostEqual(py_sigma, truth, places=3) + # no interpolation + logk = np.log(k) + diffs = (logk[2:] - logk[:-2]) / 2 + ks = k[1:-1] + py_sigma2 = get_sigma(ks, diffs) + self.assertAlmostEqual(py_sigma2, truth, places=3) + self.assertTrue(abs(results.get_sigmaR(8)[-1] / truth - 1) < 1e-4) + self.assertTrue(abs(results.get_sigmaR(np.array([8]), z_indices=-1)[-1] / truth - 1) < 1e-4) + pars.set_matter_power(nonlinear=False, k_per_logint=0, kmax=1.2, redshifts=np.arange(0, 10, 2)) + results = camb.get_results(pars) + sigmas = results.get_sigmaR(np.arange(1, 20, 1), hubble_units=False, z_indices=None) + pars.Accuracy.AccuracyBoost = 2 + results = camb.get_results(pars) + sigmas2 = results.get_sigmaR(np.arange(1, 20, 1), hubble_units=False, z_indices=None) + self.assertTrue(np.all(np.abs(sigmas / sigmas2 - 1) < 1e-3)) + pars.Accuracy.AccuracyBoost = 1 + pars.set_matter_power(nonlinear=False, k_per_logint=100, kmax=10, redshifts=np.arange(0, 10, 2)) + results = camb.get_results(pars) + sigmas2 = results.get_sigmaR(np.arange(1, 20, 1), hubble_units=False, z_indices=None) + self.assertAlmostEqual(sigmas2[4, 2], 1.77346, places=3) + self.assertTrue(np.all(np.abs(sigmas[:, 1:] / sigmas2[:, 1:] - 1) < 1e-3)) + self.assertTrue(np.all(np.abs(sigmas[:, 0] / sigmas2[:, 0] - 1) < 2e-3)) + + def testTimeTransfers(self): + from camb import initialpower + + pars = camb.set_params(H0=69, YHe=0.22, lmax=2000, lens_potential_accuracy=1, ns=0.96, As=2.5e-9) + results1 = camb.get_results(pars) + cl1 = results1.get_total_cls() + + pars = camb.set_params(H0=69, YHe=0.22, lmax=2000, lens_potential_accuracy=1) + results = camb.get_transfer_functions(pars, only_time_sources=True) + inflation_params = initialpower.InitialPowerLaw() + inflation_params.set_params(ns=0.96, As=2.5e-9) + results.power_spectra_from_transfer(inflation_params) + cl2 = results.get_total_cls() + np.testing.assert_allclose(cl1, cl2, rtol=1e-4) + inflation_params.set_params(ns=0.96, As=1.9e-9) + results.power_spectra_from_transfer(inflation_params) + inflation_params.set_params(ns=0.96, As=2.5e-9) + results.power_spectra_from_transfer(inflation_params) + cl2 = results.get_total_cls() + np.testing.assert_allclose(cl1, cl2, rtol=1e-4) + + pars = camb.CAMBparams() + pars.set_cosmology(H0=78, YHe=0.22) + pars.set_for_lmax(2000, lens_potential_accuracy=1) + pars.WantTensors = True + results = camb.get_transfer_functions(pars, only_time_sources=True) + cls = [] + for r in [0, 0.2, 0.4]: + inflation_params = initialpower.InitialPowerLaw() + inflation_params.set_params(ns=0.96, r=r, nt=0) + results.power_spectra_from_transfer(inflation_params) + cls += [results.get_total_cls(CMB_unit="muK")] + self.assertTrue(np.allclose((cls[1] - cls[0])[2:300, 2] * 2, (cls[2] - cls[0])[2:300, 2], rtol=1e-3)) + + def testDarkEnergy(self): + pars = camb.CAMBparams() + pars.set_cosmology(H0=71) + pars.InitPower.set_params(ns=0.965, r=0) + for m in ["fluid", "ppf"]: + pars.set_dark_energy(w=-0.7, wa=0.2, dark_energy_model=m) + C1 = camb.get_results(pars).get_cmb_power_spectra() + a = np.logspace(-5, 0, 1000) + w = -0.7 + 0.2 * (1 - a) + pars2 = pars.copy() + pars2.set_dark_energy_w_a(a, w, dark_energy_model=m) + C2 = camb.get_results(pars2).get_cmb_power_spectra() + for f in ["lens_potential", "lensed_scalar"]: + self.assertTrue(np.allclose(C1[f][2:, 0], C2[f][2:, 0])) + pars3 = pars2.copy() + self.assertAlmostEqual(-0.7, pars3.DarkEnergy.w) + + def testInitialPower(self): + pars = camb.CAMBparams() + pars.set_cosmology(H0=67) + import ctypes + + P = camb.InitialPowerLaw() + P2 = ctypes.pointer(P) + self.assertEqual(P.As, pars.InitPower.As) + As = 1.8e-9 + ns = 0.8 + P.set_params(As=As, ns=ns) + self.assertEqual(P.As, As) + self.assertEqual(P2.contents.As, As) + + pars2 = camb.CAMBparams() + pars2.set_cosmology(H0=67) + pars2.InitPower.set_params(As=1.7e-9, ns=ns) + self.assertEqual(pars2.InitPower.As, 1.7e-9) + pars.set_initial_power(pars2.InitPower) + self.assertEqual(pars.InitPower.As, 1.7e-9) + pars.set_initial_power(P) + self.assertEqual(pars.InitPower.As, As) + + ks = np.logspace(-5.5, 2, 1000) + pk = (ks / P.pivot_scalar) ** (ns - 1) * As + pars2.set_initial_power_table(ks, pk) + self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) + sp = camb.SplinedInitialPower(ks=ks, PK=pk) + pars2.set_initial_power(sp) + self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) + self.assertFalse(sp.has_tensors()) + self.assertFalse(pars2.InitPower.has_tensors()) + + sp = camb.SplinedInitialPower() + sp.set_scalar_log_regular(10 ** (-5.5), 10.0**2, pk) + pars2.set_initial_power(sp) + self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) + + sp.set_tensor_log_regular(10 ** (-5.5), 10.0**2, pk) + pars2.set_initial_power(sp) + self.assertAlmostEqual(pars2.tensor_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) + self.assertTrue(sp.has_tensors()) + sp.set_tensor_table([], []) + self.assertFalse(sp.has_tensors()) + pars2.set_initial_power(sp) + + results = camb.get_results(pars2) + cl = results.get_lensed_scalar_cls(CMB_unit="muK") + pars.InitPower.set_params(As=As, ns=ns) + results2 = camb.get_results(pars) + cl2 = results2.get_lensed_scalar_cls(CMB_unit="muK") + self.assertTrue(np.allclose(cl, cl2, rtol=1e-4)) + P = camb.InitialPowerLaw(As=2.1e-9, ns=0.9) + pars2.set_initial_power(P) + pars.InitPower.set_params(As=2.1e-9, ns=0.9) + self.assertAlmostEqual(pars2.scalar_power(1.1), pars.scalar_power(1.1), delta=As * 1e-4) + + def PK(k, A, n): + return A * (k / 0.05) ** (n - 1) * (1 + 0.1 * np.sin(10 * k)) + + pars.set_initial_power_function(PK, args=(3e-9, 0.95)) + P = pars.scalar_power(ks) + np.testing.assert_almost_equal(P, PK(ks, 3e-9, 0.95), decimal=4) + + # noinspection PyTypeChecker + def testSources(self): + from camb.sources import GaussianSourceWindow, SplinedSourceWindow + + pars = camb.CAMBparams() + pars.set_cosmology(H0=64, mnu=0) + pars.set_for_lmax(1200) + pars.Want_CMB = False + pars.SourceWindows = [ + GaussianSourceWindow(redshift=0.17, source_type="counts", bias=1.2, sigma=0.04, dlog10Ndm=-0.2), + GaussianSourceWindow(redshift=0.5, source_type="lensing", sigma=0.07, dlog10Ndm=0), + ] + pars.SourceTerms.limber_windows = True + results = camb.get_results(pars) + cls = results.get_source_cls_dict() + zs = np.arange(0, 0.5, 0.02) + W = np.exp(-((zs - 0.17) ** 2) / 2 / 0.04**2) / np.sqrt(2 * np.pi) / 0.04 + + ks = np.logspace(-4, 3, 50) + bias_kz = 1.2 * np.ones((len(ks), len(zs))) + test_windows = [ + SplinedSourceWindow(bias=1.2, dlog10Ndm=-0.2, z=zs, W=W), + SplinedSourceWindow(bias_z=1.2 * np.ones_like(zs), dlog10Ndm=-0.2, z=zs, W=W), + SplinedSourceWindow(k_bias=ks, bias_kz=bias_kz, dlog10Ndm=-0.2, z=zs, W=W), + ] + for window in test_windows: + pars.SourceWindows[0] = window + results = camb.get_results(pars) + cls2 = results.get_source_cls_dict() + self.assertTrue(np.allclose(cls2["W1xW1"][2:1200], cls["W1xW1"][2:1200], rtol=1e-3)) + + pars.SourceWindows = [GaussianSourceWindow(redshift=1089, source_type="lensing", sigma=30)] + results = camb.get_results(pars) + cls = results.get_source_cls_dict() + PP = cls["PxP"] + ls = np.arange(0, PP.shape[0]) + self.assertTrue(np.allclose(PP / 4 * (ls * (ls + 1)), cls["W1xW1"], rtol=1e-3)) + self.assertTrue(np.allclose(PP / 2 * np.sqrt(ls * (ls + 1)), cls["PxW1"], rtol=1e-3)) + # test something sharp with redshift distortions (tricky..) + from scipy import signal + + zs = np.arange(1.9689, 2.1057, (2.1057 - 1.9689) / 2000) + W = signal.windows.tukey(len(zs), alpha=0.1) + pars = camb.CAMBparams() + pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.122) + pars.InitPower.set_params(As=2e-9, ns=0.965) + pars.set_for_lmax(4000) + pars.SourceWindows = [SplinedSourceWindow(z=zs, W=W, source_type="counts")] + pars.SourceTerms.counts_redshift = True + results = camb.get_results(pars) + cls = results.get_source_cls_dict() + self.assertAlmostEqual(np.sum(cls["PxW1"][10:3000:20]), 0.00020001, places=5) + self.assertAlmostEqual(np.sum(cls["W1xW1"][10:3000:20]), 2.26413, places=3) + self.assertAlmostEqual(np.sum(cls["W1xW1"][10]), 0.0001097, places=6) + + def testSymbolic(self): + if fast: + return + import camb.symbolic as s + + monopole_source, ISW, doppler, quadrupole_source = s.get_scalar_temperature_sources() + temp_source = monopole_source + ISW + doppler + quadrupole_source + + pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, omk=0.1) + data = camb.get_background(pars) + tau = np.linspace(1, 1200, 300) + ks = [0.001, 0.05, 1] + monopole2 = s.make_frame_invariant(s.newtonian_gauge(monopole_source), "Newtonian") + Delta_c_N = s.make_frame_invariant(s.Delta_c, "Newtonian") + Delta_c_N2 = s.make_frame_invariant(s.synchronous_gauge(Delta_c_N), "CDM") + ev = data.get_time_evolution( + ks, + tau, + ["delta_photon", s.Delta_g, Delta_c_N, Delta_c_N2, monopole_source, monopole2, temp_source, "T_source"], + ) + self.assertTrue(np.allclose(ev[:, :, 0], ev[:, :, 1])) + self.assertTrue(np.allclose(ev[:, :, 2], ev[:, :, 3])) + self.assertTrue(np.allclose(ev[:, :, 4], ev[:, :, 5])) + self.assertTrue(np.allclose(ev[:, :, 6], ev[:, :, 7])) + + pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95) + pars.set_accuracy(lSampleBoost=2) + try: + pars.set_custom_scalar_sources( + [monopole_source + ISW + doppler + quadrupole_source, s.scalar_E_source], + source_names=["T2", "E2"], + source_ell_scales={"E2": 2}, + ) + data = camb.get_results(pars) + dic = data.get_cmb_unlensed_scalar_array_dict(CMB_unit="muK") + self.assertTrue(np.all(np.abs(dic["T2xT2"][2:2000] / dic["TxT"][2:2000] - 1) < 1e-3)) + self.assertTrue(np.all(np.abs(dic["TxT2"][2:2000] / dic["TxT"][2:2000] - 1) < 1e-3)) + # default interpolation errors much worse for E + self.assertTrue(np.all(np.abs(dic["E2xE2"][10:2000] / dic["ExE"][10:2000] - 1) < 2e-3)) + self.assertTrue(np.all(np.abs(dic["E2xE"][10:2000] / dic["ExE"][10:2000] - 1) < 2e-3)) + dic1 = data.get_cmb_power_spectra(CMB_unit="muK") + self.assertTrue(np.allclose(dic1["unlensed_scalar"][2:2000, 1], dic["ExE"][2:2000])) + finally: + pars.set_accuracy(lSampleBoost=1) + + s.internal_consistency_checks() + + def test_mathutils(self): + from camb.mathutils import chi_squared, pcl_coupling_matrix, scalar_coupling_matrix, threej_coupling + + cinv = np.linalg.inv(np.array([[1.2, 3], [3, 18.2]])) + vec = np.array([0.5, 5.0]) + self.assertAlmostEqual(chi_squared(cinv, vec), cinv.dot(vec).dot(vec)) + W = np.zeros(100) + W[0] = 1 + lmax = len(W) + Xi = threej_coupling(W, lmax) + np.testing.assert_allclose(np.diag(Xi) * (2 * np.arange(lmax + 1) + 1), np.ones(lmax + 1)) + Xis = threej_coupling(W, lmax, pol=True) + np.testing.assert_allclose(np.diag(Xis[0]) * (2 * np.arange(lmax + 1) + 1), np.ones(lmax + 1)) + P = W * 4 * np.pi + M = scalar_coupling_matrix(P, lmax) + np.testing.assert_allclose(M, np.eye(lmax + 1)) + M = pcl_coupling_matrix(P, lmax) + np.testing.assert_allclose(M, np.eye(lmax + 1)) + + def test_extra_EmissionAnglePostBorn(self): + if fast: + return + from camb import emission_angle, postborn + + pars = camb.set_params(H0=67.5, ombh2=0.022, omch2=0.122, As=2e-9, ns=0.95, tau=0.055) + BB = emission_angle.get_emission_delay_BB(pars, lmax=3500) + self.assertAlmostEqual(BB(80) * 2 * np.pi / 80 / 81.0, 1.1e-10, delta=1e-11) # type: ignore + + Bom = postborn.get_field_rotation_BB(pars, lmax=3500) + self.assertAlmostEqual(Bom(100) * 2 * np.pi / 100 / 101.0, 1.65e-11, delta=1e-12) # type: ignore + + def test_memory(self): + if platform.system() != "Windows": + import gc + import resource + + last_usage = -1 + for i in range(3): + pars = camb.CAMBparams() + pars.set_cosmology(H0=70, ombh2=0.022, omch2=0.12, mnu=0.06, omk=0, tau=0.17) + results = camb.get_results(pars) + del pars, results + gc.collect() + usage = round(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0, 1) + if 0 < last_usage != usage: + print(f"Memory usage: {usage:2.2f} KB vs {last_usage:2.2f} KB") + raise Exception("Apparent memory leak") + last_usage = usage + + camb.free_global_memory() + + def test_quintessence(self): + n = 3 + # set zc and fde_zc + pars = camb.set_params( + ombh2=0.022, + omch2=0.122, + thetastar=0.01044341764253, + dark_energy_model="EarlyQuintessence", + m=8e-53, + f=0.05, + n=n, + theta_i=3.1, + use_zc=True, + zc=1e4, + fde_zc=0.1, + ) + camb.get_background(pars) + results = camb.get_results(pars) + self.assertAlmostEqual(results.get_derived_params()["thetastar"], 1.044341764253, delta=1e-5) diff --git a/docs/changelog/ppf_gamma_evolution_limit.md b/docs/changelog/ppf_gamma_evolution_limit.md new file mode 100644 index 00000000..d5332a74 --- /dev/null +++ b/docs/changelog/ppf_gamma_evolution_limit.md @@ -0,0 +1,127 @@ +# PPF Gamma Experiment Summary + +## Motivation + +The PPF dark-energy Gamma equation is numerically fragile for high-redshift, high-`k/H` modes when dark energy is dynamically negligible. With the extra dark-energy source tolerance boost in `cmbmain.f90` commented out: + +```fortran +!if (.not. CP%DarkEnergy%is_cosmological_constant) tol1=tol1/5 +``` + +the default PPF branch shows large normalized dark-energy perturbation excursions and increased spectrum sensitivity to `IntTolBoost`. The goal of these experiments is to find a PPF Gamma update that is stable at `IntTolBoost=1`, close to the existing `ckH^2 > 1000` behavior where that behavior is reliable, and avoids the physical/pathological shifts caused by a hard `ckH^2 > 30` cutoff. + +## Modes Compared + +- `mode 0`: current branch, hard `Gamma=0` only for `ckH^2 > 1000`. +- `mode 1`: for `ckH^2 > 30`, use the algebraic quasi-static fixed point `Gamma_qs = S_Gamma / (1 + ckH^2)^2`, with weak state relaxation controlled by `ppf_test_param`. +- `mode 2`: same fixed point as mode 1, but evolve toward it with a smooth capped relaxation rate controlled by `Gamma_relax_cap`. +- `mode 3`: reference bad case, hard `Gamma=0` for `ckH^2 > 30`. +- `mode 4`: for `ckH^2 > 30`, keep using the evolved `Gamma` state but relax it toward the same quasi-static fixed point with `ppf_test_param`. This avoids the direct algebraic replacement `Gamma = Gamma_qs`. + +## Main Test Cases + +- Near-LCDM non-crossing model: `w0=-0.95`, `wa=0.15`. +- Near-LCDM crossing model: `w0=-0.998`, `wa=-0.01`. +- Odd stress-test model from the earlier note: `h=0.67`, `mnu=0.2`, `Omegam=0.3`, `Omegab=0.046`, `w0=0.1`, `wa=-0.11`. +- Fluid dark energy is used as a control only for non-crossing cases. + +All fractional-difference plots now label their reference explicitly, for example: + +- relative to the same PPF mode at `IntTolBoost=4`, +- relative to `fluid(IntTolBoost=4)`, +- or relative to `mode 0(IntTolBoost=4)`. + +Plots that include mode 3 hard 30 are clipped where needed so its large failures do not hide the other curves. The clipping is annotated on the affected axes. + +## Key Results + +For the near-LCDM `k=0.1` mode evolution: + +| case | max \|Delta_de\| | max \|v_de\| | +|---|---:|---:| +| mode 0, `IntTolBoost=1` | `3.15e3` | `3.17e5` | +| mode 1, `IntTolBoost=1` | `2.82e-2` | `2.68` | +| mode 2 cap 30, `IntTolBoost=1` | `2.82e-2` | `2.68` | +| mode 3 hard 30, `IntTolBoost=1` | `2.87e-2` | `2.68` | +| mode 4 relax 31, `IntTolBoost=1` | `2.82e-2` | `2.68` | + +For near-LCDM spectra sensitivity, comparing `IntTolBoost=1` to `IntTolBoost=4` within the same mode: + +| case | lensed TT max frac | lens PP max frac | +|---|---:|---:| +| mode 0 | `6.81e-4` | `8.77e-4` | +| mode 1 | `6.37e-5` | `1.08e-5` | +| mode 2 cap 30 | `6.37e-5` | `1.08e-5` | +| mode 3 hard 30 | `6.37e-5` | `1.08e-5` | +| mode 4 relax 31 | `6.37e-5` | `1.08e-5` | + +For the near-LCDM crossing `w0=-0.998`, `wa=-0.01` check, the same stabilization holds: + +| case | max \|Delta_de\| | max \|v_de\| | +|---|---:|---:| +| mode 0, `IntTolBoost=1` | `7.56e4` | `1.67e8` | +| mode 0, `IntTolBoost=4` | `2.23e3` | `5.62e6` | +| mode 1, `IntTolBoost=1` | `1.13e-3` | `2.68` | +| mode 2 cap 30, `IntTolBoost=1` | `1.13e-3` | `2.68` | +| mode 3 hard 30, `IntTolBoost=1` | `1.15e-3` | `2.68` | +| mode 4 relax 31, `IntTolBoost=1` | `1.13e-3` | `2.68` | + +For that crossing model's spectra sensitivity, comparing `IntTolBoost=1` to `IntTolBoost=4` within the same mode: + +| case | lensed TT max frac | lens PP max frac | +|---|---:|---:| +| mode 0 | `4.46e-4` | `9.71e-4` | +| mode 1 | `6.76e-5` | `1.19e-5` | +| mode 2 cap 30 | `6.76e-5` | `1.19e-5` | +| mode 3 hard 30 | `6.76e-5` | `1.19e-5` | +| mode 4 relax 31 | `6.76e-5` | `1.19e-5` | + +For the near-LCDM non-crossing fluid control: + +- `Delta_cdm` differs from `fluid(IntTolBoost=4)` by only `~5e-6` for mode 1 at `IntTolBoost=1`. +- CMB spectra remain close to fluid away from the lowest multipoles. For mode 1 at `IntTolBoost=1`, max fractional differences for `ell >= 30` are roughly: + - TT: `3.8e-5` + - EE: `6.2e-5` + - lens PP: `4.8e-5` + +For the odd stress-test matter power relative to `fluid(IntTolBoost=4)`: + +| case | z | k/h | PPF/fluid - 1 | +|---|---:|---:|---:| +| mode 0, `IntTolBoost=4` | 0 | 0.098 | `-5.95e-2` | +| mode 1, `IntTolBoost=1` | 0 | 0.098 | `-5.86e-2` | +| mode 2 cap 30, `IntTolBoost=1` | 0 | 0.098 | `-5.94e-2` | +| mode 3 hard 30, `IntTolBoost=1` | 0 | 0.098 | `-3.27e-2` | +| mode 4 relax 31, `IntTolBoost=1` | 0 | 0.098 | `-5.95e-2` | +| mode 0, `IntTolBoost=4` | 0 | 10 | `-5.07e-3` | +| mode 1, `IntTolBoost=1` | 0 | 10 | `-2.16e-3` | +| mode 2 cap 30, `IntTolBoost=1` | 0 | 10 | `-1.56e-3` | +| mode 3 hard 30, `IntTolBoost=1` | 0 | 10 | `+1.08e1` | +| mode 4 relax 31, `IntTolBoost=1` | 0 | 10 | `-1.55e-3` | +| mode 1, `IntTolBoost=1` | 100 | 10 | `-2.19e-3` | +| mode 2 cap 30, `IntTolBoost=1` | 100 | 10 | `-1.55e-3` | +| mode 3 hard 30, `IntTolBoost=1` | 100 | 10 | `+2.63e-1` | +| mode 4 relax 31, `IntTolBoost=1` | 100 | 10 | `-1.55e-3` | + +The odd-model `Delta_cdm` comparison gives mode 1 and mode 2 essentially the same agreement with fluid as mode 0, while mode 3 has a large `k=1` excursion: + +| case | k | max \|Delta_cdm/fluid - 1\| | +|---|---:|---:| +| mode 0, `IntTolBoost=4` | 0.1 | `3.29e-2` | +| mode 1, `IntTolBoost=1` | 0.1 | `3.26e-2` | +| mode 2 cap 30, `IntTolBoost=1` | 0.1 | `3.28e-2` | +| mode 3 hard 30, `IntTolBoost=1` | 0.1 | `3.07e-2` | +| mode 4 relax 31, `IntTolBoost=1` | 0.1 | `3.28e-2` | +| mode 0, `IntTolBoost=4` | 1 | `5.03e-3` | +| mode 1, `IntTolBoost=1` | 1 | `5.07e-3` | +| mode 2 cap 30, `IntTolBoost=1` | 1 | `5.03e-3` | +| mode 3 hard 30, `IntTolBoost=1` | 1 | `4.11e-1` | +| mode 4 relax 31, `IntTolBoost=1` | 1 | `5.03e-3` | + +For the odd stress-test C_l comparison, the mode 3 hard 30 C_l arrays themselves are all `NaN` for `ell >= 30`. The plot can otherwise look innocuous because matplotlib skips those points, so the legend now marks this case as `all NaN/Inf`. This is the clearest evidence that the hard `ckH^2 > 30` cutoff can hide the near-LCDM instability while causing unacceptable behavior elsewhere. + +## Current Takeaway + +Mode 4 with `ppf_test_param=31` is now the cleanest replacement candidate. This value matches the original local relaxation rate `1 + ckH^2` exactly at the `ckH^2=30` transition. It captures the same stabilization benefit as mode 1, avoids the hard cutoff's large odd-model matter-power distortions and `NaN` C_l behavior, and does not introduce an algebraic jump by replacing `Gamma` directly with `Gamma_qs`. + +Mode 1 used `ppf_test_param=1` in these tests. It is stable, but because it sets `Gamma = Gamma_qs` in the high-`ckH` stress-energy calculation, it is less continuous than mode 4. Mode 2 with `Gamma_relax_cap` behaves very similarly to mode 4 relax 31, but mode 4 has the appealing property that it leaves the original PPF equation unchanged below `ckH^2=30` and matches the original local relaxation rate exactly at the threshold. diff --git a/docs/changelog/recfast_rosenbrock.md b/docs/changelog/recfast_rosenbrock.md new file mode 100644 index 00000000..aa27e933 --- /dev/null +++ b/docs/changelog/recfast_rosenbrock.md @@ -0,0 +1,63 @@ +# RECFAST Rosenbrock Integrator + +## Summary + +CAMB now has an optional ROS2 Rosenbrock integrator for the RECFAST H/He/Tm system. +It is intended as a fast semi-stiff replacement for the early part of the old DVERK +evolution, with a handoff back to DVERK once hydrogen has recombined enough. + +Relevant implementation points: + +- Uses a two-stage ROS2 method with analytic Jacobian entries for `x_H`, `x_He`, and `a*T_m`. +- Includes the explicit non-autonomous `df/dz` term in the Rosenbrock stages. +- Integrates the full smooth H/He/Tm equations while Rosenbrock is active, with no Saha shortcut in that phase. +- Snaps the Rosenbrock-to-DVERK handoff to the higher-redshift RECFAST output node by redoing the crossing interval with DVERK. +- Uses an embedded Rosenbrock error estimate rather than full-step versus two-half-step differencing. +- Scales the effective Rosenbrock tolerance as `rosenbrock_tol / CP%Accuracy%IntTolBoost` before solving. +- Scales the RECFAST DVERK input tolerance as `dverk_tol / CP%Accuracy%IntTolBoost`, with `dverk` itself using an internal tolerance of `tol_in / 5`. +- Uses the stored `RECFAST_nz` value as a baseline and converts it to an internal grid with `Nz * CP%Accuracy%BackgroundTimeStepBoost`. +- Uses a smaller ionization error-scale floor than the temperature floor so full-range Rosenbrock does not allow large relative tail errors when `x_e` is small. + +## Current Parameters + +| Parameter | Current value | Notes | +| --- | --- | --- | +| `use_rosenbrock` | `.true.` | Default RECFAST mode now uses the fast Rosenbrock-to-DVERK handoff. | +| `RECFAST_nz` | `2046` | Stored class default; the internal RECFAST grid uses `Nz * BackgroundTimeStepBoost`. | +| `rosenbrock_handoff_xH` | `0.976` | Fast-handoff default for the new default `Nz = 2046` baseline. | +| `rosenbrock_tol` | `3e-4` | Tuned for the fast handoff path, not for full-range Rosenbrock. | +| `dverk_tol` input | `1.5e-5` | RECFAST passes this to DVERK, then divides by `IntTolBoost`; DVERK internally uses `tol_in / 5`, so this matches the old `3e-6` internal scale at `IntTolBoost = 1`. | +| `RECFAST_rosenbrock_ion_scale_floor` | `1e-3` | Protects low-`x_e` ionization components from effectively unconstrained relative errors. | + +## How The Parameters Were Chosen + +The tuning was done against a smooth internal reference built from full-range Rosenbrock +with `handoff = 0`, `rosenbrock_tol = 1e-7`, and `Nz = 160000`. +That reference was checked for grid convergence by comparing `Nz = 80000` and `Nz = 160000`. + +The main fast-path settings were chosen as follows: + +- `rosenbrock_handoff_xH` was scanned over snapped handoff candidates and chosen to minimize the recombination-era `x_e` mismatch while keeping `get_background` fast. +- For the default `Nz = 2046` baseline, `0.976` remains the best tested fast-handoff value and is used as the code default. +- For the nearby `Nz = 2048` regression grid, `0.976` gave the best tested recombination-era agreement against the smooth reference and is retained in the regression test. +- `rosenbrock_tol = 3e-4` was retained as the default because it is a good speed/accuracy tradeoff for the fast handoff mode. +- The internal RECFAST `dverk` scale was re-tuned after switching `dverk` to a DP5 pair. The current RECFAST input value is `1.5e-5`, but `dverk` now converts that to an internal tolerance of `3e-6`; RECFAST also divides the input value by `IntTolBoost`, so the DVERK phase tightens together with the Rosenbrock phase. +- The solver now applies `rosenbrock_tol / IntTolBoost`, so increasing CAMB integration accuracy tightens the Rosenbrock substeps as well. +- The stored `Nz` value is treated as a baseline grid and is converted internally using `Nz * BackgroundTimeStepBoost` so RECFAST refines together with CAMB's background time-step boost. +- `RECFAST_rosenbrock_ion_scale_floor = 1e-3` was introduced after checking the low-`x_e` tail, where a unit floor was formally too weak for full-range Rosenbrock. + +## Measured Behavior + +Against the smooth Rosenbrock reference: + +- The most useful accuracy summary is the recombination-era fractional `x_e` error, rather than the global max `x_e` error, because the latter is often dominated by the late low-`x_e` tail. +- Fast handoff, default baseline `Nz = 2046`, `handoff = 0.976`, `tol = 3e-4`: recombination-era fractional `x_e` error about `4.32e-5`, corresponding to absolute `x_e` error about `4.25e-5`, with recombination-era relative `T_b` error about `1.04e-6`. +- With the new DP5-based `dverk`, RECFAST now passes `dverk_tol = 1.5e-5`, which maps back to the same `3e-6` internal DVERK tolerance at `IntTolBoost = 1`. +- Fast handoff, `Nz = 2048`, `handoff = 0.976`, `tol = 3e-4`: recombination-era fractional `x_e` error is about `4.30e-5`, corresponding to absolute `x_e` error about `4.23e-5`. +- Increasing `BackgroundTimeStepBoost` with the corrected internal scaling `Nz * BackgroundTimeStepBoost` gives smooth convergence to the same reference: the default RECFAST mode improves from about `4.32e-5` in recombination-era fractional `x_e` error at boost `1` to about `6.27e-6` at boost `2`, `5.14e-6` at boost `4`, and `2.29e-6` at boost `8`. +- Increasing `IntTolBoost` alone has only a tiny effect for this fast-handoff configuration, which is consistent with the early Rosenbrock plus snapped DVERK handoff already being dominated by the RECFAST output grid rather than by local substep tolerances. +- Full-range Rosenbrock with `tol = 3e-4` is too loose and was not adopted as the default mode. +- Full-range Rosenbrock becomes respectable around `tol = 1e-5`, but this is about `2.3x` to `2.6x` slower than the fast handoff configuration in the tested cases. + +In short, the shipped defaults are tuned for the fast Rosenbrock-plus-DVERK handoff mode. +If full-range Rosenbrock is requested, it should normally be used with a tighter tolerance than the default. diff --git a/docs/source/check_accuracy.rst b/docs/source/check_accuracy.rst new file mode 100644 index 00000000..809897e1 --- /dev/null +++ b/docs/source/check_accuracy.rst @@ -0,0 +1,147 @@ +.. _check-accuracy: + +Accuracy stability checks +========================= + +When developing new models, adding new approximations, changing numerical +methods, or updating the Fortran calculation, it is good practice to check that +the requested results are stable to tighter numerical accuracy settings. Small +changes in the code can otherwise move spectra in ways that are hard to spot +from a single run. The ``check_accuracy`` command compares a normal CAMB run +with a higher-accuracy reference run and reports where any differences exceed +configurable tolerances. + +The same implementation is available from Python as the +:mod:`camb.check_accuracy` module, and from the command line as:: + + camb check_accuracy inifiles/planck_2018.ini + +This tests numerical stability with mostly fixed scale cuts. Also check +lens-potential-accuracy is set high enough (effectively increasing kmax), though +note that inaccuracies in the default non-linear halo model are often larger than +numerical errors. + +What it compares +---------------- + +The checker loads an input ``.ini`` file using :func:`camb.read_ini`, runs it +once as requested, then runs a reference calculation with boosted accuracy +settings. By default the reference uses:: + + AccuracyBoost = 2 + lSampleBoost = 2 + lAccuracyBoost = 2 + IntTolBoost = 2 + DoLateRadTruncation = True + +It compares: + +* derived parameters, reported as fractional changes; +* total lensed CMB spectra from ``get_total_cls()``, so tensor contributions are + included when present; +* the lensing potential spectrum from ``get_lens_potential_cls()``; +* matter power spectra when transfer functions are requested by the input + parameters. + +For TT and EE the comparison is fractional in each multipole range. For TE it +uses ``Delta TE / sqrt(TT * EE)``, which avoids artificial problems at TE +zero-crossings. The reported tables include maximum and rms errors, the +tolerance, pass/fail status, and the multipole or grid point of the worst +sample. CPU and wall times are reported for the standard run, reference run, and +any candidate runs tested during boost searches. + +Common uses +----------- + +Basic stability check for a standard parameter file:: + + camb check_accuracy inifiles/planck_2018.ini + +Check spectra only up to a realistic analysis scale, while forcing CAMB to +calculate that far first:: + + camb check_accuracy inifiles/planck_2018.ini --set-for-lmax 4000 + +Use higher lensing-potential accuracy, useful for high-accuracy lensed spectra:: + + camb check_accuracy inifiles/planck_2018.ini --set-for-lmax 4000 --lens-potential-accuracy 4 + +Make diagnostic plots of the fractional differences:: + + camb check_accuracy inifiles/planck_2018.ini --plot-dir accuracy_plots + +Calculate a fiducial CMB delta chi-squared using a Simons Observatory-like +noise model:: + + camb check_accuracy inifiles/planck_2018.ini --set-for-lmax 4000 --chi2 --chi2-config so + +Search for the smallest top-level boost settings that pass against the +reference calculation:: + + camb check_accuracy inifiles/planck_2018.ini --find-minimal-boosts + +Then try to identify which underlying component accuracy parameters are most +important, keeping ``AccuracyBoost=1``:: + + camb check_accuracy inifiles/planck_2018.ini --find-minimal-boosts --refine-accuracy-components + +This component refinement varies ``IntTolBoost`` and the lower-level component +accuracy settings affected by ``AccuracyBoost``. It does not vary +``lSampleBoost`` or ``lAccuracyBoost``; those remain top-level boost parameters +handled by ``--find-minimal-boosts``. + +Assess the physical effect of changing lensing reference settings without +changing the comparison run:: + + camb check_accuracy inifiles/planck_2018.ini --reference-lens-potential-accuracy 8 + +Options +------- + +The command-line option list below is generated from the current parser, so it +stays aligned with ``camb check_accuracy --help``. + +.. program-output:: camb check_accuracy --help + +Programmatic use +---------------- + +For scripts and tests that already have a :class:`camb.model.CAMBparams` object, +use :func:`camb.check_accuracy.compare_params_accuracy` directly rather than +writing a temporary ``.ini`` file. The returned +:class:`camb.check_accuracy.AccuracyCheckResult` contains the standard and +reference run outputs, comparison tables, timings, and optional chi-squared +summary. + +For example:: + + import camb + from camb.check_accuracy import compare_params_accuracy + + params = camb.read_ini("inifiles/planck_2018.ini") + result = compare_params_accuracy(params, set_for_lmax=4000) + + if not result.comparison.passed: + print(result.comparison.worst_failure) + +API reference +------------- + +.. automodule:: camb.check_accuracy + +.. autofunction:: camb.check_accuracy.compare_params_accuracy + +.. autoclass:: camb.check_accuracy.AccuracyCheckResult + :members: + +.. autoclass:: camb.check_accuracy.SearchResult + :members: + +.. autoclass:: camb.check_accuracy.ComparisonResult + :members: + +.. autoclass:: camb.check_accuracy.RunOutput + :members: + +.. autoclass:: camb.check_accuracy.NoiseConfig + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 44cfb793..e868882f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,6 +40,7 @@ "sphinx.ext.mathjax", "sphinx_rtd_theme", "sphinxcontrib.jquery", + "sphinxcontrib.programoutput", "sphinx_markdown_builder", ] diff --git a/docs/source/index.rst b/docs/source/index.rst index fa00e979..2827454a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -28,7 +28,7 @@ If you want to work on the code from `GitHub `_, git clone --recursive https://github.com/cmbant/CAMB.git pip install -e ./CAMB [--user] -You will need ifort or gfortran 6 or higher installed (and on your path) to compile from source; +You will need ifort or gfortran installed (and on your path) to compile from source; you can see :ref:`fortran-compilers` for compiler installation details if needed. If you have gfortran installed, "python setup.py make" will build the Fortran library on all systems (including Windows without directly using a Makefile), and can be used to update @@ -52,6 +52,7 @@ You can also run CAMB from the command line reading parameters from a .ini file, Sample .ini files can be obtained from the `repository `_. You can load parameters programmatically from an .ini file or URL using :func:`.camb.read_ini`. +For stability testing against higher-accuracy settings, see :ref:`check-accuracy`. Main high-level modules: @@ -83,6 +84,7 @@ Other modules: :maxdepth: 1 transfer_variables + check_accuracy modifying_code variables_guide fortran_compilers diff --git a/fortran/DarkEnergyPPF.f90 b/fortran/DarkEnergyPPF.f90 index febe568c..86a567ee 100644 --- a/fortran/DarkEnergyPPF.f90 +++ b/fortran/DarkEnergyPPF.f90 @@ -106,8 +106,9 @@ subroutine TDarkEnergyPPF_PerturbedStressEnergy(this, dgrhoe, dgqe, & real(dl), intent(in) :: ay(*) real(dl), intent(inout) :: ayprime(*) integer, intent(in) :: w_ix - real(dl) :: Gamma, S_Gamma, ckH, Gammadot, Fa, sigma + real(dl) :: Gamma, S_Gamma, ckH, ckH2, Gamma_quasi_static, Gammadot, Fa, sigma real(dl) :: vT, grhoT, k2 + real(dl), parameter :: quasi_static_ckH2 = 30._dl if (this%no_perturbations) then dgrhoe=0 @@ -128,14 +129,18 @@ subroutine TDarkEnergyPPF_PerturbedStressEnergy(this, dgrhoe, dgqe, & S_Gamma = grhov_t * (1 + w) * (vT + sigma) * k / adotoa / 2._dl / k2 ckH = this%c_Gamma_ppf * k / adotoa + ckH2 = ckH * ckH - if (ckH * ckH > 1000) then - ! Was ckH^2 > 30 originally, but this is better behaved (closer to fluid) - ! for some extreme models (thanks Yanhui Yang, Simeon Bird 2024) - Gamma = 0 - Gammadot = 0.d0 + if (ckH2 > quasi_static_ckH2) then + ! Relax to the algebraic quasi-static fixed point of the same PPF equation, but cap relaxation speed + + ! Had Gamma = 0 historically, but need to allow non-zero to higher ckH2 for some extreme models + ! to stay closish to fluid and well behaved (thanks Yanhui Yang, Simeon Bird 2024) + ! However Gamma = 0 at ckH2 > 1000 was more unstable on near-LCDM models (needs finer integration tol) + Gamma_quasi_static = S_Gamma / (1._dl + ckH2)**2 + Gammadot = (1._dl + quasi_static_ckH2) * adotoa * (Gamma_quasi_static - Gamma) else - Gammadot = S_Gamma / (1 + ckH * ckH) - Gamma - ckH * ckH * Gamma + Gammadot = S_Gamma / (1._dl + ckH2) - (1._dl + ckH2) * Gamma Gammadot = Gammadot * adotoa endif ayprime(w_ix) = Gammadot !Set this here, and don't use PerturbationEvolve diff --git a/fortran/DarkEnergyQuintessence.f90 b/fortran/DarkEnergyQuintessence.f90 index a160d561..e47c4d40 100644 --- a/fortran/DarkEnergyQuintessence.f90 +++ b/fortran/DarkEnergyQuintessence.f90 @@ -23,6 +23,7 @@ module Quintessence use constants use classes use Interpolation + use RungeKuttaDP45Module, only : RungeKuttaDP45Settings, TClassRungeKuttaDP45 implicit none private @@ -79,9 +80,8 @@ module Quintessence end type TEarlyQuintessence - procedure(TClassDverk) :: dverk - public TQuintessence, TEarlyQuintessence + procedure(TClassRungeKuttaDP45) :: RungeKuttaDP45 contains function VofPhi(this, phi, deriv) @@ -304,7 +304,8 @@ subroutine TEarlyQuintessence_Init(this, State) class(TCAMBdata), intent(in), target :: State real(dl) aend, afrom integer, parameter :: NumEqs=2 - real(dl) c(24),w(NumEqs,9), y(NumEqs) + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) w(NumEqs,9), y(NumEqs) integer ind, i, ix real(dl), parameter :: splZero = 0._dl real(dl) lastsign, da_osc, last_a, a_c @@ -460,7 +461,8 @@ subroutine TEarlyQuintessence_Init(this, State) ix = i+1 sampled_a(ix)=exp(aend) a2 = sampled_a(ix)**2 - call dverk(this,NumEqs,EvolveBackgroundLog,afrom,y,aend,this%integrate_tol,ind,c,NumEqs,w) + call RungeKuttaDP45(this, NumEqs, EvolveBackgroundLog, afrom, y, aend, this%integrate_tol, ind, & + rk_settings, NumEqs, w) if (.not. this%check_error(exp(afrom), exp(aend))) return call EvolveBackgroundLog(this,NumEqs,aend,y,w(:,1)) phi_a(ix)=y(1) @@ -511,7 +513,8 @@ subroutine TEarlyQuintessence_Init(this, State) aend = this%max_a_log + this%da*i a2 =aend**2 this%sampled_a(ix)=aend - call dverk(this,NumEqs,EvolveBackground,afrom,y,aend,this%integrate_tol,ind,c,NumEqs,w) + call RungeKuttaDP45(this, NumEqs, EvolveBackground, afrom, y, aend, this%integrate_tol, ind, & + rk_settings, NumEqs, w) if (.not. this%check_error(afrom, aend)) return call EvolveBackground(this,NumEqs,aend,y,w(:,1)) this%phi_a(ix)=y(1) @@ -640,7 +643,8 @@ subroutine calc_zc_fde(this, z_c, fde_zc) real(dl), intent(out) :: z_c, fde_zc real(dl) aend, afrom integer, parameter :: NumEqs=2 - real(dl) c(24),w(NumEqs,9), y(NumEqs) + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) w(NumEqs,9), y(NumEqs) integer ind, i, ix real(dl), parameter :: splZero = 0._dl real(dl) a_c @@ -672,7 +676,8 @@ subroutine calc_zc_fde(this, z_c, fde_zc) ix = i+1 sampled_a(ix)=exp(aend) a2 = sampled_a(ix)**2 - call dverk(this,NumEqs,EvolveBackgroundLog,afrom,y,aend,this%integrate_tol,ind,c,NumEqs,w) + call RungeKuttaDP45(this, NumEqs, EvolveBackgroundLog, afrom, y, aend, this%integrate_tol, ind, & + rk_settings, NumEqs, w) if (.not. this%check_error(exp(afrom), exp(aend))) return call EvolveBackgroundLog(this,NumEqs,aend,y,w(:,1)) fde(ix) = 1/((this%state%grho_no_de(sampled_a(ix)) + this%frac_lambda0*this%State%grhov*a2**2) & @@ -753,14 +758,15 @@ end subroutine TEarlyQuintessence_SelfPointer !class(TQuintessence) :: this !real(dl), intent(IN) :: astart, phi,phidot, atol !integer, parameter :: NumEqs=2 - !real(dl) c(24),w(NumEqs,9), y(NumEqs), ast + !type(RungeKuttaDP45Settings) :: rk_settings + !real(dl) w(NumEqs,9), y(NumEqs), ast !integer ind, i ! !ast=astart !ind=1 !y(1)=phi !y(2)=phidot*astart**2 - !call dverk(this,NumEqs,EvolveBackground,ast,y,1._dl,atol,ind,c,NumEqs,w) + !call RungeKuttaDP45(this, NumEqs, EvolveBackground, ast, y, 1._dl, atol, ind, rk_settings, NumEqs, w) !call EvolveBackground(this,NumEqs,1._dl,y,w(:,1)) ! !GetOmegaFromInitial=(0.5d0*y(2)**2 + Vofphi(y(1),0))/this%State%grhocrit !(3*adot**2) diff --git a/fortran/Makefile_main b/fortran/Makefile_main index 745c6477..fb554531 100644 --- a/fortran/Makefile_main +++ b/fortran/Makefile_main @@ -28,11 +28,25 @@ DLL_DIR ?= Releaselib CAMBLIB = libcamb.a -SOURCEFILES = constants config classes MathUtils subroutines DarkAge21cm \ +SOURCEFILES = constants config classes MathUtils RungeKuttaDP45 subroutines DarkAge21cm \ DarkEnergyInterface SourceWindows massive_neutrinos model results bessels \ $(RECOMBINATION_FILES) $(DARKENERGY_FILES) equations \ $(REIONIZATION_FILES) $(POWERSPECTRUM_FILES) $(NONLINEAR_FILES) \ lensing $(BISPECTRUM) cmbmain camb camb_python +CAMB_DEPFILES = $(addsuffix .d,$(SOURCEFILES)) +CAMB_DRIVER_DEPFILES = $(CAMB_DEPFILES) $(DRIVER).d +OUTPUT_LIBRARY_DEPFILES = $(addprefix $(OUTPUT_DIR)/,$(CAMB_DEPFILES)) +OUTPUT_DRIVER_DEPFILES = $(addprefix $(OUTPUT_DIR)/,$(CAMB_DRIVER_DEPFILES)) +DLL_LIBRARY_DEPFILES = $(addprefix $(DLL_DIR)/,$(CAMB_DEPFILES)) + +define run_build_submake + +@if test -n "$(strip $(filter-out $(wildcard $(1)),$(1)))"; then \ + echo "Bootstrapping $(2) with -j1 until dependency files are generated."; \ + $(MAKE) -j1 -C $(2) $(MAKEOPT) -r -f ../Makefile_main $(3); \ + else \ + $(MAKE) -C $(2) $(MAKEOPT) -r -f ../Makefile_main $(3); \ + fi +endef F90WFLAGS = -Waliasing -Wampersand -Wconversion -Wc-binding-type -Wintrinsics-std \ @@ -92,23 +106,25 @@ compiler.ver: @rm -f *.d camb: libforutils directories - @$(MAKE) -C $(OUTPUT_DIR) $(MAKEOPT) -r -f ../Makefile_main $(CAMBLIB) ../camb + $(call run_build_submake,$(OUTPUT_DRIVER_DEPFILES),$(OUTPUT_DIR),$(CAMBLIB) ../camb) libcamb: libforutils directories - @$(MAKE) -C $(OUTPUT_DIR) $(MAKEOPT) -r -f ../Makefile_main $(CAMBLIB) + $(call run_build_submake,$(OUTPUT_LIBRARY_DEPFILES),$(OUTPUT_DIR),$(CAMBLIB)) python: libforutils_so directories_so - @$(MAKE) -C $(DLL_DIR) $(MAKEOPT) -r -f ../Makefile_main camblib.so F90FLAGS="$(SF90FLAGS)" OUTPUT_DIR=$(DLL_DIR) + $(call run_build_submake,$(DLL_LIBRARY_DEPFILES),$(DLL_DIR),camblib.so F90FLAGS="$(SF90FLAGS)" OUTPUT_DIR=$(DLL_DIR)) + @mkdir -p $(PYCAMB_OUTPUT_DIR) + cp $(DLL_DIR)/camblib.so $(PYCAMB_OUTPUT_DIR) ../camb: $(CAMBLIB) $(DRIVER).o | silent - @$(MAKE) -C .. $(MAKEOPT) -f Makefile_main camb_exe + +@$(MAKE) -C .. $(MAKEOPT) -f Makefile_main camb_exe camb_exe: $(F90C) $(F90FLAGS) $(MODOUT) $(IFLAG)$(OUTPUT_DIR)/ $(IFLAG)"$(FORUTILS_DIR)" \ $(OUTPUT_DIR)/$(DRIVER).o $(OUTPUT_DIR)/$(CAMBLIB) $(F90CRLINK) $(LIBLINK) -o camb camblib.so: $(CAMBOBJ) - @$(MAKE) -C .. $(MAKEOPT) -f Makefile_main $(DLL_DIR)/camblib.so + +@$(MAKE) -C .. $(MAKEOPT) -f Makefile_main $(DLL_DIR)/camblib.so libcambobj: LIBCAMBOBJ = $(patsubst %,$(DLL_DIR)/%.o,$(SOURCEFILES)) @@ -117,7 +133,6 @@ libcambobj: $(DLL_DIR)/camblib.so: libcambobj $(F90C) $(SF90FLAGS) $(SMODOUT) $(IFLAG)$(DLL_DIR)/ $(IFLAG)"$(FORUTILS_DIR)" \ $(LIBCAMBOBJ) $(F90CRLINK) $(LIBLINK) -o $(DLL_DIR)/camblib.so - cp $(DLL_DIR)/camblib.so $(PYCAMB_OUTPUT_DIR) $(CAMBLIB): $(CAMBOBJ) @@ -138,12 +153,10 @@ directories_so: @mkdir -p $(DLL_DIR) libforutils_so: - @cd "$(FORUTILSPATH)" && \ - $(MAKE) $(MAKEOPT) $(DLL_DIR) + +@$(MAKE) -C "$(FORUTILSPATH)" $(MAKEOPT) $(DLL_DIR) libforutils: - @cd "$(FORUTILSPATH)" && \ - $(MAKE) $(MAKEOPT) $(OUTPUT_DIR) + +@$(MAKE) -C "$(FORUTILSPATH)" $(MAKEOPT) $(OUTPUT_DIR) clean: @@ -156,13 +169,13 @@ delete: ## CosmoRec make parts cleanCR: - cd $(COSMOREC_PATH); make tidy; + +@$(MAKE) -C $(COSMOREC_PATH) $(MAKEOPT) tidy libCosmoRec.a: - cd $(COSMOREC_PATH); make lib; + +@$(MAKE) -C $(COSMOREC_PATH) $(MAKEOPT) lib libhyrec.a: - cd $(HYREC_PATH); make libhyrec.a; + +@$(MAKE) -C $(HYREC_PATH) $(MAKEOPT) libhyrec.a silent: @: diff --git a/fortran/RungeKuttaDP45.f90 b/fortran/RungeKuttaDP45.f90 new file mode 100644 index 00000000..7b6182c4 --- /dev/null +++ b/fortran/RungeKuttaDP45.f90 @@ -0,0 +1,372 @@ + module RungeKuttaDP45Module + use Precision + implicit none + private + + integer, parameter, public :: RK45ErrorMixed = 0 + integer, parameter, public :: RK45ErrorAbsolute = 1 + integer, parameter, public :: RK45ErrorRelative = 2 + integer, parameter, public :: RK45ErrorRelativeFloor = 3 + + type, public :: RungeKuttaDP45Settings + ! User-configurable controls. Defaults mainly preserves CAMB's historical usage + ! unless the caller overrides fields and enters with ind = 2. + ! error_control chooses the max-norm weighting: + ! RK45ErrorMixed 1/max(1, abs(y(k))) (default). + ! RK45ErrorAbsolute 1. + ! RK45ErrorRelative 1/abs(y(k)). + ! RK45ErrorRelativeFloor 1/max(error_floor, abs(y(k))). + integer :: error_control = RK45ErrorMixed + ! Scalar floor for RK45ErrorRelativeFloor. Default 0 is unused unless that + ! mode is selected. + real(dl) :: error_floor = 0._dl + ! Explicit minimum trial-step magnitude. Default 0 uses the historical + ! automatic estimate 10*max(tiny, eps*max(weighted_norm_y/tol, abs(x))). + real(dl) :: min_step_size = 0._dl + ! Explicit first-step magnitude. Default 0 uses computed_max_step_size * tol**(1/5). + real(dl) :: initial_step_size = 0._dl + ! Problem scale entering the acceptance test and optional hmax cap. + ! Default 0 promotes to 1; if non-zero it also limits hmax to 2/abs(problem_scale). + real(dl) :: problem_scale = 0._dl + ! Explicit maximum trial-step magnitude. Default 0 uses CAMB default of 20 (used to be 2) unless + ! problem_scale imposes a tighter 2/abs(scale) limit. + real(dl) :: max_step_size = 0._dl + ! Optional hard cap on derivative evaluations. Default 0 disables this check. + integer :: max_function_evaluations = 0 + ! Return with ind = 4 after selecting the next step size but before the trial step. + logical :: interrupt_before_trial_step = .false. + ! Return with ind = 5 or 6 after estimating trial-step error and before + ! applying the accept/reject update. + logical :: interrupt_after_trial_step = .false. + ! Persistent solver state below this point, maintained across re-entry. + ! These correspond to the historical c(12:24) bookkeeping values. + real(dl) :: weighted_norm_y = 0._dl + real(dl) :: computed_min_step_size = 0._dl + real(dl) :: current_step_size = 0._dl + real(dl) :: computed_scale = 0._dl + real(dl) :: computed_max_step_size = 0._dl + real(dl) :: trial_x = 0._dl + real(dl) :: trial_step = 0._dl + real(dl) :: estimated_error = 0._dl + real(dl) :: previous_xend = 0._dl + logical :: xend_reached = .false. + integer :: successful_steps = 0 + integer :: successive_failures = 0 + integer :: function_evaluations = 0 + end type RungeKuttaDP45Settings + + public :: TClassRungeKuttaDP45 + abstract interface + subroutine TClassRungeKuttaDP45(this, n, fcn, x, y, xend, tol, ind, settings, nw, w) + use Precision + use classes, only : TCambComponent + import :: RungeKuttaDP45Settings + class(TCambComponent), target :: this + integer, intent(in) :: n, nw + integer, intent(inout) :: ind + real(dl), intent(inout) :: x, y(n), w(nw, 9) + real(dl), intent(in) :: xend, tol + type(RungeKuttaDP45Settings), intent(inout) :: settings + external fcn + end subroutine TClassRungeKuttaDP45 + end interface + + end module RungeKuttaDP45Module + + + subroutine RungeKuttaDP45(EV, n, fcn, x, y, xend, tol_in, ind, settings, nw, w) + use Precision + use Config, only : GlobalError, error_evolution + use RungeKuttaDP45Module, only : RK45ErrorMixed, RK45ErrorAbsolute, RK45ErrorRelative, & + RK45ErrorRelativeFloor, RungeKuttaDP45Settings + implicit none + integer, intent(in) :: n, nw + integer, intent(inout) :: ind + real(dl), intent(inout) :: x, y(n), w(nw, 9) + real(dl), intent(in) :: xend, tol_in + type(RungeKuttaDP45Settings), intent(inout) :: settings + real(dl) :: tol, temp + real(dl), parameter :: one_fifth = 1._dl / 5._dl + real(dl), parameter :: default_max_step_size = 20._dl + real(dl), parameter :: dp_a21 = 1._dl / 5._dl + real(dl), parameter :: dp_a31 = 3._dl / 40._dl + real(dl), parameter :: dp_a32 = 9._dl / 40._dl + real(dl), parameter :: dp_a41 = 44._dl / 45._dl + real(dl), parameter :: dp_a42 = -56._dl / 15._dl + real(dl), parameter :: dp_a43 = 32._dl / 9._dl + real(dl), parameter :: dp_a51 = 19372._dl / 6561._dl + real(dl), parameter :: dp_a52 = -25360._dl / 2187._dl + real(dl), parameter :: dp_a53 = 64448._dl / 6561._dl + real(dl), parameter :: dp_a54 = -212._dl / 729._dl + real(dl), parameter :: dp_a61 = 9017._dl / 3168._dl + real(dl), parameter :: dp_a62 = -355._dl / 33._dl + real(dl), parameter :: dp_a63 = 46732._dl / 5247._dl + real(dl), parameter :: dp_a64 = 49._dl / 176._dl + real(dl), parameter :: dp_a65 = -5103._dl / 18656._dl + real(dl), parameter :: dp_b1 = 35._dl / 384._dl + real(dl), parameter :: dp_b3 = 500._dl / 1113._dl + real(dl), parameter :: dp_b4 = 125._dl / 192._dl + real(dl), parameter :: dp_b5 = -2187._dl / 6784._dl + real(dl), parameter :: dp_b6 = 11._dl / 84._dl + real(dl), parameter :: dp_e1 = 71._dl / 57600._dl + real(dl), parameter :: dp_e3 = -71._dl / 16695._dl + real(dl), parameter :: dp_e4 = 71._dl / 1920._dl + real(dl), parameter :: dp_e5 = -17253._dl / 339200._dl + real(dl), parameter :: dp_e6 = 22._dl / 525._dl + real(dl), parameter :: dp_e7 = -1._dl / 40._dl + real(dl), parameter :: machine_roundoff = epsilon(1._dl) + real(dl), parameter :: machine_tiny = tiny(1._dl) + logical :: resume_after_interrupt1, resume_after_interrupt2 + real :: EV + +! RungeKuttaDP45 preserves CAMB's historical control flow, interrupt +! semantics, and workspace layout while exposing the old c(*) control array as +! named fields on a settings/state object. +! +! Arguments: +! EV, fcn: opaque object and derivative callback passed through unchanged. +! n, x, y, xend, tol_in: standard ODE system size, state, target x, and +! caller-provided tolerance. tol_in is used directly. +! ind: solver entry/return status. +! 1 reset with default settings, 2 reset using caller-populated settings, +! 3 continue normal re-entry, 4/5/6 resume after interrupts, +! returns 3/4/5/6 or -1/-2/-3. +! settings: named replacement for the old c(*) vector. +! Configurable fields are error_control, error_floor, min_step_size, +! initial_step_size, problem_scale, max_step_size, +! max_function_evaluations, interrupt_before_trial_step, and +! interrupt_after_trial_step. +! The remaining fields store persistent solver state between calls. +! nw, w: work array dimensions retained for compatibility with existing +! CAMB callers. +! +! Error control modes: +! RK45ErrorMixed default 1 / max(1, abs(y(k))) weights. +! RK45ErrorAbsolute absolute error control. +! RK45ErrorRelative relative error control. +! RK45ErrorRelativeFloor relative control with scalar floor error_floor. + + external fcn + tol = tol_in + + resume_after_interrupt1 = .false. + resume_after_interrupt2 = .false. + + select case (ind) + case (1, 2) + if (n > nw .or. tol <= 0._dl) then + call abort_runge_kutta_dp45() + return + end if + + if (ind == 1) then + settings = RungeKuttaDP45Settings() + else + call normalize_settings(settings) + end if + + settings%previous_xend = x + settings%xend_reached = .false. + settings%successful_steps = 0 + settings%successive_failures = 0 + settings%function_evaluations = 0 + + case (3) + if (settings%xend_reached .and. (x /= settings%previous_xend .or. xend == settings%previous_xend)) then + call abort_runge_kutta_dp45() + return + end if + settings%xend_reached = .false. + + case (4) + resume_after_interrupt1 = .true. + + case (5, 6) + resume_after_interrupt2 = .true. + + case default + call abort_runge_kutta_dp45() + return + end select + + step_loop: do + if (.not. resume_after_interrupt1 .and. .not. resume_after_interrupt2) then + if (settings%max_function_evaluations /= 0 .and. & + settings%function_evaluations >= settings%max_function_evaluations) then + ind = -1 + return + end if + + if (ind /= 6) then + call fcn(EV, n, x, y, w(1, 1)) + settings%function_evaluations = settings%function_evaluations + 1 + end if + + settings%computed_min_step_size = abs(settings%min_step_size) + if (settings%computed_min_step_size == 0._dl) then + select case (settings%error_control) + case (RK45ErrorAbsolute) + settings%weighted_norm_y = maxval(abs(y(1:n))) + case (RK45ErrorRelative) + settings%weighted_norm_y = 1._dl + case (RK45ErrorRelativeFloor) + temp = maxval(abs(y(1:n)) / settings%error_floor) + settings%weighted_norm_y = min(temp, 1._dl) + case default + temp = maxval(abs(y(1:n))) + settings%weighted_norm_y = min(temp, 1._dl) + end select + + settings%computed_min_step_size = 10._dl * max( & + machine_tiny, machine_roundoff * max(settings%weighted_norm_y / tol, abs(x)) & + ) + end if + + settings%computed_scale = abs(settings%problem_scale) + if (settings%computed_scale == 0._dl) settings%computed_scale = 1._dl + + if (settings%max_step_size /= 0._dl .and. settings%problem_scale /= 0._dl) then + settings%computed_max_step_size = min(abs(settings%max_step_size), 2._dl / abs(settings%problem_scale)) + else if (settings%max_step_size /= 0._dl) then + settings%computed_max_step_size = abs(settings%max_step_size) + else if (settings%problem_scale /= 0._dl) then + settings%computed_max_step_size = 2._dl / abs(settings%problem_scale) + else + settings%computed_max_step_size = default_max_step_size + end if + + if (settings%computed_min_step_size > settings%computed_max_step_size) then + ind = -2 + return + end if + + if (ind <= 2) then + settings%current_step_size = abs(settings%initial_step_size) + if (settings%current_step_size == 0._dl) then + settings%current_step_size = settings%computed_max_step_size * tol**one_fifth + end if + else if (settings%successive_failures <= 1) then + temp = 2._dl * settings%current_step_size + if (tol < (2._dl / 0.9_dl)**5 * settings%estimated_error) then + temp = 0.9_dl * (tol / settings%estimated_error)**one_fifth * settings%current_step_size + end if + settings%current_step_size = max(temp, 0.5_dl * settings%current_step_size) + else + settings%current_step_size = 0.5_dl * settings%current_step_size + end if + + settings%current_step_size = min(settings%current_step_size, settings%computed_max_step_size) + settings%current_step_size = max(settings%current_step_size, settings%computed_min_step_size) + + if (settings%interrupt_before_trial_step) then + ind = 4 + return + end if + end if + + if (.not. resume_after_interrupt2) then + if (settings%current_step_size < abs(xend - x)) then + settings%current_step_size = min(settings%current_step_size, 0.5_dl * abs(xend - x)) + settings%trial_x = x + sign(settings%current_step_size, xend - x) + else + settings%current_step_size = abs(xend - x) + settings%trial_x = xend + end if + + settings%trial_step = settings%trial_x - x + + w(1:n, 9) = y(1:n) + settings%trial_step * dp_a21 * w(1:n, 1) + call fcn(EV, n, x + settings%trial_step / 5._dl, w(1, 9), w(1, 2)) + + w(1:n, 9) = y(1:n) + settings%trial_step * (dp_a31 * w(1:n, 1) + dp_a32 * w(1:n, 2)) + call fcn(EV, n, x + settings%trial_step * (3._dl / 10._dl), w(1, 9), w(1, 3)) + + w(1:n, 9) = y(1:n) + settings%trial_step * (dp_a41 * w(1:n, 1) + dp_a42 * w(1:n, 2) + & + dp_a43 * w(1:n, 3)) + call fcn(EV, n, x + settings%trial_step * (4._dl / 5._dl), w(1, 9), w(1, 4)) + + w(1:n, 9) = y(1:n) + settings%trial_step * (dp_a51 * w(1:n, 1) + dp_a52 * w(1:n, 2) + & + dp_a53 * w(1:n, 3) + dp_a54 * w(1:n, 4)) + call fcn(EV, n, x + settings%trial_step * (8._dl / 9._dl), w(1, 9), w(1, 5)) + + w(1:n, 9) = y(1:n) + settings%trial_step * (dp_a61 * w(1:n, 1) + dp_a62 * w(1:n, 2) + & + dp_a63 * w(1:n, 3) + dp_a64 * w(1:n, 4) + dp_a65 * w(1:n, 5)) + call fcn(EV, n, x + settings%trial_step, w(1, 9), w(1, 6)) + + w(1:n, 9) = y(1:n) + settings%trial_step * (dp_b1 * w(1:n, 1) + dp_b3 * w(1:n, 3) + & + dp_b4 * w(1:n, 4) + dp_b5 * w(1:n, 5) + dp_b6 * w(1:n, 6)) + call fcn(EV, n, x + settings%trial_step, w(1, 9), w(1, 7)) + + settings%function_evaluations = settings%function_evaluations + 6 + + w(1:n, 2) = dp_e1 * w(1:n, 1) + dp_e3 * w(1:n, 3) + dp_e4 * w(1:n, 4) + & + dp_e5 * w(1:n, 5) + dp_e6 * w(1:n, 6) + dp_e7 * w(1:n, 7) + + select case (settings%error_control) + case (RK45ErrorAbsolute) + temp = maxval(abs(w(1:n, 2))) + case (RK45ErrorRelative) + temp = maxval(abs(w(1:n, 2) / y(1:n))) + case (RK45ErrorRelativeFloor) + temp = maxval(abs(w(1:n, 2)) / max(settings%error_floor, abs(y(1:n)))) + case default + temp = maxval(abs(w(1:n, 2)) / max(1._dl, abs(y(1:n)))) + end select + + settings%estimated_error = temp * settings%current_step_size * settings%computed_scale + + ind = 5 + if (settings%estimated_error > tol) ind = 6 + + if (settings%interrupt_after_trial_step) return + end if + + if (ind == 5) then + x = settings%trial_x + y(1:n) = w(1:n, 9) + settings%successful_steps = settings%successful_steps + 1 + settings%successive_failures = 0 + + if (x == xend) then + ind = 3 + settings%previous_xend = xend + settings%xend_reached = .true. + return + end if + + w(1:n, 1) = w(1:n, 7) + ind = 6 + else + settings%successive_failures = settings%successive_failures + 1 + + if (settings%current_step_size <= settings%computed_min_step_size) then + ind = -3 + return + end if + end if + + resume_after_interrupt1 = .false. + resume_after_interrupt2 = .false. + end do step_loop + + contains + + subroutine normalize_settings(settings) + type(RungeKuttaDP45Settings), intent(inout) :: settings + + settings%error_control = abs(settings%error_control) + settings%error_floor = abs(settings%error_floor) + settings%min_step_size = abs(settings%min_step_size) + settings%initial_step_size = abs(settings%initial_step_size) + settings%problem_scale = abs(settings%problem_scale) + settings%max_step_size = abs(settings%max_step_size) + settings%max_function_evaluations = abs(settings%max_function_evaluations) + + end subroutine normalize_settings + + subroutine abort_runge_kutta_dp45() + write (*,*) 'Error in RungeKuttaDP45, x =', x, ' xend =', xend + call GlobalError('RungeKuttaDP45 error', error_evolution) + end subroutine abort_runge_kutta_dp45 + + end subroutine RungeKuttaDP45 diff --git a/fortran/camb.f90 b/fortran/camb.f90 index cfda2baf..67ebd1e2 100644 --- a/fortran/camb.f90 +++ b/fortran/camb.f90 @@ -362,6 +362,7 @@ logical function CAMB_ReadParams(P, Ini, ErrMsg) call ReadAccuracyReal(P%Accuracy%lAccuracyBoost, 'lAccuracyBoost', 'l_accuracy_boost') call ReadAccuracyReal(P%Accuracy%TimeStepBoost, 'TimeStepBoost') call ReadAccuracyReal(P%Accuracy%BackgroundTimeStepBoost, 'BackgroundTimeStepBoost') + call ReadAccuracyReal(P%Accuracy%TimeSwitchBoost, 'TimeSwitchBoost') call ReadAccuracyReal(P%Accuracy%IntTolBoost, 'IntTolBoost') call ReadAccuracyReal(P%Accuracy%SourcekAccuracyBoost, 'SourcekAccuracyBoost') call ReadAccuracyReal(P%Accuracy%IntkAccuracyBoost, 'IntkAccuracyBoost') @@ -656,7 +657,7 @@ logical function CAMB_RunFromIni(Ini, InputFile, ErrMsg) character(len=:), allocatable :: outroot, VectorFileName, & ScalarFileName, TensorFileName, TotalFileName, LensedFileName,& LensedTotFileName, LensPotentialFileName, ScalarCovFileName, & - version_check + version_check, ArrayKey integer :: i character(len=Ini_max_string_len), allocatable :: TransferFileNames(:), & MatterPowerFileNames(:), TransferClFileNames(:) @@ -683,18 +684,22 @@ logical function CAMB_RunFromIni(Ini, InputFile, ErrMsg) allocate (MatterPowerFileNames(P%Transfer%PK_num_redshifts)) allocate (TransferClFileNames(P%Transfer%PK_num_redshifts)) do i=1, P%transfer%PK_num_redshifts - transferFileNames(i) = Ini%Read_String_Array('transfer_filename', i) - MatterPowerFilenames(i) = Ini%Read_String_Array('transfer_matterpower', i) - if (TransferFileNames(i) == '') then - TransferFileNames(i) = trim(numcat('transfer_',i))//'.dat' + ArrayKey = Ini%Key_To_Arraykey('transfer_filename', i) + if (i == 1) then + TransferFileNames(i) = Ini%Read_String_Default(ArrayKey, 'transfer_out.dat') + else + TransferFileNames(i) = Ini%Read_String_Default(ArrayKey, trim(numcat('transfer_',i))//'.dat') end if - if (MatterPowerFilenames(i) == '') then - MatterPowerFilenames(i) = trim(numcat('matterpower_',i))//'.dat' + + ArrayKey = Ini%Key_To_Arraykey('transfer_matterpower', i) + if (i == 1) then + MatterPowerFilenames(i) = Ini%Read_String_Default(ArrayKey, 'matterpower.dat') + else + MatterPowerFilenames(i) = Ini%Read_String_Default(ArrayKey, trim(numcat('matterpower_',i))//'.dat') end if - if (TransferFileNames(i)/= '') & - TransferFileNames(i) = trim(outroot)//TransferFileNames(i) - if (MatterPowerFilenames(i) /= '') & - MatterPowerFilenames(i)=trim(outroot)//MatterPowerFilenames(i) + + TransferFileNames(i) = outroot // TransferFileNames(i) + MatterPowerFilenames(i) = outroot // MatterPowerFilenames(i) if (P%Do21cm) then TransferClFileNames(i) = Ini%Read_String_Array('transfer_cl_filename',i) @@ -705,7 +710,7 @@ logical function CAMB_RunFromIni(Ini, InputFile, ErrMsg) end if if (TransferClFileNames(i)/= '') & - TransferClFileNames(i) = trim(outroot)//TransferClFileNames(i) + TransferClFileNames(i) = outroot // TransferClFileNames(i) end do end if @@ -714,15 +719,17 @@ logical function CAMB_RunFromIni(Ini, InputFile, ErrMsg) output_factor = Ini%Read_Double('CMB_outputscale', 1.d0) if (P%WantScalars) then - ScalarFileName = trim(outroot) // Ini%Read_String('scalar_output_file') - LensedFileName = trim(outroot) // Ini%Read_String('lensed_output_file') - LensPotentialFileName = Ini%Read_String('lens_potential_output_file') - if (LensPotentialFileName/='') LensPotentialFileName = concat(outroot,LensPotentialFileName) + ScalarFileName = Ini%Read_String_Default('scalar_output_file', 'scalCls.dat') + ScalarFileName = outroot // ScalarFileName + LensedFileName = Ini%Read_String_Default('lensed_output_file', 'lensedCls.dat') + LensedFileName = outroot // LensedFileName + LensPotentialFileName = Ini%Read_String_Default('lens_potential_output_file', 'lenspotentialCls.dat') + LensPotentialFileName = outroot // LensPotentialFileName ScalarCovFileName = Ini%Read_String_Default('scalar_covariance_output_file', & 'scalarCovCls.dat', .false.) if (ScalarCovFileName /= '') then P%want_cl_2D_array = .true. - ScalarCovFileName = concat(outroot, ScalarCovFileName) + ScalarCovFileName = outroot // ScalarCovFileName end if else ScalarFileName = '' @@ -731,11 +738,13 @@ logical function CAMB_RunFromIni(Ini, InputFile, ErrMsg) ScalarCovFileName = '' end if if (P%WantTensors) then - TensorFileName = trim(outroot) // Ini%Read_String('tensor_output_file') + TensorFileName = Ini%Read_String_Default('tensor_output_file', 'tensCls.dat') + TensorFileName = outroot // TensorFileName if (P%WantScalars) then - TotalFileName = trim(outroot) // Ini%Read_String('total_output_file') - LensedTotFileName = Ini%Read_String('lensed_total_output_file') - if (LensedTotFileName /= '') LensedTotFileName = trim(outroot) // trim(LensedTotFileName) + TotalFileName = Ini%Read_String_Default('total_output_file', 'totCls.dat') + TotalFileName = outroot // TotalFileName + LensedTotFileName = Ini%Read_String_Default('lensed_total_output_file', 'lensedtotCls.dat') + LensedTotFileName = outroot // LensedTotFileName else TotalFileName = '' LensedTotFileName = '' @@ -746,14 +755,15 @@ logical function CAMB_RunFromIni(Ini, InputFile, ErrMsg) LensedTotFileName = '' end if if (P%WantVectors) then - VectorFileName = trim(outroot) // Ini%Read_String('vector_output_file') + VectorFileName = Ini%Read_String_Default('vector_output_file', 'vecCls.dat') + VectorFileName = outroot // VectorFileName else VectorFileName = '' end if #ifdef WRITE_FITS if (P%WantCls) then - FITSfilename = trim(outroot) // Ini%Read_String('FITS_filename', .true.) + FITSfilename = outroot // Ini%Read_String('FITS_filename', .true.) if (FITSfilename /= '') then inquire(file=FITSfilename, exist=bad) if (bad) then diff --git a/fortran/camb_python.f90 b/fortran/camb_python.f90 index 6c0325aa..9e4604a6 100644 --- a/fortran/camb_python.f90 +++ b/fortran/camb_python.f90 @@ -9,6 +9,7 @@ module handles use ObjectLists use classes use Interpolation + use RungeKuttaDP45Module, only : RungeKuttaDP45Settings implicit none Type c_MatterTransferData @@ -577,7 +578,8 @@ subroutine GetOutputEvolutionFork(Data, EV, times, outputs, nsources,ncustomsour integer, intent(in) :: nsources, ncustomsources real(dl) tau,tol1,tauend, taustart integer j,ind - real(dl) c(24),w(EV%nvar,9), y(EV%nvar), cs2, opacity + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) w(EV%nvar,9), y(EV%nvar), cs2, opacity real(dl) yprime(EV%nvar), ddelta, delta, adotoa,growth, a real(dl), target :: sources(nsources), custom_sources(ncustomsources) real, target :: Arr(Transfer_max) @@ -595,7 +597,7 @@ subroutine GetOutputEvolutionFork(Data, EV, times, outputs, nsources,ncustomsour tauend = times(j) if (tauend Arr EV%OutputSources => sources diff --git a/fortran/classes.f90 b/fortran/classes.f90 index 5ce85872..59cab1aa 100644 --- a/fortran/classes.f90 +++ b/fortran/classes.f90 @@ -115,18 +115,6 @@ module classes procedure :: get_timesteps => TReionizationModel_get_timesteps end Type TReionizationModel - - interface - subroutine TClassDverk (this,n, fcn, x, y, xend, tol, ind, c, nw, w) - use Precision - import TCambComponent - class(TCambComponent), target :: this - integer n, nw, ind - real(dl) x, y(n), xend, tol, c(*), w(nw,9) - external fcn - end subroutine - end interface - contains subroutine TCambComponent_ReadParams(this, Ini) diff --git a/fortran/cmbmain.f90 b/fortran/cmbmain.f90 index db6bdaa5..ba2122b7 100644 --- a/fortran/cmbmain.f90 +++ b/fortran/cmbmain.f90 @@ -64,6 +64,7 @@ module CAMBmain use constants use DarkEnergyInterface use MathUtils + use RungeKuttaDP45Module, only : RungeKuttaDP45Settings implicit none private @@ -973,7 +974,8 @@ subroutine CalcScalarSources(EV,taustart) type(EvolutionVars) EV real(dl) tau,tol1,tauend, taustart integer j,ind,itf - real(dl) c(24),w(EV%nvar,9), y(EV%nvar), sources(ThisSources%SourceNum) + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) w(EV%nvar,9), y(EV%nvar), sources(ThisSources%SourceNum) w=0 y=0 @@ -986,11 +988,12 @@ subroutine CalcScalarSources(EV,taustart) ! Begin timestep loop. itf=1 tol1=base_tol/exp(CP%Accuracy%AccuracyBoost*CP%Accuracy%IntTolBoost-1) + if (CP%WantTransfer) then if (CP%Transfer%high_precision) tol1=tol1/100 do while (itf <= State%num_transfer_redshifts .and. State%TimeSteps%points(2) > State%Transfer_Times(itf)) !Just in case someone wants to get the transfer outputs well before recombination - call GaugeInterface_EvolveScal(EV,tau,y,State%Transfer_Times(itf),tol1,ind,c,w) + call GaugeInterface_EvolveScal(EV, tau, y, State%Transfer_Times(itf), tol1, ind, rk_settings, w) if (global_error_flag/=0) return call outtransf(EV,y, tau, State%MT%TransferData(:,EV%q_ix,itf)) itf = itf+1 @@ -1005,7 +1008,7 @@ subroutine CalcScalarSources(EV,taustart) ThisSources%LinearSrc(EV%q_ix,:,j)=0 else !Integrate over time, calulate end point derivs and calc output - call GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,c,w) + call GaugeInterface_EvolveScal(EV, tau, y, tauend, tol1, ind, rk_settings, w) if (global_error_flag/=0) return call output(EV,y,j, tau,sources, CP%CustomSources%num_custom_sources) @@ -1015,7 +1018,8 @@ subroutine CalcScalarSources(EV,taustart) 101 if (CP%WantTransfer.and.itf <= State%num_transfer_redshifts) then if (j < State%TimeSteps%npoints) then if (tauend < State%Transfer_Times(itf) .and. State%TimeSteps%points(j+1) > State%Transfer_Times(itf)) then - call GaugeInterface_EvolveScal(EV,tau,y,State%Transfer_Times(itf),tol1,ind,c,w) + call GaugeInterface_EvolveScal(EV, tau, y, State%Transfer_Times(itf), tol1, ind, & + rk_settings, w) if (global_error_flag/=0) return endif end if @@ -1045,7 +1049,8 @@ subroutine CalcTensorSources(EV,taustart) type(EvolutionVars) EV real(dl) tau,tol,tauend, taustart integer j,ind - real(dl) c(24),wt(EV%nvart,9), yt(EV%nvart) + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) wt(EV%nvart,9), yt(EV%nvart) call initialt(EV,yt, taustart) @@ -1059,7 +1064,7 @@ subroutine CalcTensorSources(EV,taustart) if (EV%q*tauend > max_etak_tensor) then ThisSources%LinearSrc(EV%q_ix,:,j) = 0 else - call GaugeInterface_EvolveTens(EV,tau,yt,tauend,tol,ind,c,wt) + call GaugeInterface_EvolveTens(EV, tau, yt, tauend, tol, ind, rk_settings, wt) call outputt(EV,yt,EV%nvart,tau,ThisSources%LinearSrc(EV%q_ix,CT_Temp,j),& ThisSources%LinearSrc(EV%q_ix,CT_E,j),ThisSources%LinearSrc(EV%q_ix,CT_B,j)) @@ -1074,13 +1079,14 @@ subroutine CalcVectorSources(EV,taustart) type(EvolutionVars) EV real(dl) tau,tol,tauend, taustart integer j,ind - real(dl) c(24),wt(EV%nvarv,9), yv(EV%nvarv) + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) wt(EV%nvarv,9), yv(EV%nvarv) call initialv(EV,yv, taustart) tau=taustart ind=1 - tol=base_tol*0.01/exp(CP%Accuracy%AccuracyBoost*CP%Accuracy%IntTolBoost-1) + tol=base_tol/100/exp(CP%Accuracy%AccuracyBoost*CP%Accuracy%IntTolBoost-1) ! Begin timestep loop. @@ -1090,7 +1096,7 @@ subroutine CalcVectorSources(EV,taustart) if ( EV%q*tauend > max_etak_vector) then ThisSources%LinearSrc(EV%q_ix,:,j) = 0 else - call dverk(EV,EV%nvarv,derivsv,tau,yv,tauend,tol,ind,c,EV%nvarv,wt) !tauend + call RungeKuttaDP45(EV, EV%nvarv, derivsv, tau, yv, tauend, tol, ind, rk_settings, EV%nvarv, wt) call outputv(EV,yv,EV%nvarv,tau,ThisSources%LinearSrc(EV%q_ix,CT_Temp,j),& ThisSources%LinearSrc(EV%q_ix,CT_E,j),ThisSources%LinearSrc(EV%q_ix,CT_B,j)) @@ -1136,18 +1142,19 @@ subroutine GetTransfer(EV,tau) type(EvolutionVars) EV real(dl) tau integer ind, i - real(dl) c(24),w(EV%nvar,9), y(EV%nvar) + type(RungeKuttaDP45Settings) :: rk_settings + real(dl) w(EV%nvar,9), y(EV%nvar) real(dl) atol atol=base_tol/exp(CP%Accuracy%AccuracyBoost*CP%Accuracy%IntTolBoost-1) - if (CP%Transfer%high_precision) atol=atol/10000 !CHECKTHIS + if (CP%Transfer%high_precision) atol=atol/100 ind=1 call initial(EV,y, tau) if (global_error_flag/=0) return do i=1,State%num_transfer_redshifts - call GaugeInterface_EvolveScal(EV,tau,y,State%Transfer_Times(i),atol,ind,c,w) + call GaugeInterface_EvolveScal(EV, tau, y, State%Transfer_Times(i), atol, ind, rk_settings, w) if (global_error_flag/=0) return call outtransf(EV,y,tau,State%MT%TransferData(:,EV%q_ix,i)) end do diff --git a/fortran/equations.f90 b/fortran/equations.f90 index 00359a66..aabe78ae 100644 --- a/fortran/equations.f90 +++ b/fortran/equations.f90 @@ -31,6 +31,7 @@ module GaugeInterface use results use MassiveNu use DarkEnergyInterface + use RungeKuttaDP45Module, only : RungeKuttaDP45Settings use Transfer implicit none public @@ -202,14 +203,21 @@ subroutine SetActiveState(P) end subroutine SetActiveState - subroutine GaugeInterface_ScalEv(EV,y,tau,tauend,tol1,ind,c,w) + subroutine GaugeInterface_ScalEv(EV,y,tau,tauend,tol1,ind,rk_settings,w) type(EvolutionVars) EV - real(dl) c(24),w(EV%nvar,9), y(EV%nvar), tol1, tau, tauend + type(RungeKuttaDP45Settings), intent(inout) :: rk_settings + real(dl) w(EV%nvar,9), y(EV%nvar), tol1, tau, tauend integer ind - call dverk(EV,EV%ScalEqsToPropagate,derivs,tau,y,tauend,tol1,ind,c,EV%nvar,w) + if (Ev%q < 2e-3_dl .and. tau <= State%taurend .and. CP%WantCls) then + rk_settings%max_step_size = 2._dl + else + rk_settings%max_step_size = 20._dl / CP%Accuracy%IntTolBoost + end if + + call RungeKuttaDP45(EV, EV%ScalEqsToPropagate, derivs, tau, y, tauend, tol1, ind, rk_settings, EV%nvar, w) if (ind==-3) then - call GlobalError('Dverk error -3: the subroutine was unable to satisfy the error ' & + call GlobalError('RungeKuttaDP45 error -3: the subroutine was unable to satisfy the error ' & //'requirement with a particular step-size that is less than or * ' & //'equal to hmin, which may mean that tol is too small' & //'--- but most likely you''ve messed up the y array indexing; ' & @@ -242,10 +250,11 @@ function next_nu_nq(nq) result (next_nq) end function next_nu_nq - recursive subroutine GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,c,w) + recursive subroutine GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,rk_settings,w) use Recombination, only : CB1 type(EvolutionVars) EV, EVout - real(dl) c(24),w(EV%nvar,9), y(EV%nvar), yout(EV%nvar), tol1, tau, tauend + type(RungeKuttaDP45Settings), intent(inout) :: rk_settings + real(dl) w(EV%nvar,9), y(EV%nvar), yout(EV%nvar), tol1, tau, tauend integer ind, nu_i real(dl) cs2, opacity, dopacity real(dl) tau_switch_ktau, tau_switch_nu_massless, tau_switch_nu_massive, next_switch @@ -293,10 +302,13 @@ recursive subroutine GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,c,w) if (CP%DoLateRadTruncation) then if (.not. EV%no_nu_multpoles) & !!.and. .not. EV%has_nu_relativistic .and. tau_switch_nu_massless ==noSwitch) & tau_switch_no_nu_multpoles= & - max(15/EV%k_buf*CP%Accuracy%AccuracyBoost,min(State%taurend,EV%ThermoData%matter_verydom_tau)) + max(15/EV%k_buf*CP%Accuracy%AccuracyBoost*CP%Accuracy%TimeSwitchBoost,& + min(State%taurend,EV%ThermoData%matter_verydom_tau)) - if (.not. EV%no_phot_multpoles .and. (.not.CP%WantCls .or. EV%k_buf>0.03*CP%Accuracy%AccuracyBoost)) & - tau_switch_no_phot_multpoles =max(15/EV%k_buf,State%taurend)*CP%Accuracy%AccuracyBoost + if (.not. EV%no_phot_multpoles .and. (.not.CP%WantCls .or. & + EV%k_buf>0.03*CP%Accuracy%AccuracyBoost*CP%Accuracy%TimeSwitchBoost)) & + tau_switch_no_phot_multpoles =max(15/EV%k_buf,State%taurend)*CP%Accuracy%AccuracyBoost & + *CP%Accuracy%TimeSwitchBoost end if next_switch = min(tau_switch_ktau, tau_switch_nu_massless,EV%TightSwitchoffTime, tau_switch_nu_massive, & @@ -305,7 +317,7 @@ recursive subroutine GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,c,w) if (next_switch < tauend) then if (next_switch > tau+smallTime) then - call GaugeInterface_ScalEv(EV, y, tau,next_switch,tol1,ind,c,w) + call GaugeInterface_ScalEv(EV, y, tau, next_switch, tol1, ind, rk_settings, w) if (global_error_flag/=0) return end if @@ -424,23 +436,25 @@ recursive subroutine GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,c,w) y(EV%Tg_ix) =y(EV%g_ix)/4 ! assume delta_TM = delta_T_gamma end if - call GaugeInterface_EvolveScal(EV,tau,y,tauend,tol1,ind,c,w) + call GaugeInterface_EvolveScal(EV, tau, y, tauend, tol1, ind, rk_settings, w) return end if - call GaugeInterface_ScalEv(EV,y,tau,tauend,tol1,ind,c,w) + call GaugeInterface_ScalEv(EV, y, tau, tauend, tol1, ind, rk_settings, w) end subroutine GaugeInterface_EvolveScal - subroutine GaugeInterface_EvolveTens(EV,tau,y,tauend,tol1,ind,c,w) + subroutine GaugeInterface_EvolveTens(EV,tau,y,tauend,tol1,ind,rk_settings,w) type(EvolutionVars) EV, EVOut - real(dl) c(24),w(EV%nvart,9), y(EV%nvart),yout(EV%nvart), tol1, tau, tauend + type(RungeKuttaDP45Settings), intent(inout) :: rk_settings + real(dl) w(EV%nvart,9), y(EV%nvart),yout(EV%nvart), tol1, tau, tauend integer ind real(dl) opacity, cs2, a if (EV%TensTightCoupling .and. tauend > EV%TightSwitchoffTime) then if (EV%TightSwitchoffTime > tau) then - call dverk(EV,EV%TensEqsToPropagate, derivst,tau,y,EV%TightSwitchoffTime,tol1,ind,c,EV%nvart,w) + call RungeKuttaDP45(EV, EV%TensEqsToPropagate, derivst, tau, y, EV%TightSwitchoffTime, tol1, ind, & + rk_settings, EV%nvart, w) end if EVOut=EV EVOut%TensTightCoupling = .false. @@ -453,7 +467,7 @@ subroutine GaugeInterface_EvolveTens(EV,tau,y,tauend,tol1,ind,c,w) y(EV%E_ix+2) = y(EV%g_ix+2)/4 end if - call dverk(EV,EV%TensEqsToPropagate, derivst,tau,y,tauend,tol1,ind,c,EV%nvart,w) + call RungeKuttaDP45(EV, EV%TensEqsToPropagate, derivst, tau, y, tauend, tol1, ind, rk_settings, EV%nvart, w) end subroutine GaugeInterface_EvolveTens @@ -508,9 +522,9 @@ subroutine GaugeInterface_Init nu_tau_notmassless(j, nu_i) = time end do - a_nonrel = 2.5d0/nu_mass*CP%Accuracy%AccuracyBoost + a_nonrel = 2.5d0/nu_mass*CP%Accuracy%AccuracyBoost*CP%Accuracy%TimeSwitchBoost nu_tau_nonrelativistic(nu_i) =DeltaTimeMaxed(0._dl,a_nonrel) - a_massive = 17.d0/nu_mass*CP%Accuracy%AccuracyBoost + a_massive = 17.d0/nu_mass*CP%Accuracy%AccuracyBoost*CP%Accuracy%TimeSwitchBoost nu_tau_massive(nu_i) =nu_tau_nonrelativistic(nu_i) + DeltaTimeMaxed(a_nonrel,a_massive) end do end associate @@ -1959,11 +1973,14 @@ subroutine initial(EV,y, tau) do nu_i = 1, CP%Nu_mass_eigenstates EV%MassiveNuApproxTime(nu_i) = Nu_tau_massive(nu_i) - a_massive = 20000*k/State%nu_masses(nu_i)*CP%Accuracy%AccuracyBoost*CP%Accuracy%lAccuracyBoost + a_massive = 20000*k/State%nu_masses(nu_i)*CP%Accuracy%AccuracyBoost & + *CP%Accuracy%TimeSwitchBoost*CP%Accuracy%lAccuracyBoost if (a_massive >=0.99) then EV%MassiveNuApproxTime(nu_i)=State%tau0+1 - else if (a_massive > 17.d0/State%nu_masses(nu_i)*CP%Accuracy%AccuracyBoost) then - EV%MassiveNuApproxTime(nu_i)=max(EV%MassiveNuApproxTime(nu_i),State%DeltaTime(0._dl,a_massive, 0.01_dl)) + else if (a_massive > 17.d0/State%nu_masses(nu_i)*CP%Accuracy%AccuracyBoost & + *CP%Accuracy%TimeSwitchBoost) then + EV%MassiveNuApproxTime(nu_i)=max(EV%MassiveNuApproxTime(nu_i), & + State%DeltaTime(0._dl,a_massive, 0.01_dl)) end if ind = EV%nu_ix(nu_i) do i=1,EV%nq(nu_i) diff --git a/fortran/halofit.f90 b/fortran/halofit.f90 index 1dc8fe81..734af277 100644 --- a/fortran/halofit.f90 +++ b/fortran/halofit.f90 @@ -596,6 +596,7 @@ SUBROUTINE HMcode(this,State,CAMB_Pk) !Currently this needs to be done at each z (mainly because of scale-dependent growth with neutrinos) !For non-massive-neutrino models this could only be done once, which would speed things up a bit CALL initialise_HM_cosmology(this,j,cosi,CAMB_PK) + if (global_error_flag/=0) return !Sets the current redshift from the table z=CAMB_Pk%Redshifts(j) @@ -955,6 +956,7 @@ SUBROUTINE fill_plintab(iz,cosm,CAMB_PK) Pk(i)=MatterPowerData_k(CAMB_PK,k(i),iz, index_cache)*(k(i)**3/(2*pi**2)) Pkc(i)=Pk(i)*Tcb_Tcbnu_ratio(k(i),z,cosm)**2 END DO + if (global_error_flag/=0) return IF(HM_verbose) WRITE(*,*) 'LINEAR POWER: Delta2_min:', Pk(1) IF(HM_verbose) WRITE(*,*) 'LINEAR POWER: Delta2_max:', Pk(nk) @@ -1094,6 +1096,7 @@ SUBROUTINE initialise_HM_cosmology(this,iz,cosm,CAMB_PK) !Fill linear power table and grows it to z=0 CALL fill_plintab(iz,cosm,CAMB_PK) + if (global_error_flag/=0) return !Fill sigma(r) table CALL fill_sigtab(this,cosm) diff --git a/fortran/model.f90 b/fortran/model.f90 index cd5a8a72..9df72ccd 100644 --- a/fortran/model.f90 +++ b/fortran/model.f90 @@ -65,6 +65,8 @@ module model real(dl) :: BackgroundTimeStepBoost = 1._dl !number of time steps for background thermal history interpolation + real(dl) :: TimeSwitchBoost = 1._dl !Accuracy for physical time/transition switches + real(dl) :: IntTolBoost = 1._dl !Tolerances for integrating differential equations real(dl) :: SourcekAccuracyBoost = 1._dl !Accuracy of k sampling for source time integration diff --git a/fortran/recfast.f90 b/fortran/recfast.f90 index 337c98c3..44de6ca8 100644 --- a/fortran/recfast.f90 +++ b/fortran/recfast.f90 @@ -97,7 +97,7 @@ !CA Lambda: 2s-1s two photon rate for Hydrogen !CA Lambda_He: 2s-1s two photon rate for Helium !CA DeltaB: energy of first excited state from continuum = 3.4eV - !CA DeltaB_He: energy of first excited state from cont. for He = 3.4eV + !CA DeltaB_He: energy of first excited state from cont. for He = 3.97eV !CA L_H_ion: level for H ionization in m^-1 !CA L_H_alpha: level for H Ly alpha in m^-1 !CA L_He1_ion: level for HeI ionization @@ -146,17 +146,17 @@ !CA tol: tolerance for the integrator - !CA cw(24),w(3,9): work space for DVERK + !CA rk_settings,w(3,9): work space for RungeKuttaDP45 !CA Ndim: number of d.e.'s to solve (integer) !CA Nz: number of output redshitf (integer) !CA I: loop index (integer) - !CA ind,nw: work-space for DVERK (integer) + !CA ind,nw: work-space for RungeKuttaDP45 (integer) !C !CF File & device access: !CF Unit /I,IO,O /Name (if known) !C !CM Modules called: - !CM DVERK (numerical integrator) + !CM RungeKuttaDP45 (numerical integrator) !CM GET_INIT (initial values for ionization fractions) !CM ION (ionization and Temp derivatives) !C @@ -167,7 +167,7 @@ !CH CREATED (simplest version) 19th March 1989 !CH RECREATED 11th January 1995 !CH includes variable Cosmology - !CH uses DVERK integrator + !CH uses RungeKuttaDP45 integrator !CH initial conditions are Saha !CH TESTED a bunch, well, OK, not really !CH MODIFIED January 1995 (include Hummer's 1994 alpha table) @@ -206,11 +206,13 @@ module Recombination + use, intrinsic :: ieee_arithmetic, only : ieee_is_finite use constants use classes use DarkAge21cm use Interpolation, only : spline, SPLINE_DANGLE use MathUtils + use RungeKuttaDP45Module, only : RungeKuttaDP45Settings, TClassRungeKuttaDP45 use Config, only : GlobalError, error_recombination use results use MpiUtils, only : MpiStop @@ -225,14 +227,31 @@ module Recombination logical, parameter :: RECFAST_Hswitch_default = .true. !include H corrections (v1.5, 2010) real(dl), parameter :: RECFAST_fudge_default = 1.14_dl !1.14_dl real(dl), parameter :: RECFAST_fudge_default2 = 1.105d0 + 0.02d0 - integer, parameter :: RECFAST_nz_default = 10000 + integer, parameter :: RECFAST_nz_default = 2046 real(dl), parameter :: RECFAST_x_He_freeze_threshold = 1.e-8_dl + logical, parameter :: RECFAST_use_rosenbrock_default = .true. + real(dl), parameter :: RECFAST_rosenbrock_handoff_xH_default = 0.976_dl + + real(dl), parameter :: ROS2_gamma = 1._dl + 1._dl/sqrt(2._dl) + real(dl), parameter :: ROS2_a21 = 1._dl/ROS2_gamma + real(dl), parameter :: ROS2_c21 = -2._dl/ROS2_gamma + real(dl), parameter :: ROS2_m1 = 3._dl/(2._dl*ROS2_gamma) + real(dl), parameter :: ROS2_m2 = 1._dl/(2._dl*ROS2_gamma) + real(dl), parameter :: ROS2_safety = 0.9_dl + real(dl), parameter :: ROS2_min_scale = 0.2_dl + real(dl), parameter :: ROS2_max_scale = 2.5_dl + ! Keep the ionization fractions on a smaller absolute scale than temperature + ! so full-range Rosenbrock does not tolerate O(1) relative tail errors. + real(dl), parameter :: RECFAST_rosenbrock_ion_scale_floor = 1.e-3_dl + ! Tuned for the fast handoff path; full-range Rosenbrock should use a tighter tol. + real(dl), parameter :: RECFAST_rosenbrock_tol_default = 3.e-4_dl + integer, parameter :: RECFAST_rosenbrock_max_steps = 4096 Type RecombinationData real(dl) :: Recombination_saha_z !Redshift at which saha OK real(dl), private :: NNow, fHe integer, private :: nz = 0 - real(dl), private :: delta_z = 0._dl + real(dl), private :: delta_z = 0._dl, minz = zfinal, maxz = zinitial real(dl), allocatable, private :: zrec(:), xrec(:), dxrec(:), Tsrec(:), dTsrec(:), tmrec(:), dtmrec(:), & xrec_horner(:, :), tsrec_horner(:, :), tmrec_horner(:, :) ! tmrec stores a*Tmat = Tmat/(1+z) @@ -274,6 +293,9 @@ module Recombination real(dl) :: wGauss1= 0.18D0 !Width of 1st Gaussian real(dl) :: wGauss2= 0.33D0 !Width of 2nd Gaussian integer :: Nz = RECFAST_nz_default + logical :: use_rosenbrock = RECFAST_use_rosenbrock_default + real(dl) :: rosenbrock_handoff_xH = RECFAST_rosenbrock_handoff_xH_default + real(dl) :: rosenbrock_tol = RECFAST_rosenbrock_tol_default Type(RecombinationData), allocatable :: Calc contains procedure :: ReadParams => TRecfast_ReadParams @@ -367,6 +389,9 @@ subroutine TRecfast_ReadParams(this, Ini) call Ini%Read('wGauss1',this%wGauss1) call Ini%Read('wGauss2',this%wGauss2) this%Nz = Ini%Read_Int("RECFAST_nz", this%Nz) + this%use_rosenbrock = Ini%Read_Logical("RECFAST_use_rosenbrock", this%use_rosenbrock) + this%rosenbrock_handoff_xH = Ini%Read_Double("RECFAST_rosenbrock_handoff_xH", this%rosenbrock_handoff_xH) + this%rosenbrock_tol = Ini%Read_Double("RECFAST_rosenbrock_tol", this%rosenbrock_tol) if (this%RECFAST_Hswitch) then this%RECFAST_fudge = this%RECFAST_fudge - (RECFAST_fudge_default - RECFAST_fudge_default2) end if @@ -384,6 +409,14 @@ subroutine TRecfast_Validate(this, OK) OK = .false. write(*,*) "RECFAST_nz must be at least 2" end if + if (this%rosenbrock_handoff_xH < 0._dl .or. this%rosenbrock_handoff_xH >= 1._dl) then + OK = .false. + write(*,*) "RECFAST_rosenbrock_handoff_xH must be in [0, 1)" + end if + if (this%rosenbrock_tol <= 0._dl) then + OK = .false. + write(*,*) "RECFAST_rosenbrock_tol must be > 0" + end if end subroutine TRecfast_Validate @@ -391,19 +424,18 @@ function TRecfast_tm(this,a) class(TRecfast) :: this real(dl), intent(in) :: a real(dl) z, zst, TRecfast_tm, aTmat_stored, az - integer ilo,ihi + integer ihi z=1/a-1 associate( Calc => this%Calc) - if (z >= Calc%zrec(1)) then + if (z >= Calc%maxz) then TRecfast_tm=Calc%Tnow/a else - if (z <= zfinal) then + if (z <= Calc%minz) then TRecfast_tm=(1._dl + zfinal)*Calc%Tmrec(Calc%nz) else zst = (zinitial - z)/Calc%delta_z ihi = int(zst) - ilo = ihi + 1 az = zst - real(ihi, dl) aTmat_stored = Calc%tmrec_horner(1, ihi) + az*(Calc%tmrec_horner(2, ihi) + az*(Calc%tmrec_horner(3, ihi) + & az*Calc%tmrec_horner(4, ihi))) @@ -420,19 +452,18 @@ function TRecfast_ts(this,a) !zrec(1) is zinitial-delta_z real(dl), intent(in) :: a real(dl) zst,z,az,TRecfast_ts, aTspin_stored - integer ilo,ihi + integer ihi z=1/a-1 associate(Calc => this%Calc) - if (z.ge.Calc%zrec(1)) then + if (z >= Calc%maxz) then TRecfast_ts=(1._dl + z)*Calc%tsrec(1) else - if (z.le.zfinal) then + if (z <= Calc%minz) then TRecfast_ts=(1._dl + zfinal)*Calc%tsrec(Calc%nz) else zst = (zinitial - z)/Calc%delta_z ihi = int(zst) - ilo = ihi + 1 az = zst - real(ihi, dl) aTspin_stored = Calc%tsrec_horner(1, ihi) + az*(Calc%tsrec_horner(2, ihi) + az*(Calc%tsrec_horner(3, ihi) + & az*Calc%tsrec_horner(4, ihi))) @@ -446,19 +477,18 @@ function TRecfast_xe(this,a) class(TRecfast) :: this real(dl), intent(in) :: a real(dl) zst,z,az,TRecfast_xe - integer ilo,ihi + integer ihi z=1/a-1 associate(Calc => this%Calc) - if (z.ge.Calc%zrec(1)) then + if (z >= Calc%maxz) then TRecfast_xe=Calc%xrec(1) else - if (z.le.zfinal) then + if (z <= Calc%minz) then TRecfast_xe=Calc%xrec(Calc%nz) else zst = (zinitial - z)/Calc%delta_z ihi = int(zst) - ilo = ihi + 1 az = zst - real(ihi, dl) TRecfast_xe = Calc%xrec_horner(1, ihi) + az*(Calc%xrec_horner(2, ihi) + az*(Calc%xrec_horner(3, ihi) + & az*Calc%xrec_horner(4, ihi))) @@ -472,21 +502,20 @@ subroutine TRecfast_xe_Tm(this,a, xe, Tm) real(dl), intent(in) :: a real(dl), intent(out) :: xe, Tm real(dl) z, zst, aTmat_stored, az - integer ilo,ihi + integer ihi z=1/a-1 associate(Calc => this%Calc) - if (z.ge.Calc%zrec(1)) then + if (z >= Calc%maxz) then xe=Calc%xrec(1) Tm = Calc%Tnow/a else - if (z.le.zfinal) then + if (z <= Calc%minz) then xe=Calc%xrec(Calc%nz) Tm = (1._dl + zfinal)*Calc%Tmrec(Calc%nz) else zst = (zinitial - z)/Calc%delta_z ihi = int(zst) - ilo = ihi + 1 az = zst - real(ihi, dl) xe = Calc%xrec_horner(1, ihi) + az*(Calc%xrec_horner(2, ihi) + az*(Calc%xrec_horner(3, ihi) + & az*Calc%xrec_horner(4, ihi))) @@ -516,28 +545,30 @@ subroutine SetRecfastCubicSplineHorner(x, y, y2, horner, n) end subroutine SetRecfastCubicSplineHorner - subroutine EnsureRecfastStorage(this, Calc, OK) - class(TRecfast), intent(in) :: this + subroutine EnsureRecfastStorage(Calc, target_nz, OK) type(RecombinationData), intent(inout) :: Calc + integer, intent(in) :: target_nz logical, intent(out) :: OK logical :: needs_allocate OK = .false. - if (this%Nz < 2) then + if (target_nz < 2) then call GlobalError("recfast_nz must be at least 2", error_recombination) return end if needs_allocate = .not. allocated(Calc%zrec) - if (.not. needs_allocate) needs_allocate = size(Calc%zrec) /= this%Nz + if (.not. needs_allocate) needs_allocate = size(Calc%zrec) /= target_nz if (needs_allocate .and. allocated(Calc%zrec)) then deallocate(Calc%zrec, Calc%xrec, Calc%dxrec, Calc%tsrec, Calc%dtsrec, Calc%tmrec, Calc%dtmrec, & Calc%xrec_horner, Calc%tsrec_horner, Calc%tmrec_horner) end if - Calc%nz = this%Nz + Calc%nz = target_nz Calc%delta_z = (zinitial-zfinal)/real(Calc%nz, dl) + Calc%minz = zfinal + 1.e-12_dl*Calc%delta_z + Calc%maxz = zinitial - Calc%delta_z - 1.e-12_dl*Calc%delta_z if (needs_allocate) then allocate(Calc%zrec(Calc%nz), Calc%xrec(Calc%nz), Calc%dxrec(Calc%nz), Calc%tsrec(Calc%nz), & Calc%dtsrec(Calc%nz), Calc%tmrec(Calc%nz), Calc%dtmrec(Calc%nz), Calc%xrec_horner(4, Calc%nz - 1), & @@ -565,26 +596,28 @@ subroutine TRecfast_init(this,State, WantTSpin) Type(RecombinationData), pointer :: Calc logical, intent(in), optional :: WantTSpin real(dl) :: z,n,x,x0,rhs,x_H,x_He,x_H0,x_He0,H, Yp - real(dl) :: zstart,zend,z_scale - real(dl) :: cw(24) + real(dl) :: zstart,zend,z_scale, rosenbrock_tol, rk45_tol_use, background_step_boost + type(RungeKuttaDP45Settings) :: rk_settings real(dl), dimension(:,:), allocatable :: w - real(dl) :: y(4) + real(dl) :: y(4), y_rosen_start(4) real(dl) :: C10, tau_21Ts - integer :: ind, nw - real(dl), parameter :: tol=1.D-5 !Tolerance for R-K - procedure(TClassDverk) :: dverk - logical :: storage_ok + integer :: ind, nw, internal_nz + real(dl), parameter :: rk45_tol=3D-6 !Input tolerance for RungeKuttaDP45 + procedure(TClassRungeKuttaDP45) :: RungeKuttaDP45 + logical :: storage_ok, rosenbrock_handed_off, rosenbrock_ok if (.not. allocated(this%Calc)) allocate(this%Calc) Calc => this%Calc - call EnsureRecfastStorage(this, Calc, storage_ok) - if (.not. storage_ok) return select type(State) class is (CAMBdata) Calc%State => State Calc%doTspin = DefaultFalse(WantTSpin) + background_step_boost = max(State%CP%Accuracy%BackgroundTimeStepBoost, 1.e-12_dl) + internal_nz = max(2, nint(this%Nz*background_step_boost)) + call EnsureRecfastStorage(Calc, internal_nz, storage_ok) + if (.not. storage_ok) return ! write(*,*)'recfast version 1.0' @@ -629,6 +662,8 @@ subroutine TRecfast_init(this,State, WantTSpin) ! Fudge factor to approximate for low z out of equilibrium effect Calc%fu=this%RECFAST_fudge + rosenbrock_tol = this%rosenbrock_tol/State%CP%Accuracy%IntTolBoost + rk45_tol_use = rk45_tol/State%CP%Accuracy%IntTolBoost ! Set initial matter temperature. y(3) stores a*Tmat directly. Tmat = Calc%Tnow*ainv !Initial rad. & mat. temperature @@ -643,12 +678,11 @@ subroutine TRecfast_init(this,State, WantTSpin) ! OK that's the initial conditions, now start writing output file - ! Set up work-space stuff for DVERK + ! Set up work-space stuff for RungeKuttaDP45 ind = 1 nw = Calc%n_eq - do i = 1,24 - cw(i) = 0._dl - end do + rosenbrock_handed_off = .not. this%use_rosenbrock + rk_settings = RungeKuttaDP45Settings() do i = 1,Calc%nz ! calculate the start and end redshift for the interval at each z @@ -665,6 +699,9 @@ subroutine TRecfast_init(this,State, WantTSpin) z = zend ainv = 1._dl + z z_scale = Calc%Tnow/COBE_CMBTemp*ainv - 1 + if (.not. rosenbrock_handed_off .and. y(1) <= this%rosenbrock_handoff_xH) then + rosenbrock_handed_off = .true. + end if if (z_scale > 8000._dl) then @@ -717,18 +754,49 @@ subroutine TRecfast_init(this,State, WantTSpin) y(3) = Calc%Tnow y(4) = Calc%Tnow + else if (.not. rosenbrock_handed_off) then + + ! Integrate the full smooth H/He/Tm system with Rosenbrock until the + ! hydrogen fraction has dropped enough to hand back to RungeKuttaDP45. + y_rosen_start = y + call RecfastRosenbrockAdvance(this, zstart, y(1:Calc%n_eq), zend, rosenbrock_tol, & + rosenbrock_ok) + if (.not. rosenbrock_ok) return + if (.not. Evolve_Ts) y(4) = y(3) + if (y(1) <= this%rosenbrock_handoff_xH) then + ! Snap the handoff to the higher-redshift grid node by redoing the + ! crossing interval with the original RungeKuttaDP45 evolution. + y = y_rosen_start + rosenbrock_handed_off = .true. + if (y(1) > 0.99d0) then + rhs = exp(1.5d0*log(CR*Calc%Tnow/ainv) - CB1/(Calc%Tnow*ainv)) / Calc%Nnow + x_H0 = 0.5d0 * (sqrt(rhs**2 + 4._dl*rhs) - rhs) + + call RungeKuttaDP45(this, 3, ION, zstart, y, zend, rk45_tol_use, ind, rk_settings, nw, w) + y(1) = x_H0 + x0 = y(1) + Calc%fHe*y(2) + y(4) = y(3) + else + call RungeKuttaDP45(this, nw, ION, zstart, y, zend, rk45_tol_use, ind, rk_settings, nw, w) + x0 = y(1) + Calc%fHe*y(2) + end if + else + x0 = y(1) + Calc%fHe*y(2) + if (y(1) > 0.985d0) Calc%Recombination_saha_z = zend + end if + else if (y(1) > 0.99d0) then rhs = exp(1.5d0*log(CR*Calc%Tnow/ainv) - CB1/(Calc%Tnow*ainv)) / Calc%Nnow x_H0 = 0.5d0 * (sqrt( rhs**2+4._dl*rhs ) - rhs ) - call DVERK(this,3,ION,zstart,y,zend,tol,ind,cw,nw,w) + call RungeKuttaDP45(this, 3, ION, zstart, y, zend, rk45_tol_use, ind, rk_settings, nw, w) y(1) = x_H0 x0 = y(1) + Calc%fHe*y(2) y(4)=y(3) else - call DVERK(this,nw,ION,zstart,y,zend,tol,ind,cw,nw,w) + call RungeKuttaDP45(this, nw, ION, zstart, y, zend, rk45_tol_use, ind, rk_settings, nw, w) x0 = y(1) + Calc%fHe*y(2) @@ -830,25 +898,287 @@ subroutine GET_INIT(Calc,z,x_H0,x_He0,x0) end subroutine GET_INIT - subroutine ION(this,Ndim,z,Y,f) + subroutine EscapeProbabilityAndDerivative(tau, p_escape, dp_dtau) + real(dl), intent(in) :: tau + real(dl), intent(out) :: p_escape, dp_dtau + real(dl) :: tau2 + + if (abs(tau) < 1.e-6_dl) then + tau2 = tau*tau + p_escape = 1._dl - tau/2._dl + tau2/6._dl + dp_dtau = -0.5_dl + tau/3._dl - tau2/8._dl + else + p_escape = (1._dl - exp(-tau))/tau + dp_dtau = (exp(-tau)*(tau + 1._dl) - 1._dl)/(tau*tau) + end if + + end subroutine EscapeProbabilityAndDerivative + + subroutine SolveSmallLinearSystem(matrix, rhs, solution, ok) + real(dl), intent(in) :: matrix(:, :), rhs(:) + real(dl), intent(out) :: solution(:) + logical, intent(out) :: ok + real(dl) :: a(size(rhs), size(rhs)), b(size(rhs)) + real(dl) :: factor, maxabs, temp, pivot_scale + integer :: i, j, k, n, pivot + + n = size(rhs) + a = matrix + b = rhs + ok = .false. + if (.not. all(ieee_is_finite(a)) .or. .not. all(ieee_is_finite(b))) return + + do k = 1, n - 1 + pivot = k + maxabs = abs(a(k, k)) + do i = k + 1, n + if (abs(a(i, k)) > maxabs) then + pivot = i + maxabs = abs(a(i, k)) + end if + end do + pivot_scale = max(1._dl, maxval(abs(a(pivot, k:n)))) + if (maxabs <= 100._dl*epsilon(1._dl)*pivot_scale) return + if (pivot /= k) then + do j = 1, n + temp = a(k, j) + a(k, j) = a(pivot, j) + a(pivot, j) = temp + end do + temp = b(k) + b(k) = b(pivot) + b(pivot) = temp + end if + do i = k + 1, n + factor = a(i, k)/a(k, k) + a(i, k) = 0._dl + do j = k + 1, n + a(i, j) = a(i, j) - factor*a(k, j) + end do + b(i) = b(i) - factor*b(k) + end do + end do + + pivot_scale = max(1._dl, maxval(abs(a(n, :)))) + if (abs(a(n, n)) <= 100._dl*epsilon(1._dl)*pivot_scale) return + + solution(n) = b(n)/a(n, n) + do i = n - 1, 1, -1 + solution(i) = b(i) + do j = i + 1, n + solution(i) = solution(i) - a(i, j)*solution(j) + end do + solution(i) = solution(i)/a(i, i) + end do + if (.not. all(ieee_is_finite(solution))) return + ok = .true. + + end subroutine SolveSmallLinearSystem + + logical function RecfastRosenbrockStateOK(y) + real(dl), intent(in) :: y(:) + + RecfastRosenbrockStateOK = all(ieee_is_finite(y)) + if (.not. RecfastRosenbrockStateOK) return + if (size(y) >= 2) then + RecfastRosenbrockStateOK = minval(y(1:2)) >= -1.e-8_dl + if (.not. RecfastRosenbrockStateOK) return + end if + RecfastRosenbrockStateOK = y(3) > 0._dl + + end function RecfastRosenbrockStateOK + + subroutine EvaluateRecfastODETimeDerivative(this, Ndim, z, y, h, f_t, force_full_hydrogen) class(TRecfast), target :: this - integer Ndim + integer, intent(in) :: Ndim + real(dl), intent(in) :: z, y(Ndim), h + real(dl), intent(out) :: f_t(Ndim) + logical, intent(in), optional :: force_full_hydrogen + real(dl) :: delta_z, delta_z_floor, z_hi, z_lo + real(dl) :: f_hi(Ndim), f_lo(Ndim) + logical :: full_hydrogen + + full_hydrogen = .false. + if (present(force_full_hydrogen)) full_hydrogen = force_full_hydrogen + + delta_z = min(0.05_dl, max(1.e-4_dl, 1.e-5_dl*max(1._dl, abs(z)))) + if (abs(h) > 0._dl) then + delta_z = min(delta_z, 0.5_dl*abs(h)) + delta_z_floor = 1.e-8_dl*max(1._dl, abs(z)) + delta_z = max(delta_z, delta_z_floor) + end if + z_hi = min(zinitial, z + delta_z) + z_lo = max(zfinal, z - delta_z) + + if (z_hi > z_lo) then + call EvaluateRecfastODE(this, Ndim, z_hi, y, f_hi, full_hydrogen) + call EvaluateRecfastODE(this, Ndim, z_lo, y, f_lo, full_hydrogen) + f_t = (f_hi - f_lo)/(z_hi - z_lo) + else + f_t = 0._dl + end if + + end subroutine EvaluateRecfastODETimeDerivative + + subroutine RecfastROS2Step(this, z, y, h, yout, yerr, ok) + class(TRecfast), target :: this + real(dl), intent(in) :: z, y(:), h + real(dl), intent(out) :: yout(:) + real(dl), intent(out) :: yerr(:) + logical, intent(out) :: ok + real(dl) :: f(size(y)), f_stage(size(y)), jac(size(y), size(y)), matrix(size(y), size(y)) + real(dl) :: f_t(size(y)), k1(size(y)), k2(size(y)), rhs(size(y)), y_stage(size(y)) + real(dl) :: gamma_h, gamma_h2 + integer :: i, n + + n = size(y) + ok = .false. + + ! Jacobians are only consumed by the Rosenbrock path, which always uses the + ! full smooth hydrogen system rather than the old piecewise H switch. + call EvaluateRecfastODE(this, n, z, y, f, .true., jac) + call EvaluateRecfastODETimeDerivative(this, n, z, y, h, f_t, .true.) + matrix = -ROS2_gamma*h*jac + do i = 1, n + matrix(i, i) = matrix(i, i) + 1._dl + end do + gamma_h = ROS2_gamma*h + gamma_h2 = gamma_h*gamma_h + + rhs = gamma_h*f + gamma_h2*f_t + call SolveSmallLinearSystem(matrix, rhs, k1, ok) + if (.not. ok) return + + y_stage = y + ROS2_a21*k1 + call EvaluateRecfastODE(this, n, z + h, y_stage, f_stage, .true.) + + rhs = gamma_h*f_stage + ROS2_gamma*ROS2_c21*k1 - gamma_h2*f_t + call SolveSmallLinearSystem(matrix, rhs, k2, ok) + if (.not. ok) return + + yout = y + ROS2_m1*k1 + ROS2_m2*k2 + yerr = (k1 + k2)/(2._dl*ROS2_gamma) + ok = RecfastRosenbrockStateOK(yout) .and. all(ieee_is_finite(yerr)) + + end subroutine RecfastROS2Step + + subroutine RecfastRosenbrockAdvance(this, zstart, y, zend, tol, ok) + class(TRecfast), target :: this + real(dl), intent(in) :: zstart, zend, tol + real(dl), intent(inout) :: y(:) + logical, intent(out) :: ok + real(dl) :: direction, err, factor, min_step, scale, step, z + real(dl) :: y_err(size(y)), y_trial(size(y)) + integer :: attempt, i + logical :: step_ok + + ok = .false. + if (zend == zstart) then + ok = .true. + return + end if + + direction = sign(1._dl, zend - zstart) + z = zstart + step = zend - zstart + min_step = max(abs(step)*1.e-8_dl, 1.e-10_dl) + + do attempt = 1, RECFAST_rosenbrock_max_steps + if (direction*(zend - z) <= 0._dl) then + ok = .true. + return + end if + if (direction*(zend - (z + step)) < 0._dl) step = zend - z + + call RecfastROS2Step(this, z, y, step, y_trial, y_err, step_ok) + + if (step_ok) then + err = 0._dl + do i = 1, size(y) + if (i <= 2) then + scale = max(RECFAST_rosenbrock_ion_scale_floor, abs(y(i)), abs(y_trial(i))) + else + scale = max(1._dl, abs(y(i)), abs(y_trial(i))) + end if + err = max(err, abs(y_err(i))/scale) + end do + if (.not. ieee_is_finite(err)) step_ok = .false. + end if + + if (step_ok .and. err <= tol) then + y = y_trial + y(1:min(2, size(y))) = max(y(1:min(2, size(y))), 0._dl) + z = z + step + if (direction*(zend - z) <= 0._dl) then + ok = .true. + return + end if + if (err == 0._dl) then + factor = ROS2_max_scale + else + factor = ROS2_safety*(tol/err)**0.5_dl + factor = min(ROS2_max_scale, max(ROS2_min_scale, factor)) + end if + step = direction*min(abs(step)*factor, abs(zend - z)) + else + if (step_ok) then + factor = ROS2_safety*(tol/max(err, tiny(1._dl)))**0.5_dl + factor = min(0.9_dl, max(ROS2_min_scale, factor)) + else + factor = ROS2_min_scale + end if + step = step*factor + if (abs(step) < min_step) return + end if + end do + + end subroutine RecfastRosenbrockAdvance - real(dl) z,x,n,n_He,Trad,Tmat,Tspin,x_H,x_He, Hz, aTmat, ainv, aTs - real(dl) y(Ndim),f(Ndim) - real(dl) Rup,Rdown,K,K_He,Rup_He,Rdown_He,He_Boltz - real(dl) timeTh,timeH - real(dl) a_VF,b_VF,T_0,T_1,sq_0,sq_1,a_PPB,b_PPB,c_PPB,d_PPB - real(dl) tauHe_s,pHe_s - real(dl) a_trip,b_trip,Rdown_trip,Rup_trip - real(dl) Doppler,gamma_2Ps,pb,qb,AHcon - real(dl) tauHe_t,pHe_t,CL_PSt,CfHe_t,gamma_2Pt - real(dl) epsilon, daTmat_dz, dTspin_dz - integer Heflag - real(dl) C10, dHdz, z_scale + recursive subroutine EvaluateRecfastODE(this, Ndim, z, y, f, force_full_hydrogen, jacobian) + class(TRecfast), target :: this + integer, intent(in) :: Ndim + real(dl), intent(in) :: z, y(Ndim) + real(dl), intent(out) :: f(Ndim) + logical, intent(in), optional :: force_full_hydrogen + real(dl), intent(out), optional :: jacobian(Ndim, Ndim) + real(dl) :: x, n, n_He, Trad, Tmat, Tspin, x_H, x_He, Hz, aTmat, ainv, aTs + real(dl) :: Rup, Rdown, K, K_He, Rup_He, Rdown_He, He_Boltz + real(dl) :: timeTh, timeH + real(dl) :: a_VF, b_VF, T_0, T_1, sq_0, sq_1, a_PPB, b_PPB, c_PPB, d_PPB + real(dl) :: tauHe_s, pHe_s, dpHe_s_dtau + real(dl) :: a_trip, b_trip, Rdown_trip, Rup_trip + real(dl) :: Doppler, gamma_2Ps, pb, qb, AHcon + real(dl) :: tauHe_t, pHe_t, dpHe_t_dtau, CL_PSt, CfHe_t, gamma_2Pt, AHcon_t + real(dl) :: epsilon, daTmat_dz, dTspin_dz + real(dl) :: C10, dHdz, z_scale + real(dl) :: A_H, A_H_xH, A_H_xHe, A_H_T, B_H, C_H, C_H_xH, C_H_T + real(dl) :: A_He, A_He_xH, A_He_xHe, A_He_T + real(dl) :: A_trip_term, A_trip_xH, A_trip_xHe, A_trip_T + real(dl) :: BHe, BHe_xH, BHe_xHe, BHe_T, CHe, CHe_xH, CHe_xHe, CHe_T + real(dl) :: K_He_xH, K_He_xHe, K_He_T + real(dl) :: AHcon_xH, AHcon_xHe, AHcon_dT + real(dl) :: AHcon_t_xH, AHcon_t_xHe, AHcon_t_dT + real(dl) :: dlnRdown, dRdown, dRup, dRupE + real(dl) :: dlnRdown_He, dRdown_He, dRup_He, dHe_Boltz, dRupHeE + real(dl) :: dlnRdown_trip, dRdown_trip, dRup_trip, dRupTripE, dEPSt + real(dl) :: denH, EHe, EPSt, ETrip, LHe, LHe_xHe, LHe_T + real(dl) :: MHe, MHe_xHe, MHe_T, pHe_s_xHe, pHe_t_xHe + real(dl) :: RupE, RupHeE, RupTripE, S, S_T, eps_x, P, P_xH, P_xHe + real(dl) :: Q, Q_xH, Q_xHe, Q_T, coupling_prefac, loose_prefac + real(dl) :: Trip_source, Trip_source_xH, Trip_source_xHe, Trip_source_T + real(dl) :: CfHe_t_xH, CfHe_t_xHe, CfHe_t_T, ypert_plus(Ndim), ypert_minus(Ndim), fpert_plus(Ndim), & + fpert_minus(Ndim), delta + real(dl) :: tauHe_s_const, tauHe_t_const + integer :: Heflag, col + logical :: full_hydrogen type(RecombinationData), pointer :: Recomb Recomb => this%Calc + full_hydrogen = .false. + if (present(force_full_hydrogen)) full_hydrogen = force_full_hydrogen + + f = 0._dl + if (present(jacobian)) jacobian = 0._dl ! the Pequignot, Petitjean & Boisson fitting parameters for Hydrogen a_PPB = 4.309d0 @@ -859,18 +1189,16 @@ subroutine ION(this,Ndim,z,Y,f) ! fixed to match those in the SSS papers, and now correct a_VF = 10.d0**(-16.744d0) b_VF = 0.711d0 - T_0 = 10.d0**(0.477121d0) !3K + T_0 = 10.d0**(0.477121d0) T_1 = 10.d0**(5.114d0) ! fitting parameters for HeI triplets ! (matches Hummer's table with <1% error for 10^2.8 < T/K < 10^4) - a_trip = 10.d0**(-16.306d0) b_trip = 0.761D0 - x_H = y(1) x_He = y(2) - x = x_H + Recomb%fHe * x_He + x = x_H + Recomb%fHe*x_He ainv = 1._dl + z aTmat = y(3) Tmat = ainv*aTmat @@ -878,193 +1206,377 @@ subroutine ION(this,Ndim,z,Y,f) n = Recomb%Nnow*ainv**3 n_He = Recomb%fHe*Recomb%Nnow*ainv**3 Trad = Recomb%Tnow*ainv - - Hz = ainv**2/dtauda(Recomb%State,1/ainv)/MPC_in_sec - + Hz = ainv**2/dtauda(Recomb%State, 1/ainv)/MPC_in_sec + denH = Hz*ainv ! Get the radiative rates using PPQ fit, identical to Hummer's table - - Rdown=1.d-19*a_PPB*(Tmat/1.d4)**b_PPB & - /(1._dl+c_PPB*(Tmat/1.d4)**d_PPB) - Rup = Rdown * (CR*Tmat)**(1.5d0)*exp(-CDB/Tmat) + Rdown = 1.d-19*a_PPB*(Tmat/1.d4)**b_PPB/(1._dl + c_PPB*(Tmat/1.d4)**d_PPB) + Rup = Rdown*(CR*Tmat)**1.5d0*exp(-CDB/Tmat) ! calculate He using a fit to a Verner & Ferland type formula sq_0 = sqrt(Tmat/T_0) sq_1 = sqrt(Tmat/T_1) ! typo here corrected by Wayne Hu and Savita Gahlaut - Rdown_He = a_VF/(sq_0*(1.d0+sq_0)**(1.d0-b_VF)) - Rdown_He = Rdown_He/(1.d0+sq_1)**(1.d0+b_VF) - Rup_He = Rdown_He*(CR*Tmat)**(1.5d0)*exp(-CDB_He/Tmat) - Rup_He = 4.d0*Rup_He !statistical weights factor for HeI + Rdown_He = a_VF/(sq_0*(1.d0 + sq_0)**(1.d0 - b_VF)) + Rdown_He = Rdown_He/(1.d0 + sq_1)**(1.d0 + b_VF) + Rup_He = 4.d0*Rdown_He*(CR*Tmat)**1.5d0*exp(-CDB_He/Tmat) ! Avoid overflow (pointed out by Jacques Roland) - if((Bfact/Tmat) > 680.d0)then + if ((Bfact/Tmat) > 680.d0) then He_Boltz = exp(680.d0) else He_Boltz = exp(Bfact/Tmat) end if + ! now deal with H and its fudges if (.not. this%RECFAST_Hswitch) then - K = CK/Hz !Peebles coefficient K=lambda_a^3/8piH + K = CK/Hz else !c fit a double Gaussian correction function - z_scale = this%Calc%Tnow/COBE_CMBTemp*ainv-1 - K = CK/Hz*(1.0d0 & - +this%AGauss1*exp(-((log(1.0d0+z_scale)-this%zGauss1)/this%wGauss1)**2.d0) & - +this%AGauss2*exp(-((log(1.0d0+z_scale)-this%zGauss2)/this%wGauss2)**2.d0)) + z_scale = this%Calc%Tnow/COBE_CMBTemp*ainv - 1 + K = CK/Hz*(1.0d0 + this%AGauss1*exp(-((log(1.0d0 + z_scale) - this%zGauss1)/this%wGauss1)**2.d0) & + + this%AGauss2*exp(-((log(1.0d0 + z_scale) - this%zGauss2)/this%wGauss2)**2.d0)) end if - ! add the HeI part, using same T_0 and T_1 values - Rdown_trip = a_trip/(sq_0*(1.d0+sq_0)**(1.0-b_trip)) - Rdown_trip = Rdown_trip/((1.d0+sq_1)**(1.d0+b_trip)) - Rup_trip = Rdown_trip*dexp(-h_P*C*L_He2St_ion/(k_B*Tmat)) - Rup_trip = Rup_trip*((CR*Tmat)**(1.5d0))*(4.d0/3.d0) + Rdown_trip = a_trip/(sq_0*(1.d0 + sq_0)**(1.0d0 - b_trip)) + Rdown_trip = Rdown_trip/(1.d0 + sq_1)**(1.d0 + b_trip) + Rup_trip = Rdown_trip*exp(-h_P*C*L_He2St_ion/(k_B*Tmat))*(CR*Tmat)**1.5d0*(4.d0/3.d0) ! last factor here is the statistical weight ! try to avoid "NaN" when x_He gets too small - if ((x_He < RECFAST_x_He_freeze_threshold) .or. (x_He.gt.0.98d0)) then + if ((x_He < RECFAST_x_He_freeze_threshold) .or. (x_He > 0.98d0)) then Heflag = 0 else Heflag = this%RECFAST_Heswitch end if - if (Heflag.eq.0)then !use Peebles coeff. for He - K_He = CK_He/Hz - else !for Heflag>0 !use Sobolev escape probability - tauHe_s = A2P_s*CK_He*3.d0*n_He*(1.d0-x_He)/Hz - pHe_s = (1.d0 - dexp(-tauHe_s))/tauHe_s - K_He = 1.d0/(A2P_s*pHe_s*3.d0*n_He*(1.d0-x_He)) - ! if (((Heflag.eq.2) .or. (Heflag.ge.5)) .and. x_H < 0.99999d0) then - if (((Heflag.eq.2) .or. (Heflag.ge.5)) .and. x_H < 0.9999999d0) then + + tauHe_s = 0._dl + pHe_s = 1._dl + dpHe_s_dtau = -0.5_dl + K_He = CK_He/Hz + AHcon = 0._dl + AHcon_t = 0._dl + CfHe_t = 0._dl + CL_PSt = 0._dl + EPSt = 0._dl + + !use Peebles coeff. for He by default; for Heflag>0 use Sobolev escape probability + if (Heflag /= 0) then + tauHe_s = A2P_s*CK_He*3.d0*n_He*(1.d0 - x_He)/Hz + call EscapeProbabilityAndDerivative(tauHe_s, pHe_s, dpHe_s_dtau) + K_He = 1.d0/(A2P_s*pHe_s*3.d0*n_He*(1.d0 - x_He)) + if (((Heflag == 2) .or. (Heflag >= 5)) .and. x_H < 0.9999999d0) then !AL changed July 08 to get smoother Helium ! use fitting formula for continuum opacity of H ! first get the Doppler width parameter - Doppler = 2.D0*k_B*Tmat/(m_H*not4*C*C) - Doppler = C*L_He_2p*dsqrt(Doppler) - gamma_2Ps = 3.d0*A2P_s*Recomb%fHe*(1.d0-x_He)*C*C & - /(dsqrt(const_pi)*sigma_He_2Ps*const_eightpi*Doppler*(1.d0-x_H)) & - /((C*L_He_2p)**2.d0) - pb = 0.36d0 !value from KIV (2007) + Doppler = 2.d0*k_B*Tmat/(m_H*not4*C*C) + Doppler = C*L_He_2p*sqrt(Doppler) + gamma_2Ps = 3.d0*A2P_s*Recomb%fHe*(1.d0 - x_He)*C*C + gamma_2Ps = gamma_2Ps/(sqrt(const_pi)*sigma_He_2Ps*const_eightpi*Doppler*(1.d0 - x_H)) + gamma_2Ps = gamma_2Ps/((C*L_He_2p)**2) + pb = 0.36d0 qb = this%RECFAST_fudge_He ! calculate AHcon, the value of A*p_(con,H) for H continuum opacity - AHcon = A2P_s/(1.d0+pb*(gamma_2Ps**qb)) - K_He=1.d0/((A2P_s*pHe_s+AHcon)*3.d0*n_He*(1.d0-x_He)) + AHcon = A2P_s/(1.d0 + pb*(gamma_2Ps**qb)) + K_He = 1.d0/((A2P_s*pHe_s + AHcon)*3.d0*n_He*(1.d0 - x_He)) end if - if (Heflag.ge.3) then !include triplet effects - tauHe_t = A2P_t*n_He*(1.d0-x_He)*3.d0 - tauHe_t = tauHe_t /(const_eightpi*Hz*L_He_2Pt**(3.d0)) - pHe_t = (1.d0 - dexp(-tauHe_t))/tauHe_t + !include triplet effects + if (Heflag >= 3) then + tauHe_t = A2P_t*n_He*(1.d0 - x_He)*3.d0 + tauHe_t = tauHe_t/(const_eightpi*Hz*L_He_2Pt**3) + call EscapeProbabilityAndDerivative(tauHe_t, pHe_t, dpHe_t_dtau) CL_PSt = h_P*C*(L_He_2Pt - L_He_2st)/k_B - if ((Heflag.eq.3) .or. (Heflag.eq.5).or.(x_H.gt.0.99999d0)) then !Recfast 1.4.2 (?) - ! if ((Heflag.eq.3) .or. (Heflag.eq.5) .or. x_H >= 0.9999999d0) then !no H cont. effect - CfHe_t = A2P_t*pHe_t*dexp(-CL_PSt/Tmat) - CfHe_t = CfHe_t/(Rup_trip+CfHe_t) !"C" factor for triplets - else !include H cont. effect + EPSt = exp(-CL_PSt/Tmat) + !Recfast 1.4.2 (?) + if ((Heflag == 3) .or. (Heflag == 5) .or. (x_H > 0.99999d0)) then + CfHe_t = A2P_t*pHe_t*EPSt + CfHe_t = CfHe_t/(Rup_trip + CfHe_t) + else + !include H cont. effect Doppler = 2.d0*k_B*Tmat/(m_H*not4*C*C) - Doppler = C*L_He_2Pt*dsqrt(Doppler) - gamma_2Pt = 3.d0*A2P_t*Recomb%fHe*(1.d0-x_He)*C*C & - /(dsqrt(const_pi)*sigma_He_2Pt*const_eightpi*Doppler*(1.d0-x_H)) & - /((C*L_He_2Pt)**2.d0) + Doppler = C*L_He_2Pt*sqrt(Doppler) + gamma_2Pt = 3.d0*A2P_t*Recomb%fHe*(1.d0 - x_He)*C*C + gamma_2Pt = gamma_2Pt/(sqrt(const_pi)*sigma_He_2Pt*const_eightpi*Doppler*(1.d0 - x_H)) + gamma_2Pt = gamma_2Pt/((C*L_He_2Pt)**2) ! use the fitting parameters from KIV (2007) in this case pb = 0.66d0 qb = 0.9d0 - AHcon = A2P_t/(1.d0+pb*gamma_2Pt**qb)/3.d0 - CfHe_t = (A2P_t*pHe_t+AHcon)*dexp(-CL_PSt/Tmat) - CfHe_t = CfHe_t/(Rup_trip+CfHe_t) !"C" factor for triplets + AHcon_t = A2P_t/(1.d0 + pb*gamma_2Pt**qb)/3.d0 + CfHe_t = (A2P_t*pHe_t + AHcon_t)*EPSt + CfHe_t = CfHe_t/(Rup_trip + CfHe_t) end if end if end if - ! Estimates of Thomson scattering time and Hubble time - timeTh=(1._dl/(CT*Trad**4))*(1._dl+x+Recomb%fHe)/x !Thomson time - timeH=2./(3.*Recomb%HO*ainv**1.5) !Hubble time + timeTh = (1._dl/(CT*Trad**4))*(1._dl + x + Recomb%fHe)/x + timeH = 2._dl/(3._dl*Recomb%HO*ainv**1.5) ! calculate the derivatives ! turn on H only for x_H<0.99, and use Saha derivative for 0.98 0.99) then !don't change at all - f(1) = 0._dl - !! else if (x_H > 0.98_dl) then - else if (x_H.gt.0.985d0) then !use Saha rate for Hydrogen - f(1) = (x*x_H*n*Rdown - Rup*(1.d0-x_H)*dexp(-CL/Tmat)) /(Hz*ainv) - Recomb%Recombination_saha_z = z - !AL: following commented as not used - ! for interest, calculate the correction factor compared to Saha - ! (without the fudge) - ! factor=(1.d0 + K*Lambda*n*(1.d0-x_H)) - ! /(Hz*(1.d0+z)*(1.d0+K*Lambda*n*(1.d0-x) - ! +K*Rup*n*(1.d0-x))) - else !use full rate for H - - f(1) = ((x*x_H*n*Rdown - Rup*(1.d0-x_H)*exp(-CL/Tmat)) & - *(1.d0 + K*Lambda*n*(1.d0-x_H))) & - /(Hz*ainv*(1.d0/Recomb%fu+K*Lambda*n*(1.d0-x_H)/Recomb%fu & - +K*Rup*n*(1.d0-x_H))) - + RupE = Rup*exp(-CL/Tmat) + A_H = x*x_H*n*Rdown - RupE*(1.d0 - x_H) + if (.not. full_hydrogen) then + if (x_H > 0.99d0) then + f(1) = 0._dl + else if (x_H > 0.985d0) then + f(1) = A_H/denH + Recomb%Recombination_saha_z = z + else + B_H = 1.d0 + K*Lambda*n*(1.d0 - x_H) + C_H = 1.d0/Recomb%fu + K*Lambda*n*(1.d0 - x_H)/Recomb%fu + K*Rup*n*(1.d0 - x_H) + f(1) = A_H*B_H/(denH*C_H) + end if + else + B_H = 1.d0 + K*Lambda*n*(1.d0 - x_H) + C_H = 1.d0/Recomb%fu + K*Lambda*n*(1.d0 - x_H)/Recomb%fu + K*Rup*n*(1.d0 - x_H) + f(1) = A_H*B_H/(denH*C_H) end if ! turn off the He once it is small if (x_He < RECFAST_x_He_freeze_threshold) then - f(2)=0.d0 + f(2) = 0._dl else - - f(2) = ((x*x_He*n*Rdown_He & - - Rup_He*(1-x_He)*exp(-CL_He/Tmat)) & - *(1 + K_He*Lambda_He*n_He*(1.d0-x_He)*He_Boltz)) & - /(Hz*ainv & - * (1 + K_He*(Lambda_He+Rup_He)*n_He*(1.d0-x_He)*He_Boltz)) - - ! Modification to HeI recombination including channel via triplets - if (Heflag.ge.3) then - f(2) = f(2)+ (x*x_He*n*Rdown_trip & - - (1.d0-x_He)*3.d0*Rup_trip*dexp(-h_P*C*L_He_2st/(k_B*Tmat))) & - *CfHe_t/(Hz*ainv) + EHe = exp(-CL_He/Tmat) + RupHeE = Rup_He*EHe + A_He = x*x_He*n*Rdown_He - RupHeE*(1.d0 - x_He) + LHe = Lambda_He*n_He*(1.d0 - x_He)*He_Boltz + MHe = (Lambda_He + Rup_He)*n_He*(1.d0 - x_He)*He_Boltz + BHe = 1.d0 + K_He*LHe + CHe = 1.d0 + K_He*MHe + f(2) = A_He*BHe/(denH*CHe) + + if (Heflag >= 3) then + ETrip = exp(-h_P*C*L_He_2st/(k_B*Tmat)) + RupTripE = 3.d0*Rup_trip*ETrip + A_trip_term = x*x_He*n*Rdown_trip - (1.d0 - x_He)*RupTripE + f(2) = f(2) + A_trip_term*CfHe_t/denH end if - end if if (timeTh < H_frac*timeH) then ! Original RECFAST formula here is for dTmat/dz; written directly for aTmat. ! The first term is the exact tightly-coupled limit aTmat -> Tnow. - dHdz = (Recomb%HO**2/2.d0/Hz)*(4.d0*ainv**3/(1.d0+Recomb%z_eq)*Recomb%OmegaT & - + 3.d0*Recomb%OmegaT*ainv**2 + 2.d0*Recomb%OmegaK*ainv ) + dHdz = (Recomb%HO**2/2.d0/Hz)*(4.d0*ainv**3/(1.d0 + Recomb%z_eq)*Recomb%OmegaT & + + 3.d0*Recomb%OmegaT*ainv**2 + 2.d0*Recomb%OmegaK*ainv) - epsilon = Hz*(1.d0+x+Recomb%fHe)/(CT*Trad**3*x) + epsilon = Hz*(1.d0 + x + Recomb%fHe)/(CT*Trad**3*x) daTmat_dz = (Recomb%Tnow - aTmat)/ainv & - + epsilon*((1.d0+Recomb%fHe)/(1.d0+Recomb%fHe+x))*((f(1)+Recomb%fHe*f(2))/x)/ainv & - - epsilon*dHdz/(Hz*ainv) + 3.0d0*epsilon/ainv**2 - + + epsilon*((1.d0 + Recomb%fHe)/(1.d0 + Recomb%fHe + x))*((f(1) + Recomb%fHe*f(2))/x)/ainv & + - epsilon*dHdz/(Hz*ainv) + 3.d0*epsilon/ainv**2 else ! Original RECFAST formula is for dTmat/dz. Using Tmat=(1+z)*aTmat and Trad=(1+z)*Tnow gives: ! d(aTmat)/dz = [CT*Trad^4*x*(aTmat-Tnow)/(Hz*(1+x+fHe)) + aTmat]/(1+z). - daTmat_dz = (CT*(Trad**4)*x*(aTmat-Recomb%Tnow)/(Hz*(1._dl+x+Recomb%fHe)) + aTmat)/ainv + daTmat_dz = (CT*Trad**4*x*(aTmat - Recomb%Tnow)/(Hz*(1.d0 + x + Recomb%fHe)) + aTmat)/ainv end if f(3) = daTmat_dz - if (evolve_Ts) then - - ! follow the matter temperature once it has a chance of diverging + if (Evolve_Ts) then if (timeTh < H_frac*timeH) then - f(4) = 0._dl !a*Tspin follows a*Trad and a*Tmat + f(4) = 0._dl else - if (z< 1/Do21cm_minev-1) then - + if (z < 1/Do21cm_minev - 1) then aTs = y(4) Tspin = ainv*aTs - C10 = n*(kappa_HH_21cm(Tmat,.false.)*(1-x_H) + kappa_eH_21cm(Tmat,.false.)*x) - - dTspin_dz = 4*Tspin/(Hz*ainv)*((Tspin/Tmat-1._dl)*C10 + Trad/T_21cm*(Tspin/Trad-1._dl)*A10) - & - f(1)*Tspin/(1-x_H) + C10 = n*(kappa_HH_21cm(Tmat, .false.)*(1.d0 - x_H) + kappa_eH_21cm(Tmat, .false.)*x) + dTspin_dz = 4*Tspin/(Hz*ainv)*((Tspin/Tmat - 1._dl)*C10 + Trad/T_21cm*(Tspin/Trad - 1._dl)*A10) + dTspin_dz = dTspin_dz - f(1)*Tspin/(1.d0 - x_H) f(4) = (dTspin_dz - aTs)/ainv else - f(4)=daTmat_dz + f(4) = daTmat_dz + end if + end if + end if + + if (.not. present(jacobian)) return + + dlnRdown = (b_PPB + c_PPB*(Tmat/1.d4)**d_PPB*(b_PPB - d_PPB))/(1.d0 + c_PPB*(Tmat/1.d4)**d_PPB)/Tmat + dRdown = Rdown*dlnRdown + dRup = Rup*(dlnRdown + 1.5d0/Tmat + CDB/Tmat**2) + dRupE = RupE*(dlnRdown + 1.5d0/Tmat + CB1/Tmat**2) + + A_H_xH = n*Rdown*(x + x_H) + RupE + A_H_xHe = n*Rdown*Recomb%fHe*x_H + A_H_T = n*x*x_H*dRdown - (1.d0 - x_H)*dRupE + + if (.not. full_hydrogen .and. x_H > 0.99d0) then + jacobian(1, 1:3) = 0._dl + else if (.not. full_hydrogen .and. x_H > 0.985d0) then + jacobian(1, 1) = A_H_xH/denH + jacobian(1, 2) = A_H_xHe/denH + jacobian(1, 3) = ainv*A_H_T/denH + else + B_H = 1.d0 + K*Lambda*n*(1.d0 - x_H) + C_H = 1.d0/Recomb%fu + K*Lambda*n*(1.d0 - x_H)/Recomb%fu + K*Rup*n*(1.d0 - x_H) + C_H_xH = -K*n*(Lambda/Recomb%fu + Rup) + C_H_T = K*n*(1.d0 - x_H)*dRup + jacobian(1, 1) = ((A_H_xH*B_H - A_H*K*Lambda*n)*C_H - A_H*B_H*C_H_xH)/(denH*C_H**2) + jacobian(1, 2) = A_H_xHe*B_H/(denH*C_H) + jacobian(1, 3) = ainv*(A_H_T*B_H*C_H - A_H*B_H*C_H_T)/(denH*C_H**2) + end if + + dlnRdown_He = -(1.d0 + (1.d0 - b_VF)*sq_0/(1.d0 + sq_0) + (1.d0 + b_VF)*sq_1/(1.d0 + sq_1)) + dlnRdown_He = dlnRdown_He/(2.d0*Tmat) + dRdown_He = Rdown_He*dlnRdown_He + dRup_He = Rup_He*(dlnRdown_He + 1.5d0/Tmat + CDB_He/Tmat**2) + if ((Bfact/Tmat) > 680.d0) then + dHe_Boltz = 0._dl + else + dHe_Boltz = -He_Boltz*Bfact/Tmat**2 + end if + + K_He_xH = 0._dl + K_He_xHe = 0._dl + K_He_T = 0._dl + AHcon_xH = 0._dl + AHcon_xHe = 0._dl + AHcon_dT = 0._dl + if (Heflag /= 0) then + tauHe_s_const = A2P_s*CK_He*3.d0*n_He/Hz + pHe_s_xHe = dpHe_s_dtau*(-tauHe_s_const) + if (((Heflag == 2) .or. (Heflag >= 5)) .and. x_H < 0.9999999d0) then + AHcon_xH = -A2P_s*pb*qb*gamma_2Ps**(qb - 1.d0) + AHcon_xH = AHcon_xH*gamma_2Ps/((1.d0 + pb*gamma_2Ps**qb)**2*(1.d0 - x_H)) + AHcon_xHe = A2P_s*pb*qb*gamma_2Ps**qb + AHcon_xHe = AHcon_xHe/((1.d0 + pb*gamma_2Ps**qb)**2*(1.d0 - x_He)) + AHcon_dT = A2P_s*pb*qb*gamma_2Ps**qb + AHcon_dT = AHcon_dT/(2.d0*Tmat*(1.d0 + pb*gamma_2Ps**qb)**2) + end if + K_He_xH = -K_He**2*AHcon_xH*3.d0*n_He*(1.d0 - x_He) + K_He_xHe = -K_He**2*((A2P_s*pHe_s_xHe + AHcon_xHe)*3.d0*n_He*(1.d0 - x_He) & + - (A2P_s*pHe_s + AHcon)*3.d0*n_He) + K_He_T = -K_He**2*AHcon_dT*3.d0*n_He*(1.d0 - x_He) + end if + + if (x_He >= RECFAST_x_He_freeze_threshold) then + EHe = exp(-CL_He/Tmat) + RupHeE = Rup_He*EHe + dRupHeE = RupHeE*(dlnRdown_He + 1.5d0/Tmat + CB1_He1/Tmat**2) + A_He = x*x_He*n*Rdown_He - RupHeE*(1.d0 - x_He) + A_He_xH = n*Rdown_He*x_He + A_He_xHe = n*Rdown_He*(x + Recomb%fHe*x_He) + RupHeE + A_He_T = n*x*x_He*dRdown_He - (1.d0 - x_He)*dRupHeE + + LHe = Lambda_He*n_He*(1.d0 - x_He)*He_Boltz + LHe_xHe = -Lambda_He*n_He*He_Boltz + LHe_T = Lambda_He*n_He*(1.d0 - x_He)*dHe_Boltz + MHe = (Lambda_He + Rup_He)*n_He*(1.d0 - x_He)*He_Boltz + MHe_xHe = -(Lambda_He + Rup_He)*n_He*He_Boltz + MHe_T = n_He*(1.d0 - x_He)*(dRup_He*He_Boltz + (Lambda_He + Rup_He)*dHe_Boltz) + + BHe = 1.d0 + K_He*LHe + CHe = 1.d0 + K_He*MHe + BHe_xH = K_He_xH*LHe + BHe_xHe = K_He_xHe*LHe + K_He*LHe_xHe + BHe_T = K_He_T*LHe + K_He*LHe_T + CHe_xH = K_He_xH*MHe + CHe_xHe = K_He_xHe*MHe + K_He*MHe_xHe + CHe_T = K_He_T*MHe + K_He*MHe_T + + jacobian(2, 1) = ((A_He_xH*BHe + A_He*BHe_xH)*CHe - A_He*BHe*CHe_xH)/(denH*CHe**2) + jacobian(2, 2) = ((A_He_xHe*BHe + A_He*BHe_xHe)*CHe - A_He*BHe*CHe_xHe)/(denH*CHe**2) + jacobian(2, 3) = ainv*((A_He_T*BHe + A_He*BHe_T)*CHe - A_He*BHe*CHe_T)/(denH*CHe**2) + + if (Heflag >= 3) then + dlnRdown_trip = -(1.d0 + (1.d0 - b_trip)*sq_0/(1.d0 + sq_0) + (1.d0 + b_trip)*sq_1/(1.d0 + sq_1)) + dlnRdown_trip = dlnRdown_trip/(2.d0*Tmat) + dRdown_trip = Rdown_trip*dlnRdown_trip + dRup_trip = Rup_trip*(dlnRdown_trip + 1.5d0/Tmat + h_P*C*L_He2St_ion/(k_B*Tmat**2)) + ETrip = exp(-h_P*C*L_He_2st/(k_B*Tmat)) + RupTripE = 3.d0*Rup_trip*ETrip + dRupTripE = RupTripE*(dlnRdown_trip + 1.5d0/Tmat + CB1_He1/Tmat**2) + + A_trip_term = x*x_He*n*Rdown_trip - (1.d0 - x_He)*RupTripE + A_trip_xH = n*Rdown_trip*x_He + A_trip_xHe = n*Rdown_trip*(x + Recomb%fHe*x_He) + RupTripE + A_trip_T = n*x*x_He*dRdown_trip - (1.d0 - x_He)*dRupTripE + + AHcon_t_xH = 0._dl + AHcon_t_xHe = 0._dl + AHcon_t_dT = 0._dl + pHe_t_xHe = dpHe_t_dtau*(-A2P_t*3.d0*n_He/(const_eightpi*Hz*L_He_2Pt**3)) + dEPSt = EPSt*CL_PSt/Tmat**2 + if (.not. ((Heflag == 3) .or. (Heflag == 5) .or. (x_H > 0.99999d0))) then + AHcon_t_xH = -A2P_t*pb*qb*gamma_2Pt**(qb - 1.d0) + AHcon_t_xH = AHcon_t_xH*gamma_2Pt/(3.d0*(1.d0 + pb*gamma_2Pt**qb)**2*(1.d0 - x_H)) + AHcon_t_xHe = A2P_t*pb*qb*gamma_2Pt**qb + AHcon_t_xHe = AHcon_t_xHe/(3.d0*(1.d0 + pb*gamma_2Pt**qb)**2*(1.d0 - x_He)) + AHcon_t_dT = A2P_t*pb*qb*gamma_2Pt**qb + AHcon_t_dT = AHcon_t_dT/(6.d0*Tmat*(1.d0 + pb*gamma_2Pt**qb)**2) end if + + Trip_source = (A2P_t*pHe_t + AHcon_t)*EPSt + Trip_source_xH = AHcon_t_xH*EPSt + Trip_source_xHe = (A2P_t*pHe_t_xHe + AHcon_t_xHe)*EPSt + Trip_source_T = AHcon_t_dT*EPSt + (A2P_t*pHe_t + AHcon_t)*dEPSt + CfHe_t_xH = Trip_source_xH*Rup_trip/(Rup_trip + Trip_source)**2 + CfHe_t_xHe = Trip_source_xHe*Rup_trip/(Rup_trip + Trip_source)**2 + CfHe_t_T = (Trip_source_T*Rup_trip - Trip_source*dRup_trip)/(Rup_trip + Trip_source)**2 + + jacobian(2, 1) = jacobian(2, 1) + (A_trip_xH*CfHe_t + A_trip_term*CfHe_t_xH)/denH + jacobian(2, 2) = jacobian(2, 2) + (A_trip_xHe*CfHe_t + A_trip_term*CfHe_t_xHe)/denH + jacobian(2, 3) = jacobian(2, 3) + ainv*(A_trip_T*CfHe_t + A_trip_term*CfHe_t_T)/denH end if + else + jacobian(2, 1:3) = 0._dl + end if + if (timeTh < H_frac*timeH) then + S = f(1) + Recomb%fHe*f(2) + S_T = jacobian(1, 3)/ainv + Recomb%fHe*jacobian(2, 3)/ainv + eps_x = -Hz*(1.d0 + Recomb%fHe)/(CT*Trad**3*x**2) + P = (1.d0 + Recomb%fHe)/(1.d0 + Recomb%fHe + x) + P_xH = -P/(1.d0 + Recomb%fHe + x) + P_xHe = Recomb%fHe*P_xH + Q = S/x + Q_xH = ((jacobian(1, 1) + Recomb%fHe*jacobian(2, 1))*x - S)/x**2 + Q_xHe = ((jacobian(1, 2) + Recomb%fHe*jacobian(2, 2))*x - S*Recomb%fHe)/x**2 + Q_T = S_T/x + coupling_prefac = -dHdz/(Hz*ainv) + 3.d0/ainv**2 + + jacobian(3, 1) = (eps_x*P*Q + epsilon*P_xH*Q + epsilon*P*Q_xH)/ainv + eps_x*coupling_prefac + jacobian(3, 2) = (Recomb%fHe*eps_x*P*Q + epsilon*P_xHe*Q + epsilon*P*Q_xHe)/ainv + jacobian(3, 2) = jacobian(3, 2) + Recomb%fHe*eps_x*coupling_prefac + jacobian(3, 3) = -1._dl/ainv + epsilon*P*Q_T + else + loose_prefac = CT*Trad**4*(aTmat - Recomb%Tnow)/(Hz*(1.d0 + x + Recomb%fHe)**2) + jacobian(3, 1) = loose_prefac*(1.d0 + Recomb%fHe)/ainv + jacobian(3, 2) = loose_prefac*(1.d0 + Recomb%fHe)*Recomb%fHe/ainv + jacobian(3, 3) = (CT*Trad**4*x/(Hz*(1.d0 + x + Recomb%fHe)) + 1.d0)/ainv end if + if (Ndim > 3) then + jacobian(1:3, 4) = 0._dl + do col = 1, Ndim + delta = 1.e-7_dl*max(1._dl, abs(y(col))) + ypert_plus = y + ypert_plus(col) = ypert_plus(col) + delta + if (col <= 2 .and. y(col) - delta < 0._dl) then + call EvaluateRecfastODE(this, Ndim, z, ypert_plus, fpert_plus, full_hydrogen) + jacobian(4, col) = (fpert_plus(4) - f(4))/delta + else + ypert_minus = y + ypert_minus(col) = ypert_minus(col) - delta + call EvaluateRecfastODE(this, Ndim, z, ypert_plus, fpert_plus, full_hydrogen) + call EvaluateRecfastODE(this, Ndim, z, ypert_minus, fpert_minus, full_hydrogen) + jacobian(4, col) = (fpert_plus(4) - fpert_minus(4))/(2._dl*delta) + end if + end do + end if + + end subroutine EvaluateRecfastODE + + subroutine ION(this,Ndim,z,Y,f) + class(TRecfast), target :: this + integer Ndim + real(dl) :: z + real(dl) :: y(Ndim), f(Ndim) + + call EvaluateRecfastODE(this, Ndim, z, y, f) + end subroutine ION diff --git a/fortran/results.f90 b/fortran/results.f90 index cbbf6382..9c36b358 100644 --- a/fortran/results.f90 +++ b/fortran/results.f90 @@ -1811,7 +1811,8 @@ subroutine Thermo_Init(this, State,taumin) last_dotmu = 0 this%matter_verydom_tau = 0 - a_verydom = CP%Accuracy%AccuracyBoost*5*(State%grhog+State%grhornomass)/(State%grhoc+State%grhob) + a_verydom = CP%Accuracy%AccuracyBoost*CP%Accuracy%TimeSwitchBoost & + *5*(State%grhog+State%grhornomass)/(State%grhoc+State%grhob) if (CP%Reion%Reionization) then call CP%Reion%get_timesteps(State%reion_n_steps, reion_z_start, reion_z_complete) State%reion_tau_start = max(0.05_dl, State%TimeOfZ(reion_z_start, 1d-3)) @@ -2350,7 +2351,7 @@ subroutine SetTimeSteps(this,State,TimeSteps) if (State%CP%Reion%Reionization) then - nri0=int(State%reion_n_steps*State%CP%Accuracy%AccuracyBoost) + nri0=int(State%reion_n_steps * TimeSampleBoost) !Steps while reionization going from zero to maximum call TimeSteps%Add(State%reion_tau_start,State%reion_tau_complete,nri0) end if @@ -3339,8 +3340,14 @@ function MatterPowerData_k(PK, kh, itf, index_cache) result(outpower) ( PK%log_kh(2)-PK%log_kh(1) ) outpower = PK%matpower(1,itf) + dp*(logk - PK%log_kh(1)) else if (logk > PK%log_kh(PK%num_k)) then - !Do dodgy linear extrapolation on assumption accuracy of result won't matter + if (PK%matpower(PK%num_k,itf) >= PK%matpower(PK%num_k-1,itf)) then + call GlobalError(FormatString('MatterPowerData_k: cannot extrapolate rising high-k matter power tail' // & + ' from k/h=%f to requested k/h=%f', exp(PK%log_kh(PK%num_k)), kh), error_nonlinear) + outpower = 0 + return + end if + !Do dodgy linear extrapolation on assumption accuracy of result won't matter dp = (PK%matpower(PK%num_k,itf) - PK%matpower(PK%num_k-1,itf)) / & ( PK%log_kh(PK%num_k)-PK%log_kh(PK%num_k-1) ) outpower = PK%matpower(PK%num_k,itf) + dp*(logk - PK%log_kh(PK%num_k)) diff --git a/fortran/subroutines.f90 b/fortran/subroutines.f90 index 06e5dc2c..9a82caed 100644 --- a/fortran/subroutines.f90 +++ b/fortran/subroutines.f90 @@ -1,4 +1,4 @@ - !Low-level numerical routines for splines and dverk for differential equation integration. + !Low-level numerical routines for splines. module splines use Precision @@ -119,765 +119,3 @@ subroutine splint(y,z,n) end subroutine splint end module splines - - - !This version is modified to pass an object parameter to the function on each call - !Fortunately Fortran doesn't do type checking on functions, so we can pretend the - !passed object parameter (EV) is any type we like. In reality it is just a pointer. - subroutine dverk (EV,n, fcn, x, y, xend, tol, ind, c, nw, w) - use Precision - use MpiUtils - use Config, only : GlobalError, error_evolution - integer n, ind, nw, k - real(dl) x, y(n), xend, tol, c(*), w(nw,9), temp - real EV !it isn't, but as long as it maintains it as a pointer we are OK - ! - !*********************************************************************** - ! * - ! note added 11/14/85. * - ! * - ! if you discover any errors in this subroutine, please contact * - ! * - ! kenneth r. jackson * - ! department of computer science * - ! university of toronto * - ! toronto, ontario, * - ! canada m5s 1a4 * - ! * - ! phone: 416-978-7075 * - ! * - ! electronic mail: * - ! uucp: {cornell,decvax,ihnp4,linus,uw-beaver}!utcsri!krj * - ! csnet: krj@toronto * - ! arpa: krj.toronto@csnet-relay * - ! bitnet: krj%toronto@csnet-relay.arpa * - ! * - ! dverk is written in fortran 66. * - ! * - ! the constants dwarf and rreb -- c(10) and c(11), respectively -- are * - ! set for a vax in double precision. they should be reset, as * - ! described below, if this program is run on another machine. * - ! * - ! the c array is declared in this subroutine to have one element only, * - ! although more elements are referenced in this subroutine. this * - ! causes some compilers to issue warning messages. there is, though, * - ! no error provided c is declared sufficiently large in the calling * - ! program, as described below. * - ! * - ! the following external statement for fcn was added to avoid a * - ! warning message from the unix f77 compiler. the original dverk * - ! comments and code follow it. * - ! * - !*********************************************************************** - ! - external fcn - ! - !*********************************************************************** - ! * - ! purpose - this is a runge-kutta subroutine based on verner's * - ! fifth and sixth order pair of formulas for finding approximations to * - ! the solution of a system of first order ordinary differential * - ! equations with initial conditions. it attempts to keep the global * - ! error proportional to a tolerance specified by the user. (the * - ! proportionality depends on the kind of error control that is used, * - ! as well as the differential equation and the range of integration.) * - ! * - ! various options are available to the user, including different * - ! kinds of error control, restrictions on step sizes, and interrupts * - ! which permit the user to examine the state of the calculation (and * - ! perhaps make modifications) during intermediate stages. * - ! * - ! the program is efficient for non-stiff systems. however, a good * - ! variable-order-adams method will probably be more efficient if the * - ! function evaluations are very costly. such a method would also be * - ! more suitable if one wanted to obtain a large number of intermediate * - ! solution values by interpolation, as might be the case for example * - ! with graphical output. * - ! * - ! hull-enright-jackson 1/10/76 * - ! * - !*********************************************************************** - ! * - ! use - the user must specify each of the following * - ! * - ! n number of equations * - ! * - ! fcn name of subroutine for evaluating functions - the subroutine * - ! itself must also be provided by the user - it should be of * - ! the following form * - ! subroutine fcn(n, x, y, yprime) * - ! integer n * - ! real(dl) x, y(n), yprime(n) * - ! *** etc *** * - ! and it should evaluate yprime, given n, x and y * - ! * - ! x independent variable - initial value supplied by user * - ! * - ! y dependent variable - initial values of components y(1), y(2), * - ! ..., y(n) supplied by user * - ! * - ! xend value of x to which integration is to be carried out - it may * - ! be less than the initial value of x * - ! * - ! tol tolerance - the subroutine attempts to control a norm of the * - ! local error in such a way that the global error is * - ! proportional to tol. in some problems there will be enough * - ! damping of errors, as well as some cancellation, so that * - ! the global error will be less than tol. alternatively, the * - ! control can be viewed as attempting to provide a * - ! calculated value of y at xend which is the exact solution * - ! to the problem y' = f(x,y) + e(x) where the norm of e(x) * - ! is proportional to tol. (the norm is a max norm with * - ! weights that depend on the error control strategy chosen * - ! by the user. the default weight for the k-th component is * - ! 1/max(1,abs(y(k))), which therefore provides a mixture of * - ! absolute and relative error control.) * - ! * - ! ind indicator - on initial entry ind must be set equal to either * - ! 1 or 2. if the user does not wish to use any options, he * - ! should set ind to 1 - all that remains for the user to do * - ! then is to declare c and w, and to specify nw. the user * - ! may also select various options on initial entry by * - ! setting ind = 2 and initializing the first 9 components of * - ! c as described in the next section. he may also re-enter * - ! the subroutine with ind = 3 as mentioned again below. in * - ! any event, the subroutine returns with ind equal to * - ! 3 after a normal return * - ! 4, 5, or 6 after an interrupt (see options c(8), c(9)) * - ! -1, -2, or -3 after an error condition (see below) * - ! * - ! c communications vector - the dimension must be greater than or * - ! equal to 24, unless option c(1) = 4 or 5 is used, in which * - ! case the dimension must be greater than or equal to n+30 * - ! * - ! nw first dimension of workspace w - must be greater than or * - ! equal to n * - ! * - ! w workspace matrix - first dimension must be nw and second must * - ! be greater than or equal to 9 * - ! * - ! the subroutine will normally return with ind = 3, having * - ! replaced the initial values of x and y with, respectively, the value * - ! of xend and an approximation to y at xend. the subroutine can be * - ! called repeatedly with new values of xend without having to change * - ! any other argument. however, changes in tol, or any of the options * - ! described below, may also be made on such a re-entry if desired. * - ! * - ! three error returns are also possible, in which case x and y * - ! will be the most recently accepted values - * - ! with ind = -3 the subroutine was unable to satisfy the error * - ! requirement with a particular step-size that is less than or * - ! equal to hmin, which may mean that tol is too small * - ! with ind = -2 the value of hmin is greater than hmax, which * - ! probably means that the requested tol (which is used in the * - ! calculation of hmin) is too small * - ! with ind = -1 the allowed maximum number of fcn evaluations has * - ! been exceeded, but this can only occur if option c(7), as * - ! described in the next section, has been used * - ! * - ! there are several circumstances that will cause the calculations * - ! to be terminated, along with output of information that will help * - ! the user determine the cause of the trouble. these circumstances * - ! involve entry with illegal or inconsistent values of the arguments, * - ! such as attempting a normal re-entry without first changing the * - ! value of xend, or attempting to re-enter with ind less than zero. * - ! * - !*********************************************************************** - ! * - ! options - if the subroutine is entered with ind = 1, the first 9 * - ! components of the communications vector are initialized to zero, and * - ! the subroutine uses only default values for each option. if the * - ! subroutine is entered with ind = 2, the user must specify each of * - ! these 9 components - normally he would first set them all to zero, * - ! and then make non-zero those that correspond to the particular * - ! options he wishes to select. in any event, options may be changed on * - ! re-entry to the subroutine - but if the user changes any of the * - ! options, or tol, in the course of a calculation he should be careful * - ! about how such changes affect the subroutine - it may be better to * - ! restart with ind = 1 or 2. (components 10 to 24 of c are used by the * - ! program - the information is available to the user, but should not * - ! normally be changed by him.) * - ! * - ! c(1) error control indicator - the norm of the local error is the * - ! max norm of the weighted error estimate vector, the * - ! weights being determined according to the value of c(1) - * - ! if c(1)=1 the weights are 1 (absolute error control) * - ! if c(1)=2 the weights are 1/abs(y(k)) (relative error * - ! control) * - ! if c(1)=3 the weights are 1/max(abs(c(2)),abs(y(k))) * - ! (relative error control, unless abs(y(k)) is less * - ! than the floor value, abs(c(2)) ) * - ! if c(1)=4 the weights are 1/max(abs(c(k+30)),abs(y(k))) * - ! (here individual floor values are used) * - ! if c(1)=5 the weights are 1/abs(c(k+30)) * - ! for all other values of c(1), including c(1) = 0, the * - ! default values of the weights are taken to be * - ! 1/max(1,abs(y(k))), as mentioned earlier * - ! (in the two cases c(1) = 4 or 5 the user must declare the * - ! dimension of c to be at least n+30 and must initialize the * - ! components c(31), c(32), ..., c(n+30).) * - ! * - ! c(2) floor value - used when the indicator c(1) has the value 3 * - ! * - ! c(3) hmin specification - if not zero, the subroutine chooses hmin * - ! to be abs(c(3)) - otherwise it uses the default value * - ! 10*max(dwarf,rreb*max(weighted norm y/tol,abs(x))), * - ! where dwarf is a very small positive machine number and * - ! rreb is the relative roundoff error bound * - ! * - ! c(4) hstart specification - if not zero, the subroutine will use * - ! an initial hmag equal to abs(c(4)), except of course for * - ! the restrictions imposed by hmin and hmax - otherwise it * - ! uses the default value of hmax*(tol)**(1/6) * - ! * - ! c(5) scale specification - this is intended to be a measure of the * - ! scale of the problem - larger values of scale tend to make * - ! the method more reliable, first by possibly restricting * - ! hmax (as described below) and second, by tightening the * - ! acceptance requirement - if c(5) is zero, a default value * - ! of 1 is used. for linear homogeneous problems with * - ! constant coefficients, an appropriate value for scale is a * - ! norm of the associated matrix. for other problems, an * - ! approximation to an average value of a norm of the * - ! jacobian along the trajectory may be appropriate * - ! * - ! c(6) hmax specification - four cases are possible * - ! if c(6).ne.0 and c(5).ne.0, hmax is taken to be * - ! min(abs(c(6)),2/abs(c(5))) * - ! if c(6).ne.0 and c(5).eq.0, hmax is taken to be abs(c(6)) * - ! if c(6).eq.0 and c(5).ne.0, hmax is taken to be * - ! 2/abs(c(5)) * - ! if c(6).eq.0 and c(5).eq.0, hmax is given a default value * - ! of 2 * - ! * - ! c(7) maximum number of function evaluations - if not zero, an * - ! error return with ind = -1 will be caused when the number * - ! of function evaluations exceeds abs(c(7)) * - ! * - ! c(8) interrupt number 1 - if not zero, the subroutine will * - ! interrupt the calculations after it has chosen its * - ! preliminary value of hmag, and just before choosing htrial * - ! and xtrial in preparation for taking a step (htrial may * - ! differ from hmag in sign, and may require adjustment if * - ! xend is near) - the subroutine returns with ind = 4, and * - ! will resume calculation at the point of interruption if * - ! re-entered with ind = 4 * - ! * - ! c(9) interrupt number 2 - if not zero, the subroutine will * - ! interrupt the calculations immediately after it has * - ! decided whether or not to accept the result of the most * - ! recent trial step, with ind = 5 if it plans to accept, or * - ! ind = 6 if it plans to reject - y(*) is the previously * - ! accepted result, while w(*,9) is the newly computed trial * - ! value, and w(*,2) is the unweighted error estimate vector. * - ! the subroutine will resume calculations at the point of * - ! interruption on re-entry with ind = 5 or 6. (the user may * - ! change ind in this case if he wishes, for example to force * - ! acceptance of a step that would otherwise be rejected, or * - ! vice versa. he can also restart with ind = 1 or 2.) * - ! * - !*********************************************************************** - ! * - ! summary of the components of the communications vector * - ! * - ! prescribed at the option determined by the program * - ! of the user * - ! * - ! c(10) rreb(rel roundoff err bnd) * - ! c(1) error control indicator c(11) dwarf (very small mach no) * - ! c(2) floor value c(12) weighted norm y * - ! c(3) hmin specification c(13) hmin * - ! c(4) hstart specification c(14) hmag * - ! c(5) scale specification c(15) scale * - ! c(6) hmax specification c(16) hmax * - ! c(7) max no of fcn evals c(17) xtrial * - ! c(8) interrupt no 1 c(18) htrial * - ! c(9) interrupt no 2 c(19) est * - ! c(20) previous xend * - ! c(21) flag for xend * - ! c(22) no of successful steps * - ! c(23) no of successive failures * - ! c(24) no of fcn evals * - ! * - ! if c(1) = 4 or 5, c(31), c(32), ... c(n+30) are floor values * - ! * - !*********************************************************************** - ! * - ! an overview of the program * - ! * - ! begin initialization, parameter checking, interrupt re-entries * - ! ......abort if ind out of range 1 to 6 * - ! . cases - initial entry, normal re-entry, interrupt re-entries * - ! . case 1 - initial entry (ind .eq. 1 or 2) * - ! v........abort if n.gt.nw or tol.le.0 * - ! . if initial entry without options (ind .eq. 1) * - ! . set c(1) to c(9) equal to zero * - ! . else initial entry with options (ind .eq. 2) * - ! . make c(1) to c(9) non-negative * - ! . make floor values non-negative if they are to be used * - ! . end if * - ! . initialize rreb, dwarf, prev xend, flag, counts * - ! . case 2 - normal re-entry (ind .eq. 3) * - ! .........abort if xend reached, and either x changed or xend not * - ! . re-initialize flag * - ! . case 3 - re-entry following an interrupt (ind .eq. 4 to 6) * - ! v transfer control to the appropriate re-entry point....... * - ! . end cases . * - ! . end initialization, etc. . * - ! . v * - ! . loop through the following 4 stages, once for each trial step . * - ! . stage 1 - prepare . * - !***********error return (with ind=-1) if no of fcn evals too great . * - ! . calc slope (adding 1 to no of fcn evals) if ind .ne. 6 . * - ! . calc hmin, scale, hmax . * - !***********error return (with ind=-2) if hmin .gt. hmax . * - ! . calc preliminary hmag . * - !***********interrupt no 1 (with ind=4) if requested.......re-entry.v * - ! . calc hmag, xtrial and htrial . * - ! . end stage 1 . * - ! v stage 2 - calc ytrial (adding 7 to no of fcn evals) . * - ! . stage 3 - calc the error estimate . * - ! . stage 4 - make decisions . * - ! . set ind=5 if step acceptable, else set ind=6 . * - !***********interrupt no 2 if requested....................re-entry.v * - ! . if step accepted (ind .eq. 5) * - ! . update x, y from xtrial, ytrial * - ! . add 1 to no of successful steps * - ! . set no of successive failures to zero * - !**************return(with ind=3, xend saved, flag set) if x .eq. xend * - ! . else step not accepted (ind .eq. 6) * - ! . add 1 to no of successive failures * - !**************error return (with ind=-3) if hmag .le. hmin * - ! . end if * - ! . end stage 4 * - ! . end loop * - ! . * - ! begin abort action * - ! output appropriate message about stopping the calculations, * - ! along with values of ind, n, nw, tol, hmin, hmax, x, xend, * - ! previous xend, no of successful steps, no of successive * - ! failures, no of fcn evals, and the components of y * - ! stop * - ! end abort action * - ! * - !*********************************************************************** - ! - ! ****************************************************************** - ! * begin initialization, parameter checking, interrupt re-entries * - ! ****************************************************************** - ! - ! ......abort if ind out of range 1 to 6 - if (ind.lt.1 .or. ind.gt.6) go to 500 - ! - ! cases - initial entry, normal re-entry, interrupt re-entries - ! go to (5, 5, 45, 1111, 2222, 2222), ind - if (ind==3) goto 45 - if (ind==4) goto 1111 - if (ind==5 .or. ind==6) goto 2222 - - ! case 1 - initial entry (ind .eq. 1 or 2) - ! .........abort if n.gt.nw or tol.le.0 - if (n.gt.nw .or. tol.le.0._dl) go to 500 - if (ind.eq. 2) go to 15 - ! initial entry without options (ind .eq. 1) - ! set c(1) to c(9) equal to 0 - do k = 1, 9 - c(k) = 0._dl - end do - go to 35 -15 continue - ! initial entry with options (ind .eq. 2) - ! make c(1) to c(9) non-negative - do k = 1, 9 - c(k) = dabs(c(k)) - end do - ! make floor values non-negative if they are to be used - if (c(1).ne.4._dl .and. c(1).ne.5._dl) go to 30 - do k = 1, n - c(k+30) = dabs(c(k+30)) - end do -30 continue -35 continue - ! initialize rreb, dwarf, prev xend, flag, counts - c(10) = 2._dl**(-56) - c(11) = 1.d-35 - ! set previous xend initially to initial value of x - c(20) = x - do k = 21, 24 - c(k) = 0._dl - end do - go to 50 - ! case 2 - normal re-entry (ind .eq. 3) - ! .........abort if xend reached, and either x changed or xend not -45 if (c(21).ne.0._dl .and. & - (x.ne.c(20) .or. xend.eq.c(20))) go to 500 - ! re-initialize flag - c(21) = 0._dl - go to 50 - ! case 3 - re-entry following an interrupt (ind .eq. 4 to 6) - ! transfer control to the appropriate re-entry point.......... - ! this has already been handled by the computed go to . - ! end cases v -50 continue - ! - ! end initialization, etc. - ! - ! ****************************************************************** - ! * loop through the following 4 stages, once for each trial step * - ! * until the occurrence of one of the following * - ! * (a) the normal return (with ind .eq. 3) on reaching xend in * - ! * stage 4 * - ! * (b) an error return (with ind .lt. 0) in stage 1 or stage 4 * - ! * (c) an interrupt return (with ind .eq. 4, 5 or 6), if * - ! * requested, in stage 1 or stage 4 * - ! ****************************************************************** - ! -99999 continue - ! - ! *************************************************************** - ! * stage 1 - prepare - do calculations of hmin, hmax, etc., * - ! * and some parameter checking, and end up with suitable * - ! * values of hmag, xtrial and htrial in preparation for taking * - ! * an integration step. * - ! *************************************************************** - ! - !***********error return (with ind=-1) if no of fcn evals too great - if (c(7).eq.0._dl .or. c(24).lt.c(7)) go to 100 - ind = -1 - return -100 continue - ! - ! calculate slope (adding 1 to no of fcn evals) if ind .ne. 6 - if (ind .eq. 6) go to 105 - call fcn(EV,n, x, y, w(1,1)) - c(24) = c(24) + 1._dl -105 continue - ! - ! calculate hmin - use default unless value prescribed - c(13) = c(3) - if (c(3) .ne. 0._dl) go to 165 - ! calculate default value of hmin - ! first calculate weighted norm y - c(12) - as specified - ! by the error control indicator c(1) - temp = 0._dl - if (c(1) .ne. 1._dl) go to 115 - ! absolute error control - weights are 1 - do 110 k = 1, n - temp = dmax1(temp, dabs(y(k))) -110 continue - c(12) = temp - go to 160 -115 if (c(1) .ne. 2._dl) go to 120 - ! relative error control - weights are 1/dabs(y(k)) so - ! weighted norm y is 1 - c(12) = 1._dl - go to 160 -120 if (c(1) .ne. 3._dl) go to 130 - ! weights are 1/max(c(2),abs(y(k))) - do 125 k = 1, n - temp = dmax1(temp, dabs(y(k))/c(2)) -125 continue - c(12) = dmin1(temp, 1._dl) - go to 160 -130 if (c(1) .ne. 4._dl) go to 140 - ! weights are 1/max(c(k+30),abs(y(k))) - do 135 k = 1, n - temp = dmax1(temp, dabs(y(k))/c(k+30)) -135 continue - c(12) = dmin1(temp, 1._dl) - go to 160 -140 if (c(1) .ne. 5._dl) go to 150 - ! weights are 1/c(k+30) - do 145 k = 1, n - temp = dmax1(temp, dabs(y(k))/c(k+30)) -145 continue - c(12) = temp - go to 160 -150 continue - ! default case - weights are 1/max(1,abs(y(k))) - do 155 k = 1, n - temp = dmax1(temp, dabs(y(k))) -155 continue - c(12) = dmin1(temp, 1._dl) -160 continue - c(13) = 10._dl*dmax1(c(11),c(10)*dmax1(c(12)/tol,dabs(x))) -165 continue - ! - ! calculate scale - use default unless value prescribed - c(15) = c(5) - if (c(5) .eq. 0._dl) c(15) = 1._dl - ! - ! calculate hmax - consider 4 cases - ! case 1 both hmax and scale prescribed - if (c(6).ne.0._dl .and. c(5).ne.0._dl) & - c(16) = dmin1(c(6), 2._dl/c(5)) - ! case 2 - hmax prescribed, but scale not - if (c(6).ne.0._dl .and. c(5).eq.0._dl) c(16) = c(6) - ! case 3 - hmax not prescribed, but scale is - if (c(6).eq.0._dl .and. c(5).ne.0._dl) c(16) = 2._dl/c(5) - ! case 4 - neither hmax nor scale is provided - if (c(6).eq.0._dl .and. c(5).eq.0._dl) c(16) = 2._dl - ! - !***********error return (with ind=-2) if hmin .gt. hmax - if (c(13) .le. c(16)) go to 170 - ind = -2 - return -170 continue - ! - ! calculate preliminary hmag - consider 3 cases - if (ind .gt. 2) go to 175 - ! case 1 - initial entry - use prescribed value of hstart, if - ! any, else default - c(14) = c(4) - if (c(4) .eq. 0._dl) c(14) = c(16)*tol**(1._dl/6._dl) - go to 185 -175 if (c(23) .gt. 1._dl) go to 180 - ! case 2 - after a successful step, or at most one failure, - ! use min(2, .9*(tol/est)**(1/6))*hmag, but avoid possible - ! overflow. then avoid reduction by more than half. - temp = 2._dl*c(14) - if (tol .lt. (2._dl/.9d0)**6*c(19)) & - temp = .9d0*(tol/c(19))**(1._dl/6._dl)*c(14) - c(14) = dmax1(temp, .5d0*c(14)) - go to 185 -180 continue - ! case 3 - after two or more successive failures - c(14) = .5d0*c(14) -185 continue - ! - ! check against hmax - c(14) = dmin1(c(14), c(16)) - ! - ! check against hmin - c(14) = dmax1(c(14), c(13)) - ! - !***********interrupt no 1 (with ind=4) if requested - if (c(8) .eq. 0._dl) go to 1111 - ind = 4 - return - ! resume here on re-entry with ind .eq. 4 ........re-entry.. -1111 continue - ! - ! calculate hmag, xtrial - depending on preliminary hmag, xend - if (c(14) .ge. dabs(xend - x)) go to 190 - ! do not step more than half way to xend - c(14) = dmin1(c(14), .5d0*dabs(xend - x)) - c(17) = x + dsign(c(14), xend - x) - go to 195 -190 continue - ! hit xend exactly - c(14) = dabs(xend - x) - c(17) = xend -195 continue - ! - ! calculate htrial - c(18) = c(17) - x - ! - ! end stage 1 - ! - ! *************************************************************** - ! * stage 2 - calculate ytrial (adding 7 to no of fcn evals). * - ! * w(*,2), ... w(*,8) hold intermediate results needed in * - ! * stage 3. w(*,9) is temporary storage until finally it holds * - ! * ytrial. * - ! *************************************************************** - ! - temp = c(18)/1398169080000._dl - ! - do 200 k = 1, n - w(k,9) = y(k) + temp*w(k,1)*233028180000._dl -200 continue - call fcn(EV,n, x + c(18)/6._dl, w(1,9), w(1,2)) - ! - do 205 k = 1, n - w(k,9) = y(k) + temp*( w(k,1)*74569017600._dl & - + w(k,2)*298276070400._dl ) -205 continue - call fcn(EV,n, x + c(18)*(4._dl/15._dl), w(1,9), w(1,3)) - ! - do 210 k = 1, n - w(k,9) = y(k) + temp*( w(k,1)*1165140900000._dl & - - w(k,2)*3728450880000._dl & - + w(k,3)*3495422700000._dl ) -210 continue - call fcn(EV,n, x + c(18)*(2._dl/3._dl), w(1,9), w(1,4)) - ! - do 215 k = 1, n - w(k,9) = y(k) + temp*( - w(k,1)*3604654659375._dl & - + w(k,2)*12816549900000._dl & - - w(k,3)*9284716546875._dl & - + w(k,4)*1237962206250._dl ) -215 continue - call fcn(EV,n, x + c(18)*(5._dl/6._dl), w(1,9), w(1,5)) - ! - do 220 k = 1, n - w(k,9) = y(k) + temp*( w(k,1)*3355605792000._dl & - - w(k,2)*11185352640000._dl & - + w(k,3)*9172628850000._dl & - - w(k,4)*427218330000._dl & - + w(k,5)*482505408000._dl ) -220 continue - call fcn(EV,n, x + c(18), w(1,9), w(1,6)) - ! - do 225 k = 1, n - w(k,9) = y(k) + temp*( - w(k,1)*770204740536._dl & - + w(k,2)*2311639545600._dl & - - w(k,3)*1322092233000._dl & - - w(k,4)*453006781920._dl & - + w(k,5)*326875481856._dl ) -225 continue - call fcn(EV,n, x + c(18)/15._dl, w(1,9), w(1,7)) - ! - do 230 k = 1, n - w(k,9) = y(k) + temp*( w(k,1)*2845924389000._dl & - - w(k,2)*9754668000000._dl & - + w(k,3)*7897110375000._dl & - - w(k,4)*192082660000._dl & - + w(k,5)*400298976000._dl & - + w(k,7)*201586000000._dl ) -230 continue - call fcn(EV,n, x + c(18), w(1,9), w(1,8)) - ! - ! calculate ytrial, the extrapolated approximation and store - ! in w(*,9) - do 235 k = 1, n - w(k,9) = y(k) + temp*( w(k,1)*104862681000._dl & - + w(k,3)*545186250000._dl & - + w(k,4)*446637345000._dl & - + w(k,5)*188806464000._dl & - + w(k,7)*15076875000._dl & - + w(k,8)*97599465000._dl ) -235 continue - ! - ! add 7 to the no of fcn evals - c(24) = c(24) + 7._dl - ! - ! end stage 2 - ! - ! *************************************************************** - ! * stage 3 - calculate the error estimate est. first calculate * - ! * the unweighted absolute error estimate vector (per unit * - ! * step) for the unextrapolated approximation and store it in * - ! * w(*,2). then calculate the weighted max norm of w(*,2) as * - ! * specified by the error control indicator c(1). finally, * - ! * modify this result to produce est, the error estimate (per * - ! * unit step) for the extrapolated approximation ytrial. * - ! *************************************************************** - ! - ! calculate the unweighted absolute error estimate vector - do 300 k = 1, n - w(k,2) = ( w(k,1)*8738556750._dl & - + w(k,3)*9735468750._dl & - - w(k,4)*9709507500._dl & - + w(k,5)*8582112000._dl & - + w(k,6)*95329710000._dl & - - w(k,7)*15076875000._dl & - - w(k,8)*97599465000._dl)/1398169080000._dl -300 continue - ! - ! calculate the weighted max norm of w(*,2) as specified by - ! the error control indicator c(1) - temp = 0._dl - if (c(1) .ne. 1._dl) go to 310 - ! absolute error control - do 305 k = 1, n - temp = dmax1(temp,dabs(w(k,2))) -305 continue - go to 360 -310 if (c(1) .ne. 2._dl) go to 320 - ! relative error control - do 315 k = 1, n - temp = dmax1(temp, dabs(w(k,2)/y(k))) -315 continue - go to 360 -320 if (c(1) .ne. 3._dl) go to 330 - ! weights are 1/max(c(2),abs(y(k))) - do 325 k = 1, n - temp = dmax1(temp, dabs(w(k,2)) & - / dmax1(c(2), dabs(y(k))) ) -325 continue - go to 360 -330 if (c(1) .ne. 4._dl) go to 340 - ! weights are 1/max(c(k+30),abs(y(k))) - do 335 k = 1, n - temp = dmax1(temp, dabs(w(k,2)) & - / dmax1(c(k+30), dabs(y(k))) ) -335 continue - go to 360 -340 if (c(1) .ne. 5._dl) go to 350 - ! weights are 1/c(k+30) - do 345 k = 1, n - temp = dmax1(temp, dabs(w(k,2)/c(k+30))) -345 continue - go to 360 -350 continue - ! default case - weights are 1/max(1,abs(y(k))) - do 355 k = 1, n - temp = dmax1(temp, dabs(w(k,2)) & - / dmax1(1._dl, dabs(y(k))) ) -355 continue -360 continue - ! - ! calculate est - (the weighted max norm of w(*,2))*hmag*scale - ! - est is intended to be a measure of the error per unit - ! step in ytrial - c(19) = temp*c(14)*c(15) - ! - ! end stage 3 - ! - ! *************************************************************** - ! * stage 4 - make decisions. * - ! *************************************************************** - ! - ! set ind=5 if step acceptable, else set ind=6 - ind = 5 - if (c(19) .gt. tol) ind = 6 - ! - !***********interrupt no 2 if requested - if (c(9) .eq. 0._dl) go to 2222 - return - ! resume here on re-entry with ind .eq. 5 or 6 ...re-entry.. -2222 continue - ! - if (ind .eq. 6) go to 410 - ! step accepted (ind .eq. 5), so update x, y from xtrial, - ! ytrial, add 1 to the no of successful steps, and set - ! the no of successive failures to zero - x = c(17) - do 400 k = 1, n - y(k) = w(k,9) -400 continue - c(22) = c(22) + 1._dl - c(23) = 0._dl - !**************return(with ind=3, xend saved, flag set) if x .eq. xend - if (x .ne. xend) go to 405 - ind = 3 - c(20) = xend - c(21) = 1._dl - return -405 continue - go to 420 -410 continue - ! step not accepted (ind .eq. 6), so add 1 to the no of - ! successive failures - c(23) = c(23) + 1._dl - !**************error return (with ind=-3) if hmag .le. hmin - if (c(14) .gt. c(13)) go to 415 - ind = -3 - return -415 continue -420 continue - ! - ! end stage 4 - ! - go to 99999 - ! end loop - ! - ! begin abort action -500 continue - ! - - write (*,*) 'Error in dverk, x =',x, 'xend=', xend - call GlobalError('DVERK error', error_evolution) - ! - end subroutine dverk diff --git a/fortran/tests/CAMB_test_files.py b/fortran/tests/CAMB_test_files.py index 028b7eab..34b18c8d 100644 --- a/fortran/tests/CAMB_test_files.py +++ b/fortran/tests/CAMB_test_files.py @@ -1,980 +1,980 @@ -import argparse -import copy -import filecmp -import fnmatch -import math -import os -import shutil -import stat -import subprocess -import sys -import tempfile -import time - -TESTS_DIR = os.path.dirname(os.path.abspath(__file__)) -FORTRAN_DIR = os.path.abspath(os.path.join(TESTS_DIR, "..")) -REPO_ROOT = os.path.abspath(os.path.join(FORTRAN_DIR, "..")) -DEFAULT_BASE_SETTINGS = os.path.join(REPO_ROOT, "inifiles", "params.ini") - - -def parse_override_assignment(text): - if "=" not in text: - raise argparse.ArgumentTypeError("Overrides must use KEY=VALUE syntax") - key, value = text.split("=", 1) - key = key.strip() - if not key: - raise argparse.ArgumentTypeError("Override key cannot be empty") - return key, value.strip() - - -def build_parser(): - parser = argparse.ArgumentParser(description="Run CAMB tests") - parser.add_argument("ini_dir", help="ini file directory") - parser.add_argument("--make_ini", "--make-ini", action="store_true", help="If set, output ini files to ini_dir") - parser.add_argument("--out_files_dir", "--out-files-dir", default="test_outputs", help="output files directory") - parser.add_argument( - "--base_settings", - "--base-settings", - default=DEFAULT_BASE_SETTINGS, - help="settings to include as defaults for all combinations", - ) - parser.add_argument("--no_run_test", "--no-run-test", action="store_true", help="Don't run tests on files") - parser.add_argument( - "--runner", - choices=("module", "command"), - default="module", - help="Use the compiled Python camb module or the legacy command-line executable", - ) - parser.add_argument("--prog", default="./camb", help="executable to run when --runner=command") - parser.add_argument("--no_validate", "--no-validate", action="store_true", help="Skip ini validation") - parser.add_argument("--clean", action="store_true", help="delete output dir before run") - parser.add_argument("--diff_to", "--diff-to", help="output directory to compare to, e.g. test_outputs2") - parser.add_argument( - "--diff_tolerance", - "--diff-tolerance", - type=float, - help="the tolerance for the numerical diff when no explicit diff is given [default: 1e-4]", - default=1e-4, - ) - parser.add_argument( - "--verbose_diff_output", - "--verbose-diff-output", - "--verbose", - action="store_true", - help="during diff_to print more error messages", - ) - parser.add_argument("--num_diff", "--num-diff", action="store_true", help="during diff_to use absolute diffs") - parser.add_argument("--no_sources", "--no-sources", action="store_true", help="turn off CAMB sources tests") - parser.add_argument("--no_de", "--no-de", action="store_true", help="Don't run dark energy tests") - parser.add_argument("--max_tests", "--max-tests", type=int, help="maximum tests to run") - parser.add_argument( - "--override", - "--set", - action="append", - type=parse_override_assignment, - default=[], - metavar="KEY=VALUE", - help="override an ini parameter for every test; can be repeated", - ) - return parser - - -args = argparse.Namespace() -prog = "" -out_files_dir = "" - -logfile = None - - -def printlog(text): - global logfile - print(text) - sys.stdout.flush() - if logfile is None: - logfile = open(os.path.join(args.ini_dir, "test_results.log"), "a", encoding="utf-8") - logfile.write(text + "\n") - - -def close_logfile(): - global logfile - if logfile is not None: - logfile.close() - logfile = None - - -def repo_inifile(name): - return os.path.join(REPO_ROOT, "inifiles", name) - - -def resolve_from_ini_dir(path): - if os.path.isabs(path): - return path - return os.path.join(args.ini_dir, path) - - -def ensure_camb_on_path(): - if REPO_ROOT not in sys.path: - sys.path.insert(0, REPO_ROOT) - - -def make_ini_file(*args, **kwargs): - ensure_camb_on_path() - from camb.inifile import IniFile - - return IniFile(*args, **kwargs) - - -def apply_ini_overrides(filename): - if not args.override: - return filename - ini = make_ini_file(filename) - for key, value in args.override: - if key not in ini.params: - ini.readOrder.append(key) - ini.params[key] = value - ini.saveFile(filename) - return filename - - -def write_flat_ini_file(filename, parameter_lines): - filename = os.path.abspath(filename) - with tempfile.NamedTemporaryFile( - "w", suffix=".ini", dir=os.path.dirname(filename), delete=False, encoding="utf-8" - ) as handle: - handle.write("\n".join(parameter_lines)) - handle.write("\n") - temp_filename = handle.name - - try: - make_ini_file(temp_filename).saveFile(filename) - finally: - os.remove(temp_filename) - - return filename - - -def remove_tree(path): - def onerror(func, target, exc_info): - for _ in range(10): - try: - os.chmod(target, stat.S_IWRITE | stat.S_IREAD) - except OSError: - pass - try: - func(target) - return - except PermissionError: - time.sleep(0.2) - raise exc_info[1] - - shutil.rmtree(path, onerror=onerror) - - -# The tolerance matrix gives the tolerances for comparing two values of the actual results with -# results given in a diff_to. Filename globbing is supported by fnmatch. The first glob matching -# is the winner and its tolerances will be used. To implement a first match order, a regular array -# had to be used instead of a dictionary for the filetolmatrix. The first match is then implemented -# in the routine getToleranceVector(). - - -class ColTol(dict): - """ - Specify the column tolerances for all columns in a file. - This class is inherited from the dict class and overrides the missing - method to return the tolerance of the asterisk key, which is denoting - the tolerances for all not explicitly specified columns. The - tolerance for a column can be Ignore()d be using the Ignore() value or - has to be a tupple, where the first item tells whether the second is to - be evaluated. The first item can be a bool (value does not matter), to always - select the second item for evaluation, or a function accepting the - dictionary of inifile setting. The function then has to return true, - when the second item of the tupple has to be evaluated. - A tolerance for the |old-new| < tol can be specified by giving the - scalar tolerance or a function of two vectors for the second item of - the tupple. The first vector contains all columns of the old values - the second all values of the new values. The values are addressed by - the columns names taken from the newer file. The function has to return - true, when the new value is ok, false else. - Additionally ranges of tolerances or functions can be given as a sorted - list of 2-tupples. The first value is the lower bound of the column "L" for - which the second value/function is applicable. The lists is traversed as - long as "L" is smaller then the first value or the list ends. The second - value is then taken for the comparison. That value also can be an Ignore() - object, denoting that the value is to always accepted. - """ - - def __missing__(self, item): - return self["*"] - - -class Ignore: - """ - Ignore() files of this class completely. - """ - - -def diffnsqrt(old, new, tol, c1, c2): - """ - Implement |C1'x'C2_{new} - C1'x'C2_{old}| / sqrt(C1'x'C1_{old} * C2'x'C2_{old}) < tol. - :param old: The row of the old values. - :param new: The row of the new values. - :param tol: The tolerance to match. - :param c1: The name of the first component. - :param c2: The name of the second component. - :return: True, when |C1'x'C2_{new} - C1'x'C2_{old}| / sqrt(C1'x'C1_{old} * C2'x'C2_{old}) < tol, false else. - :rtype : bool - """ - oc1c1 = old[c1 + "x" + c1] - oc2c2 = old[c2 + "x" + c2] - # Skip the test when exactly one variable is negative, but not both. - if (oc1c1 < 0 or oc2c2 < 0) and (oc1c1 >= 0 or oc2c2 >= 0): - return True - res = math.fabs(new[c1 + "x" + c2] - old[c1 + "x" + c2]) / math.sqrt(oc1c1 * oc2c2) < tol - if args.verbose_diff_output and not res: - printlog( - "diffnsqrt: |%g - %g|/sqrt(%g * %g) = %g > %g" - % ( - new[c1 + "x" + c2], - old[c1 + "x" + c2], - oc1c1, - oc2c2, - math.fabs(new[c1 + "x" + c2] - old[c1 + "x" + c2]) / math.sqrt(oc1c1 * oc2c2), - tol, - ) - ) - return res - - -def normabs(o, n, tol): - """ - Compute |o - n| / |o| < tol - :param o: The old value - :param n: The new value - :param tol: toleranace - :return: True when |o - n| / |o| < tol, false else - """ - res = (math.fabs(o - n) / math.fabs(o) if o != 0.0 else math.fabs(o - n)) < tol - if args.verbose_diff_output and not res: - printlog( - "normabs: |%g - %g| / |%g| = %g > %g" - % (o, n, o, math.fabs(o - n) / math.fabs(o) if o != 0.0 else math.fabs(o - n), tol) - ) - return res - - -def wantCMBTandlmaxscalarge2000(ini_file): - """ - Return true when want_CMB is set in the ini file and l_max_scalar is >= 2000. - :param ini_file: The dictionary all inifile settings. - :return: True, when want_CMB and l_max_scalar >= 2000, false else. - """ - return ini_file.int("l_max_scalar") >= 2000 and ini_file.bool("want_CMB") - - -def wantCMBT(ini_file): - """ - Return true when want_CMB is set. - :param ini_file: The dictionary all inifile settings. - :return: True, when want_CMB is set. - """ - return ini_file.bool("want_CMB") - - -# A short cut for lensedCls and lenspotentialCls files. -coltol1 = ColTol( - { - "L": Ignore(), - "TxT": (wantCMBT, [(0, 3e-3), (600, 1e-3), (2500, 3e-3), (6000, 0.02)]), - "ExE": (wantCMBT, [(0, 3e-3), (600, 1e-3), (2500, 3e-3), (6000, 0.02), (8000, 0.1)]), - "BxB": (wantCMBTandlmaxscalarge2000, [(0, 5e-3), (1000, 1e-2), (6000, 0.02), (8000, 0.1)]), - "TxE": ( - wantCMBT, - [ - (0, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), - (600, lambda o, n: diffnsqrt(o, n, 1e-3, "T", "E")), - (2500, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), - (6000, lambda o, n: diffnsqrt(o, n, 3e-2, "T", "E")), - ], - ), - "PxP": (True, [(0, 5e-3), (1000, 1e-2), (6000, 0.02)]), - "TxP": (wantCMBT, [(0, lambda o, n: diffnsqrt(o, n, 0.01, "T", "P")), (100, Ignore())]), - "ExP": (wantCMBT, [(0, lambda o, n: diffnsqrt(o, n, 0.02, "E", "P")), (60, Ignore())]), - "TxW1": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "T", "W1")), - "ExW1": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "E", "W1")), - "PxW1": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "P", "W1")), - "W1xT": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "T")), - "W1xE": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "E")), - "W1xP": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "P")), - "W1xW1": (True, 5e-3), - "PxW2": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "P", "W2")), - "W1xW2": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "W2")), - "W2xT": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "T")), - "W2xE": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "E")), - "W2xP": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "P")), - "W2xW1": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "W1")), - "W2xW2": (True, 5e-3), - "*": Ignore(), - } -) - -coltolunlensed = dict(coltol1) -for x in ["TxT", "ExE"]: - coltolunlensed[x] = (wantCMBT, [(0, 3e-3), (600, 1e-3), (2500, 3e-3), (6000, Ignore())]) - -coltolunlensed["TxE"] = ( - wantCMBT, - [ - (0, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), - (600, lambda o, n: diffnsqrt(o, n, 1e-3, "T", "E")), - (2500, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), - (6000, Ignore()), - ], -) - -# The filetolmatrix as described above. -filetolmatrix = [ - ["*scalCls.dat", Ignore()], # Ignore() all scalCls.dat files. - ["*lensedCls.dat", coltol1], # lensed and lenspotential files both use coltol1 given above - ["*lensedtotCls.dat", coltol1], - ["*lenspotentialCls.dat", coltolunlensed], - ["*scalarCovCls.dat", coltolunlensed], - [ - "*tensCls.dat", - ColTol({"TE": (True, lambda o, n: diffnsqrt(o, n, 1e-2, "T", "E")), "*": (True, [(0, 1e-2), (600, Ignore())])}), - ], - [ - "*matterpower.dat", - ColTol({"P": (True, lambda o, n: normabs(o["P"], n["P"], 1e-3 if n["k/h"] < 1 else 3e-3)), "*": Ignore()}), - ], - [ - "*transfer_out.dat", - ColTol( - { - "baryon": (True, lambda o, n: normabs(o["baryon"], n["baryon"], 1e-3 if n["k/h"] < 1 else 3e-3)), - "CDM": (True, lambda o, n: normabs(o["CDM"], n["CDM"], 1e-3 if n["k/h"] < 1 else 3e-3)), - "v_CDM": (True, lambda o, n: normabs(o["v_CDM"], n["v_CDM"], 1e-3 if n["k/h"] < 1 else 3e-3)), - "v_b": (True, lambda o, n: normabs(o["v_b"], n["v_b"], 1e-3 if n["k/h"] < 1 else 3e-3)), - "*": Ignore(), - } - ), - ], - ["*sharp_cl_*.dat", ColTol({"CL": (True, 1e-3), "P": (True, 1e-3), "P_vv": (True, 1e-3), "*": Ignore()})], - ["*", ColTol({"*": (True, 1e-4)})], -] - - -def runScript(fname): - now = time.time() - try: - if args.runner == "module": - ensure_camb_on_path() - import camb - - camb.run_ini(fname, no_validate=args.no_validate) - res = "" - else: - res = subprocess.check_output([prog, fname], stderr=subprocess.STDOUT, text=True) - code = 0 - except subprocess.CalledProcessError as error: - res = error.output - code = error.returncode - except Exception as error: - res = str(error) - code = 1 - return time.time() - now, res, code - - -def getInis(ini_dir): - ini_files = [] - for fname in sorted(os.listdir(ini_dir)): - if fnmatch.fnmatch(fname, "*.ini"): - ini_files.append(os.path.join(args.ini_dir, fname)) - return ini_files - - -def getTestParams(): - params = [["base"]] - - for lmax in [1000, 2000, 2500, 3000, 4500, 6000]: - params.append(["lmax%s" % lmax, "l_max_scalar = %s" % lmax, "k_eta_max_scalar = %s" % (lmax * 2.5)]) - - for lmax in [1000, 2000, 2500, 3000, 4500]: - params.append( - [ - "nonlin_lmax%s" % lmax, - "do_nonlinear =2", - "get_transfer= T", - "l_max_scalar = %s" % lmax, - "k_eta_max_scalar = %s" % (lmax * 2.5), - ] - ) - - for lmax in [400, 600, 1000]: - params.append( - [ - "tensor_lmax%s" % lmax, - "get_tensor_cls = T", - "l_max_tensor = %s" % lmax, - "k_eta_max_tensor = %s" % (lmax * 2), - ] - ) - - params.append(["tensoronly", "get_scalar_cls=F", "get_tensor_cls = T"]) - params.append( - ["tensor_tranfer", "get_scalar_cls=F", "get_tensor_cls = T", "get_transfer= T", "transfer_high_precision = T"] - ) - params.append(["tranfer_only", "get_scalar_cls=F", "get_transfer= T", "transfer_high_precision = F"]) - params.append(["tranfer_highprec", "get_scalar_cls=F", "get_transfer= T", "transfer_high_precision = T"]) - - params.append(["all", "get_scalar_cls=T", "get_tensor_cls = T", "get_transfer= T"]) - params.append(["all_nonlin1", "get_scalar_cls=T", "get_tensor_cls = T", "get_transfer= T", "do_nonlinear=1"]) - params.append(["all_nonlin2", "get_scalar_cls=T", "get_tensor_cls = T", "get_transfer= T", "do_nonlinear=2"]) - params.append( - [ - "all_nonlinhigh", - "get_scalar_cls=T", - "get_tensor_cls = T", - "get_transfer= T", - "do_nonlinear=2", - "transfer_high_precision = T", - ] - ) - params.append( - [ - "tranfer_delta10", - "get_scalar_cls=F", - "get_transfer= T", - "transfer_high_precision = T", - "transfer_k_per_logint =10", - ] - ) - params.append( - ["tranfer_redshifts", "get_scalar_cls=F", "get_transfer= T", "transfer_num_redshifts=2"] - + [ - "transfer_redshift(1)=1", - "transfer_redshift(2)=0.7", - "transfer_filename(2)=transfer_out2.dat", - "transfer_matterpower(2)=matterpower2.dat", - ] - ) - params.append( - ["tranfer_redshifts2", "get_scalar_cls=F", "get_transfer= T", "transfer_num_redshifts=2"] - + [ - "transfer_redshift(1)=0.7", - "transfer_redshift(2)=0", - "transfer_filename(2)=transfer_out2.dat", - "transfer_matterpower(2)=matterpower2.dat", - ] - ) - - params.append(["tranfer_nonu", "get_scalar_cls=F", "get_transfer= T", "transfer_power_var = 8"]) - - # AM - Added HMcode and halomodel tests (halofit_version=5,6) - params.append(["HMcode", "transfer_kmax=100", "halofit_version=5", "do_nonlinear=1", "get_transfer= T"]) - params.append(["halomodel", "transfer_kmax=100", "halofit_version=6", "do_nonlinear=1", "get_transfer= T"]) - # AM - End of edits - - params.append(["zre", "re_use_optical_depth = F", "re_redshift = 8.5"]) - params.append(["nolens", "lensing = F"]) - params.append(["noderived", "derived_parameters = F"]) - params.append(["no_rad_trunc", "do_late_rad_truncation = F"]) - - for acc in [1.1, 1.5, 2.2]: - params.append(["accuracy_boost%s" % acc, "accuracy_boost = %s" % acc]) - - for acc in [1, 1.5, 2]: - params.append(["l_accuracy_boost%s" % acc, "l_accuracy_boost = %s" % acc]) - - params.append(["acc", "l_accuracy_boost =2", "accuracy_boost=2"]) - params.append(["accsamp", "l_accuracy_boost =2", "accuracy_boost=2", "l_sample_boost = 1.5"]) - - params.append(["mu_massless", "omnuh2 =0"]) - - for mnu in [0, 0.01, 0.03, 0.1]: - omnu = mnu / 100.0 - params.append(["mu_mass%s" % mnu, "omnuh2 =%s" % omnu, "massive_neutrinos = 3"]) - params.append( - [ - "mu_masssplit", - "omnuh2 =0.03", - "massive_neutrinos = 1 1", - "nu_mass_fractions=0.2 0.8", - "nu_mass_degeneracies = 1 1", - "nu_mass_eigenstates = 2", - "massless_neutrinos = 1.046", - ] - ) - - for etamax in [10000, 14000, 20000, 40000]: - params.append( - [ - "acclens_ketamax%s" % etamax, - "do_nonlinear = 2", - "l_max_scalar = 6000", - "k_eta_max_scalar = %s" % etamax, - "accurate_BB = F", - ] - ) - - for etamax in [10000, 14000, 20000, 40000]: - params.append( - [ - "acclensBB_ketamax%s" % etamax, - "do_nonlinear = 2", - "l_max_scalar = 2500", - "k_eta_max_scalar = %s" % etamax, - "accurate_BB = T", - ] - ) - - pars = { - "ombh2": [0.0219, 0.0226, 0.0253], - "omch2": [0.1, 0.08, 0.15], - "omk": [0, -0.03, 0.04, 0.001, -0.001], - "hubble": [62, 67, 71, 78], - "w": [-1.2, -1, -0.98, -0.75], - "helium_fraction": [0.21, 0.23, 0.27], - "scalar_spectral_index(1)": [0.94, 0.98], - "scalar_nrun(1)": [-0.015, 0, 0.03], - "re_optical_depth": [0.03, 0.05, 0.08, 0.11], - } - - for par, vals in pars.items(): - for val in vals: - params.append( - [ - f"{par}_{val:.3f}", - "get_transfer= T", - "do_nonlinear=1", - "transfer_high_precision = T", - f"{par} = {val}", - ] - ) - - if not args.no_de and not os.environ.get("CAMB_TESTS_NO_DE"): - for wa in [-0.3, -0.01, 0.5]: - for w in [-1.2, -0.998, -0.7]: - params.append( - [ - f"ppf_w{w}_wa{wa}", - "w = %s" % w, - "wa =%s" % wa, - "do_nonlinear = 2", - "get_transfer= T", - "dark_energy_model=PPF", - ] - ) - - params.append( - [ - "ppf_w-1.000_wa0.000", - "w = -1.0", - "wa = 0.0", - "do_nonlinear = 1", - "get_transfer= T", - "transfer_high_precision = T", - "dark_energy_model=PPF", - ] - ) - - if not args.no_sources and not os.environ.get("CAMB_TESTS_NO_SOURCES"): - # ##CAMB sources options and new outputs - params.append( - ["delta_xe", "evolve_delta_xe =T", "get_transfer= T", "do_nonlinear=2", "transfer_high_precision = T"] - ) - - def make_win(i, z, kind, bias, sigma, s): - return [ - f"redshift({i}) = {z}", - f"redshift_kind({i}) = {kind}", - f"redshift_bias({i}) = {bias}", - f"redshift_sigma({i}) = {sigma}", - f"redshift_dlog10Ndm({i}) = {s}", - ] - - counts_def = [f"DEFAULT({repo_inifile('params_counts.ini')})"] - source_counts = ( - ["num_redshiftwindows = 2"] - + make_win(1, 0.3, "counts", 1.5, 0.06, 0.42) - + make_win(2, 1, "counts", 2, 0.3, 0) - ) - bool_options = ["counts_evolve", "DoRedshiftLensing", "counts_redshift", "evolve_delta_xe"] - for b1 in ["T", "F"]: - for b2 in ["T", "F"]: - for b3 in ["T", "F"]: - for b4 in ["T", "F"]: - bs = [b1, b2, b3, b4] - pars = copy.copy(source_counts) - for opt, b in zip(bool_options, bs): - pars += [opt + " = " + b] - params.append(["counts_opts_" + "_".join(bs)] + counts_def + pars) - params.append( - ["counts_1bin"] + counts_def + ["num_redshiftwindows = 1"] + make_win(1, 0.15, "counts", 1.2, 0.04, -0.2) - ) - params.append(["counts_lmax1", "l_max_scalar = 400", "want_CMB = F"] + counts_def + source_counts) - params.append(["counts_lmax2", "l_max_scalar = 1200"] + counts_def + source_counts) - - params.append( - ["counts_overlap"] - + counts_def - + ["num_redshiftwindows = 2"] - + make_win(1, 0.17, "counts", 1.2, 0.04, -0.2) - + make_win(2, 0.2, "counts", 1.2, 0.04, -0.2) - ) - params.append(["lensing_base", f"DEFAULT({repo_inifile('params_lensing.ini')})"]) - params.append(["21cm_base", f"DEFAULT({repo_inifile('params_21cm.ini')})"]) - params.append(["21cm_base2", f"DEFAULT({repo_inifile('params_21cm.ini')})", "get_transfer = T"]) - params.append( - ["counts_lens", f"DEFAULT({repo_inifile('params_counts.ini')})"] - + ["num_redshiftwindows = 2"] - + make_win(1, 0.17, "counts", 1.2, 0.04, -0.2) - + make_win(2, 0.5, "lensing", 0, 0.07, 0.2) - ) - - max_tests = args.max_tests or os.environ.get("CAMB_TESTS_MAX") - if max_tests: - params = params[: int(max_tests)] - return params - - -def list_files(file_dir): - return [f for f in os.listdir(file_dir) if ".ini" not in f] - - -def output_file_num(file_dir): - return len(list_files(file_dir)) - - -def makeIniFiles(): - printlog("Making test ini files...") - params = getTestParams() - ini_files = [] - base_settings = os.path.abspath(args.base_settings) - for pars in params: - name = "params_" + pars[0] - fname = os.path.join(args.ini_dir, name + ".ini") - ini_files.append(fname) - write_flat_ini_file( - fname, - [ - "output_root=" + os.path.join(out_files_dir, name), - *pars[1:], - f"DEFAULT({base_settings})", - ], - ) - apply_ini_overrides(fname) - printlog("Made test ini files.") - return ini_files - - -def getPreparedIniFiles(): - if args.make_ini: - return makeIniFiles(), None - - ini_files = getInis(args.ini_dir) - if not args.override: - return ini_files, None - - override_dir = tempfile.mkdtemp(prefix="test_ini_overrides_", dir=args.ini_dir) - prepared = [] - for ini in ini_files: - copied_ini = os.path.join(override_dir, os.path.basename(ini)) - shutil.copy(ini, copied_ini) - prepared.append(apply_ini_overrides(copied_ini)) - return prepared, override_dir - - -def get_tolerance_vector(filename, cols): - """ - Get the tolerances for the given filename. - :param filename: The name of the file to retrieve the tolerances for. - :param cols: Gives the column names. - :returns: False, when the file is to be Ignored completely; - the vector of tolerances when a pattern in the filetolmatrix matched; - an empty ColTol when no match was found. - """ - for key, val in filetolmatrix: - if fnmatch.fnmatch(filename, key): - if isinstance(val, Ignore): - return False - else: - return [val.get(t, (False, False)) for t in cols] - return ColTol() - - -def num_unequal(filename, cmpFcn): - """ - Check whether two files are numerically unequal for the given compare function. - :param filename: The base name of the files to check. - :param cmpFcn: The default comparison function. Can be overriden by the filetolmatrix. - :return: True, when the files do not match, false else. - """ - orig_name = os.path.join(resolve_from_ini_dir(args.diff_to), filename) - with open(orig_name) as f: - origMat = [[_x for _x in ln.split()] for ln in f] - # Check if the first row has one more column, which is the # - if len(origMat[0]) == len(origMat[1]) + 1: - origBase = 1 - origMat[0] = origMat[0][1:] - else: - origBase = 0 - new_name = os.path.join(args.ini_dir, args.out_files_dir, filename) - with open(new_name) as f: - newMat = [[_x for _x in ln.split()] for ln in f] - if len(newMat[0]) == len(newMat[1]) + 1: - newBase = 1 - newMat[0] = newMat[0][1:] - else: - newBase = 0 - if len(origMat) - origBase != len(newMat) - newBase: - if args.verbose_diff_output: - printlog("num rows do not match in %s: %d != %d" % (filename, len(origMat), len(newMat))) - return True - if newBase == 1: - cols = [s[0] + "x" + s[1] if len(s) == 2 and s != "nu" else s for s in newMat[0]] - else: - cols = range(len(newMat[0])) - - tolerances = get_tolerance_vector(filename, cols) - row = 0 - col = 0 - try: - if tolerances: - inifilenameparts = filename.rsplit("_", 2) - inifilename = "_".join(inifilenameparts[0:2]) if inifilenameparts[1] != "transfer" else inifilenameparts[0] - inifilename += "_params.ini" - inifilename = os.path.join(args.ini_dir, args.out_files_dir, inifilename) - if not os.path.exists(inifilename): - if "sharp_cl_params" in inifilename: - inifile = make_ini_file() - else: - printlog("ini filename does not exist: %s" % inifilename) - else: - try: - # The following split fails for *_transfer_out.* files where it not needed anyway. - inifile = make_ini_file() - inifile.readFile(inifilename) - except OSError: - printlog("Could not open ini filename: %s" % inifilename) - for o_row, n_row in zip(origMat[origBase:], newMat[newBase:]): - row += 1 - if len(o_row) != len(n_row): - if args.verbose_diff_output: - printlog("num columns do not match in %s: %d != %d" % (filename, len(o_row), len(n_row))) - return True - col = 0 - of_row = [float(f) for f in o_row] - nf_row = [] - for f in n_row: - try: - nf_row += [float(f)] - except ValueError: - sp = customsplit(f) - nf_row += [float(sp[0] + "E" + sp[1])] - oldrowdict = False - newrowdict = False - for o, n in zip(of_row, nf_row): - if isinstance(tolerances[col], Ignore): - pass - else: - cond, tols = tolerances[col] - # When the column condition is bool (True or False) or a function - # returning False, then skip this column. - if isinstance(cond, bool) or not cond(inifile): - pass - else: - if isinstance(tols, float): - if not cmpFcn(o, n, tols): - if args.verbose_diff_output: - printlog( - 'value mismatch at %d, %d ("%s") of %s: %s != %s' - % (row, col + 1, cols[col], filename, o, n) - ) - return True - elif not isinstance(tols, Ignore): - if not oldrowdict: - oldrowdict = dict(zip(cols, of_row)) - newrowdict = dict(zip(cols, nf_row)) - if isinstance(tols, list): - cand = False - for lim, rhs in tols: - if lim < newrowdict["L"]: - cand = rhs - else: - break - if isinstance(cand, float): - if not cmpFcn(o, n, cand): - if args.verbose_diff_output: - printlog( - 'value mismatch at %d, %d ("%s") of %s: %s != %s' - % (row, col + 1, cols[col], filename, o, n) - ) - return True - elif not isinstance(cand, (bool, Ignore)): - if not cand(oldrowdict, newrowdict): - if args.verbose_diff_output: - printlog( - 'value mismatch at %d, %d ("%s") of %s: %s != %s' - % (row, col + 1, cols[col], filename, o, n) - ) - return True - else: - if not tols(oldrowdict, newrowdict): - if args.verbose_diff_output: - printlog( - 'value mismatch at %d, %d ("%s") of %s: %s != %s' - % (row, col + 1, cols[col], filename, o, n) - ) - return True - col += 1 - return False - else: - # if args.verbose_diff_output: - # printlog("Skipped file %s" % (filename)) - return False - except ValueError as e: - printlog("ValueError: '%s' at %d, %d in file: %s" % (e, row, col + 1, filename)) - return True - - -def customsplit(s): - """ - Need to implement our own split, because for exponents of three digits - the 'E' marking the exponent is dropped, which is not supported by python. - :param s: The string to split. - :return: An array containing the mantissa and the exponent, or the value, when no split was possible. - """ - n = len(s) - i = n - 1 - # Split the exponent from the string by looking for ['E']('+'|'-')D+ - while i > 4: - if s[i] == "+" or s[i] == "-": - return [s[0 : i - 1], s[i:n]] - i -= 1 - return [s] - - -def textualcmp(o, n, tolerance): - """ - Do a textual comparison for numbers whose exponent is zero or greater. - The fortran code writes floating point values, with 5 significant digits - after the comma and an exponent. I.e., for numbers with a positive - exponent the usual comparison against a delta fails. - :param o: The old value. - :param n: The new value. - :param tolerance: The allowed tolerance. - :return: True, when |o - n| is greater then the tolerance allows, false else. - """ - o_s = customsplit(o) - n_s = customsplit(n) - if len(o_s) > 1 and len(n_s) > 1: - o_mantise = float(o_s[0]) - o_exp = int(o_s[1]) - n_mantise = float(n_s[0]) - n_exp = int(n_s[1]) - # Check without respect of the exponent, when that is greater zero. - if 0 <= o_exp: - if o_exp != n_exp: - # Quit when exponent difference is significantly larger - if abs(o_exp - n_exp) > 1: - return True - if o_exp > n_exp: - o_mantise *= 10.0 - else: - n_mantise *= 10.0 - return math.fabs(float(o_mantise) - float(n_mantise)) >= tolerance - return math.fabs(float(o_s[0] + "E" + o_s[1]) - float(n_s[0] + "E" + n_s[1])) >= tolerance - # In all other cases do a numerical check - return math.fabs(float(o) - float(n)) >= tolerance - - -def run_diff(): - printlog("Running diff_to...") - if args.num_diff: - defCmpFcn = lambda o, n, t: math.fabs(float(o) - float(n)) >= t - else: - defCmpFcn = normabs - out_files_dir2 = resolve_from_ini_dir(args.diff_to) - _, mismatch, errors = filecmp.cmpfiles( - out_files_dir, out_files_dir2, list(set(list_files(out_files_dir)) | set(list_files(out_files_dir2))) - ) - len_errors = len(errors) - if len_errors and len_errors != 1 and errors[0] != args.diff_to: - printlog("Missing/Extra files:") - for err in errors: - if err != args.diff_to: - printlog(" " + err) - if mismatch: - numerical_mismatch = [f for f in mismatch if num_unequal(f, defCmpFcn)] - if numerical_mismatch: - printlog("Files do not match:") - for err in numerical_mismatch: - printlog(" " + err) - len_num_mismatch = len(numerical_mismatch) - else: - len_num_mismatch = 0 - - printlog("Done with %d numerical accuracy mismatches and %d extra/missing files" % (len_num_mismatch, len_errors)) - return 1 if len_errors > 0 or len_num_mismatch > 0 else 0 - - -def main(argv=None): - global args, prog, out_files_dir - - args = build_parser().parse_args(argv) - args.ini_dir = os.path.abspath(args.ini_dir) - args.base_settings = os.path.abspath(args.base_settings) - filetolmatrix[-1][1]["*"] = (True, args.diff_tolerance) - prog = os.path.abspath(args.prog) - - os.makedirs(args.ini_dir, exist_ok=True) - out_files_dir = os.path.join(args.ini_dir, args.out_files_dir) - - if args.clean and os.path.exists(out_files_dir): - remove_tree(out_files_dir) - os.makedirs(out_files_dir, exist_ok=True) - - override_dir = None - try: - if args.diff_to: - return run_diff() - - inis, override_dir = getPreparedIniFiles() - if args.no_run_test: - return 0 - - errors = 0 - files = output_file_num(out_files_dir) - if files: - printlog("Output directory is not empty (run with --clean to force delete): %s" % out_files_dir) - return 1 - start = time.time() - error_list = [] - for ini in inis: - printlog(os.path.basename(ini) + "...") - timing, output, return_code = runScript(ini) - if return_code: - printlog("error %s" % return_code) - if output: - printlog(str(output).strip()) - nfiles = output_file_num(out_files_dir) - if nfiles > files: - msg = f"..OK, produced {nfiles - files} files in {timing:.2f}s" - else: - errors += 1 - error_list.append(os.path.basename(ini)) - msg = "..no files in %.2fs" % timing - printlog(msg) - files = nfiles - printlog(f"Done, {errors} errors in {time.time() - start:.2f}s (outputs not checked yet)") - if errors: - printlog("Fails in : %s" % error_list) - return 1 if errors else 0 - finally: - if override_dir and os.path.exists(override_dir): - remove_tree(override_dir) - close_logfile() - - -if __name__ == "__main__": - raise SystemExit(main()) +import argparse +import copy +import filecmp +import fnmatch +import math +import os +import shutil +import stat +import subprocess +import sys +import tempfile +import time + +TESTS_DIR = os.path.dirname(os.path.abspath(__file__)) +FORTRAN_DIR = os.path.abspath(os.path.join(TESTS_DIR, "..")) +REPO_ROOT = os.path.abspath(os.path.join(FORTRAN_DIR, "..")) +DEFAULT_BASE_SETTINGS = os.path.join(REPO_ROOT, "inifiles", "params.ini") + + +def parse_override_assignment(text): + if "=" not in text: + raise argparse.ArgumentTypeError("Overrides must use KEY=VALUE syntax") + key, value = text.split("=", 1) + key = key.strip() + if not key: + raise argparse.ArgumentTypeError("Override key cannot be empty") + return key, value.strip() + + +def build_parser(): + parser = argparse.ArgumentParser(description="Run CAMB tests") + parser.add_argument("ini_dir", help="ini file directory") + parser.add_argument("--make_ini", "--make-ini", action="store_true", help="If set, output ini files to ini_dir") + parser.add_argument("--out_files_dir", "--out-files-dir", default="test_outputs", help="output files directory") + parser.add_argument( + "--base_settings", + "--base-settings", + default=DEFAULT_BASE_SETTINGS, + help="settings to include as defaults for all combinations", + ) + parser.add_argument("--no_run_test", "--no-run-test", action="store_true", help="Don't run tests on files") + parser.add_argument( + "--runner", + choices=("module", "command"), + default="module", + help="Use the compiled Python camb module or the legacy command-line executable", + ) + parser.add_argument("--prog", default="./camb", help="executable to run when --runner=command") + parser.add_argument("--no_validate", "--no-validate", action="store_true", help="Skip ini validation") + parser.add_argument("--clean", action="store_true", help="delete output dir before run") + parser.add_argument("--diff_to", "--diff-to", help="output directory to compare to, e.g. test_outputs2") + parser.add_argument( + "--diff_tolerance", + "--diff-tolerance", + type=float, + help="the tolerance for the numerical diff when no explicit diff is given [default: 1e-4]", + default=1e-4, + ) + parser.add_argument( + "--verbose_diff_output", + "--verbose-diff-output", + "--verbose", + action="store_true", + help="during diff_to print more error messages", + ) + parser.add_argument("--num_diff", "--num-diff", action="store_true", help="during diff_to use absolute diffs") + parser.add_argument("--no_sources", "--no-sources", action="store_true", help="turn off CAMB sources tests") + parser.add_argument("--no_de", "--no-de", action="store_true", help="Don't run dark energy tests") + parser.add_argument("--max_tests", "--max-tests", type=int, help="maximum tests to run") + parser.add_argument( + "--override", + "--set", + action="append", + type=parse_override_assignment, + default=[], + metavar="KEY=VALUE", + help="override an ini parameter for every test; can be repeated", + ) + return parser + + +args = argparse.Namespace() +prog = "" +out_files_dir = "" + +logfile = None + + +def printlog(text): + global logfile + print(text) + sys.stdout.flush() + if logfile is None: + logfile = open(os.path.join(args.ini_dir, "test_results.log"), "a", encoding="utf-8") + logfile.write(text + "\n") + + +def close_logfile(): + global logfile + if logfile is not None: + logfile.close() + logfile = None + + +def repo_inifile(name): + return os.path.join(REPO_ROOT, "inifiles", name) + + +def resolve_from_ini_dir(path): + if os.path.isabs(path): + return path + return os.path.join(args.ini_dir, path) + + +def ensure_camb_on_path(): + if REPO_ROOT not in sys.path: + sys.path.insert(0, REPO_ROOT) + + +def make_ini_file(*args, **kwargs): + ensure_camb_on_path() + from camb.inifile import IniFile + + return IniFile(*args, **kwargs) + + +def apply_ini_overrides(filename): + if not args.override: + return filename + ini = make_ini_file(filename) + for key, value in args.override: + if key not in ini.params: + ini.readOrder.append(key) + ini.params[key] = value + ini.saveFile(filename) + return filename + + +def write_flat_ini_file(filename, parameter_lines): + filename = os.path.abspath(filename) + with tempfile.NamedTemporaryFile( + "w", suffix=".ini", dir=os.path.dirname(filename), delete=False, encoding="utf-8" + ) as handle: + handle.write("\n".join(parameter_lines)) + handle.write("\n") + temp_filename = handle.name + + try: + make_ini_file(temp_filename).saveFile(filename) + finally: + os.remove(temp_filename) + + return filename + + +def remove_tree(path): + def onerror(func, target, exc_info): + for _ in range(10): + try: + os.chmod(target, stat.S_IWRITE | stat.S_IREAD) + except OSError: + pass + try: + func(target) + return + except PermissionError: + time.sleep(0.2) + raise exc_info[1] + + shutil.rmtree(path, onerror=onerror) + + +# The tolerance matrix gives the tolerances for comparing two values of the actual results with +# results given in a diff_to. Filename globbing is supported by fnmatch. The first glob matching +# is the winner and its tolerances will be used. To implement a first match order, a regular array +# had to be used instead of a dictionary for the filetolmatrix. The first match is then implemented +# in the routine getToleranceVector(). + + +class ColTol(dict): + """ + Specify the column tolerances for all columns in a file. + This class is inherited from the dict class and overrides the missing + method to return the tolerance of the asterisk key, which is denoting + the tolerances for all not explicitly specified columns. The + tolerance for a column can be Ignore()d be using the Ignore() value or + has to be a tupple, where the first item tells whether the second is to + be evaluated. The first item can be a bool (value does not matter), to always + select the second item for evaluation, or a function accepting the + dictionary of inifile setting. The function then has to return true, + when the second item of the tupple has to be evaluated. + A tolerance for the |old-new| < tol can be specified by giving the + scalar tolerance or a function of two vectors for the second item of + the tupple. The first vector contains all columns of the old values + the second all values of the new values. The values are addressed by + the columns names taken from the newer file. The function has to return + true, when the new value is ok, false else. + Additionally ranges of tolerances or functions can be given as a sorted + list of 2-tupples. The first value is the lower bound of the column "L" for + which the second value/function is applicable. The lists is traversed as + long as "L" is smaller then the first value or the list ends. The second + value is then taken for the comparison. That value also can be an Ignore() + object, denoting that the value is to always accepted. + """ + + def __missing__(self, item): + return self["*"] + + +class Ignore: + """ + Ignore() files of this class completely. + """ + + +def diffnsqrt(old, new, tol, c1, c2): + """ + Implement |C1'x'C2_{new} - C1'x'C2_{old}| / sqrt(C1'x'C1_{old} * C2'x'C2_{old}) < tol. + :param old: The row of the old values. + :param new: The row of the new values. + :param tol: The tolerance to match. + :param c1: The name of the first component. + :param c2: The name of the second component. + :return: True, when |C1'x'C2_{new} - C1'x'C2_{old}| / sqrt(C1'x'C1_{old} * C2'x'C2_{old}) < tol, false else. + :rtype : bool + """ + oc1c1 = old[c1 + "x" + c1] + oc2c2 = old[c2 + "x" + c2] + # Skip the test when exactly one variable is negative, but not both. + if (oc1c1 < 0 or oc2c2 < 0) and (oc1c1 >= 0 or oc2c2 >= 0): + return True + res = math.fabs(new[c1 + "x" + c2] - old[c1 + "x" + c2]) / math.sqrt(oc1c1 * oc2c2) < tol + if args.verbose_diff_output and not res: + printlog( + "diffnsqrt: |%g - %g|/sqrt(%g * %g) = %g > %g" + % ( + new[c1 + "x" + c2], + old[c1 + "x" + c2], + oc1c1, + oc2c2, + math.fabs(new[c1 + "x" + c2] - old[c1 + "x" + c2]) / math.sqrt(oc1c1 * oc2c2), + tol, + ) + ) + return res + + +def normabs(o, n, tol): + """ + Compute |o - n| / |o| < tol + :param o: The old value + :param n: The new value + :param tol: toleranace + :return: True when |o - n| / |o| < tol, false else + """ + res = (math.fabs(o - n) / math.fabs(o) if o != 0.0 else math.fabs(o - n)) < tol + if args.verbose_diff_output and not res: + printlog( + "normabs: |%g - %g| / |%g| = %g > %g" + % (o, n, o, math.fabs(o - n) / math.fabs(o) if o != 0.0 else math.fabs(o - n), tol) + ) + return res + + +def wantCMBTandlmaxscalarge2000(ini_file): + """ + Return true when want_CMB is set in the ini file and l_max_scalar is >= 2000. + :param ini_file: The dictionary all inifile settings. + :return: True, when want_CMB and l_max_scalar >= 2000, false else. + """ + return ini_file.int("l_max_scalar") >= 2000 and ini_file.bool("want_CMB") + + +def wantCMBT(ini_file): + """ + Return true when want_CMB is set. + :param ini_file: The dictionary all inifile settings. + :return: True, when want_CMB is set. + """ + return ini_file.bool("want_CMB") + + +# A short cut for lensedCls and lenspotentialCls files. +coltol1 = ColTol( + { + "L": Ignore(), + "TxT": (wantCMBT, [(0, 3e-3), (600, 1e-3), (2500, 3e-3), (6000, 0.02)]), + "ExE": (wantCMBT, [(0, 3e-3), (600, 1e-3), (2500, 3e-3), (6000, 0.02), (8000, 0.1)]), + "BxB": (wantCMBTandlmaxscalarge2000, [(0, 5e-3), (1000, 1e-2), (6000, 0.02), (8000, 0.1)]), + "TxE": ( + wantCMBT, + [ + (0, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), + (600, lambda o, n: diffnsqrt(o, n, 1e-3, "T", "E")), + (2500, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), + (6000, lambda o, n: diffnsqrt(o, n, 3e-2, "T", "E")), + ], + ), + "PxP": (True, [(0, 5e-3), (1000, 1e-2), (6000, 0.02)]), + "TxP": (wantCMBT, [(0, lambda o, n: diffnsqrt(o, n, 0.01, "T", "P")), (100, Ignore())]), + "ExP": (wantCMBT, [(0, lambda o, n: diffnsqrt(o, n, 0.02, "E", "P")), (60, Ignore())]), + "TxW1": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "T", "W1")), + "ExW1": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "E", "W1")), + "PxW1": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "P", "W1")), + "W1xT": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "T")), + "W1xE": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "E")), + "W1xP": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "P")), + "W1xW1": (True, 5e-3), + "PxW2": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "P", "W2")), + "W1xW2": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W1", "W2")), + "W2xT": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "T")), + "W2xE": (wantCMBT, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "E")), + "W2xP": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "P")), + "W2xW1": (True, lambda o, n: diffnsqrt(o, n, 5e-3, "W2", "W1")), + "W2xW2": (True, 5e-3), + "*": Ignore(), + } +) + +coltolunlensed = dict(coltol1) +for x in ["TxT", "ExE"]: + coltolunlensed[x] = (wantCMBT, [(0, 3e-3), (600, 1e-3), (2500, 3e-3), (6000, Ignore())]) + +coltolunlensed["TxE"] = ( + wantCMBT, + [ + (0, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), + (600, lambda o, n: diffnsqrt(o, n, 1e-3, "T", "E")), + (2500, lambda o, n: diffnsqrt(o, n, 3e-3, "T", "E")), + (6000, Ignore()), + ], +) + +# The filetolmatrix as described above. +filetolmatrix = [ + ["*scalCls.dat", Ignore()], # Ignore() all scalCls.dat files. + ["*lensedCls.dat", coltol1], # lensed and lenspotential files both use coltol1 given above + ["*lensedtotCls.dat", coltol1], + ["*lenspotentialCls.dat", coltolunlensed], + ["*scalarCovCls.dat", coltolunlensed], + [ + "*tensCls.dat", + ColTol({"TE": (True, lambda o, n: diffnsqrt(o, n, 1e-2, "T", "E")), "*": (True, [(0, 1e-2), (600, Ignore())])}), + ], + [ + "*matterpower.dat", + ColTol({"P": (True, lambda o, n: normabs(o["P"], n["P"], 1e-3 if n["k/h"] < 1 else 3e-3)), "*": Ignore()}), + ], + [ + "*transfer_out.dat", + ColTol( + { + "baryon": (True, lambda o, n: normabs(o["baryon"], n["baryon"], 1e-3 if n["k/h"] < 1 else 3e-3)), + "CDM": (True, lambda o, n: normabs(o["CDM"], n["CDM"], 1e-3 if n["k/h"] < 1 else 3e-3)), + "v_CDM": (True, lambda o, n: normabs(o["v_CDM"], n["v_CDM"], 1e-3 if n["k/h"] < 1 else 3e-3)), + "v_b": (True, lambda o, n: normabs(o["v_b"], n["v_b"], 1e-3 if n["k/h"] < 1 else 3e-3)), + "*": Ignore(), + } + ), + ], + ["*sharp_cl_*.dat", ColTol({"CL": (True, 1e-3), "P": (True, 1e-3), "P_vv": (True, 1e-3), "*": Ignore()})], + ["*", ColTol({"*": (True, 1e-4)})], +] + + +def runScript(fname): + now = time.time() + try: + if args.runner == "module": + ensure_camb_on_path() + import camb + + camb.run_ini(fname, no_validate=args.no_validate) + res = "" + else: + res = subprocess.check_output([prog, fname], stderr=subprocess.STDOUT, text=True) + code = 0 + except subprocess.CalledProcessError as error: + res = error.output + code = error.returncode + except Exception as error: + res = str(error) + code = 1 + return time.time() - now, res, code + + +def getInis(ini_dir): + ini_files = [] + for fname in sorted(os.listdir(ini_dir)): + if fnmatch.fnmatch(fname, "*.ini"): + ini_files.append(os.path.join(args.ini_dir, fname)) + return ini_files + + +def getTestParams(): + params = [["base"]] + + for lmax in [1000, 2000, 2500, 3000, 4500, 6000]: + params.append(["lmax%s" % lmax, "l_max_scalar = %s" % lmax, "k_eta_max_scalar = %s" % (lmax * 2.5)]) + + for lmax in [1000, 2000, 2500, 3000, 4500]: + params.append( + [ + "nonlin_lmax%s" % lmax, + "do_nonlinear =2", + "get_transfer= T", + "l_max_scalar = %s" % lmax, + "k_eta_max_scalar = %s" % (lmax * 2.5), + ] + ) + + for lmax in [400, 600, 1000]: + params.append( + [ + "tensor_lmax%s" % lmax, + "get_tensor_cls = T", + "l_max_tensor = %s" % lmax, + "k_eta_max_tensor = %s" % (lmax * 2), + ] + ) + + params.append(["tensoronly", "get_scalar_cls=F", "get_tensor_cls = T"]) + params.append( + ["tensor_tranfer", "get_scalar_cls=F", "get_tensor_cls = T", "get_transfer= T", "transfer_high_precision = T"] + ) + params.append(["tranfer_only", "get_scalar_cls=F", "get_transfer= T", "transfer_high_precision = F"]) + params.append(["tranfer_highprec", "get_scalar_cls=F", "get_transfer= T", "transfer_high_precision = T"]) + + params.append(["all", "get_scalar_cls=T", "get_tensor_cls = T", "get_transfer= T"]) + params.append(["all_nonlin1", "get_scalar_cls=T", "get_tensor_cls = T", "get_transfer= T", "do_nonlinear=1"]) + params.append(["all_nonlin2", "get_scalar_cls=T", "get_tensor_cls = T", "get_transfer= T", "do_nonlinear=2"]) + params.append( + [ + "all_nonlinhigh", + "get_scalar_cls=T", + "get_tensor_cls = T", + "get_transfer= T", + "do_nonlinear=2", + "transfer_high_precision = T", + ] + ) + params.append( + [ + "tranfer_delta10", + "get_scalar_cls=F", + "get_transfer= T", + "transfer_high_precision = T", + "transfer_k_per_logint =10", + ] + ) + params.append( + ["tranfer_redshifts", "get_scalar_cls=F", "get_transfer= T", "transfer_num_redshifts=2"] + + [ + "transfer_redshift(1)=1", + "transfer_redshift(2)=0.7", + "transfer_filename(2)=transfer_out2.dat", + "transfer_matterpower(2)=matterpower2.dat", + ] + ) + params.append( + ["tranfer_redshifts2", "get_scalar_cls=F", "get_transfer= T", "transfer_num_redshifts=2"] + + [ + "transfer_redshift(1)=0.7", + "transfer_redshift(2)=0", + "transfer_filename(2)=transfer_out2.dat", + "transfer_matterpower(2)=matterpower2.dat", + ] + ) + + params.append(["tranfer_nonu", "get_scalar_cls=F", "get_transfer= T", "transfer_power_var = 8"]) + + # AM - Added HMcode and halomodel tests (halofit_version=5,6) + params.append(["HMcode", "transfer_kmax=100", "halofit_version=5", "do_nonlinear=1", "get_transfer= T"]) + params.append(["halomodel", "transfer_kmax=100", "halofit_version=6", "do_nonlinear=1", "get_transfer= T"]) + # AM - End of edits + + params.append(["zre", "re_use_optical_depth = F", "re_redshift = 8.5"]) + params.append(["nolens", "lensing = F"]) + params.append(["noderived", "derived_parameters = F"]) + params.append(["no_rad_trunc", "do_late_rad_truncation = F"]) + + for acc in [1.1, 1.5, 2.2]: + params.append(["accuracy_boost%s" % acc, "accuracy_boost = %s" % acc]) + + for acc in [1, 1.5, 2]: + params.append(["l_accuracy_boost%s" % acc, "l_accuracy_boost = %s" % acc]) + + params.append(["acc", "l_accuracy_boost =2", "accuracy_boost=2"]) + params.append(["accsamp", "l_accuracy_boost =2", "accuracy_boost=2", "l_sample_boost = 1.5"]) + + params.append(["mu_massless", "omnuh2 =0"]) + + for mnu in [0, 0.01, 0.03, 0.1]: + omnu = mnu / 100.0 + params.append(["mu_mass%s" % mnu, "omnuh2 =%s" % omnu, "massive_neutrinos = 3"]) + params.append( + [ + "mu_masssplit", + "omnuh2 =0.03", + "massive_neutrinos = 1 1", + "nu_mass_fractions=0.2 0.8", + "nu_mass_degeneracies = 1 1", + "nu_mass_eigenstates = 2", + "massless_neutrinos = 1.046", + ] + ) + + for etamax in [10000, 14000, 20000, 40000]: + params.append( + [ + "acclens_ketamax%s" % etamax, + "do_nonlinear = 2", + "l_max_scalar = 6000", + "k_eta_max_scalar = %s" % etamax, + "accurate_BB = F", + ] + ) + + for etamax in [10000, 14000, 20000, 40000]: + params.append( + [ + "acclensBB_ketamax%s" % etamax, + "do_nonlinear = 2", + "l_max_scalar = 2500", + "k_eta_max_scalar = %s" % etamax, + "accurate_BB = T", + ] + ) + + pars = { + "ombh2": [0.0219, 0.0226, 0.0253], + "omch2": [0.1, 0.08, 0.15], + "omk": [0, -0.03, 0.04, 0.001, -0.001], + "hubble": [62, 67, 71, 78], + "w": [-1.2, -1, -0.98, -0.75], + "helium_fraction": [0.21, 0.23, 0.27], + "scalar_spectral_index(1)": [0.94, 0.98], + "scalar_nrun(1)": [-0.015, 0, 0.03], + "re_optical_depth": [0.03, 0.05, 0.08, 0.11], + } + + for par, vals in pars.items(): + for val in vals: + params.append( + [ + f"{par}_{val:.3f}", + "get_transfer= T", + "do_nonlinear=1", + "transfer_high_precision = T", + f"{par} = {val}", + ] + ) + + if not args.no_de and not os.environ.get("CAMB_TESTS_NO_DE"): + for wa in [-0.3, -0.01, 0.5]: + for w in [-1.2, -0.998, -0.7]: + params.append( + [ + f"ppf_w{w}_wa{wa}", + "w = %s" % w, + "wa =%s" % wa, + "do_nonlinear = 2", + "get_transfer= T", + "dark_energy_model=PPF", + ] + ) + + params.append( + [ + "ppf_w-1.000_wa0.000", + "w = -1.0", + "wa = 0.0", + "do_nonlinear = 1", + "get_transfer= T", + "transfer_high_precision = T", + "dark_energy_model=PPF", + ] + ) + + if not args.no_sources and not os.environ.get("CAMB_TESTS_NO_SOURCES"): + # ##CAMB sources options and new outputs + params.append( + ["delta_xe", "evolve_delta_xe =T", "get_transfer= T", "do_nonlinear=2", "transfer_high_precision = T"] + ) + + def make_win(i, z, kind, bias, sigma, s): + return [ + f"redshift({i}) = {z}", + f"redshift_kind({i}) = {kind}", + f"redshift_bias({i}) = {bias}", + f"redshift_sigma({i}) = {sigma}", + f"redshift_dlog10Ndm({i}) = {s}", + ] + + counts_def = [f"DEFAULT({repo_inifile('params_counts.ini')})"] + source_counts = ( + ["num_redshiftwindows = 2"] + + make_win(1, 0.3, "counts", 1.5, 0.06, 0.42) + + make_win(2, 1, "counts", 2, 0.3, 0) + ) + bool_options = ["counts_evolve", "DoRedshiftLensing", "counts_redshift", "evolve_delta_xe"] + for b1 in ["T", "F"]: + for b2 in ["T", "F"]: + for b3 in ["T", "F"]: + for b4 in ["T", "F"]: + bs = [b1, b2, b3, b4] + pars = copy.copy(source_counts) + for opt, b in zip(bool_options, bs): + pars += [opt + " = " + b] + params.append(["counts_opts_" + "_".join(bs)] + counts_def + pars) + params.append( + ["counts_1bin"] + counts_def + ["num_redshiftwindows = 1"] + make_win(1, 0.15, "counts", 1.2, 0.04, -0.2) + ) + params.append(["counts_lmax1", "l_max_scalar = 400", "want_CMB = F"] + counts_def + source_counts) + params.append(["counts_lmax2", "l_max_scalar = 1200"] + counts_def + source_counts) + + params.append( + ["counts_overlap"] + + counts_def + + ["num_redshiftwindows = 2"] + + make_win(1, 0.17, "counts", 1.2, 0.04, -0.2) + + make_win(2, 0.2, "counts", 1.2, 0.04, -0.2) + ) + params.append(["lensing_base", f"DEFAULT({repo_inifile('params_lensing.ini')})"]) + params.append(["21cm_base", f"DEFAULT({repo_inifile('params_21cm.ini')})"]) + params.append(["21cm_base2", f"DEFAULT({repo_inifile('params_21cm.ini')})", "get_transfer = T"]) + params.append( + ["counts_lens", f"DEFAULT({repo_inifile('params_counts.ini')})"] + + ["num_redshiftwindows = 2"] + + make_win(1, 0.17, "counts", 1.2, 0.04, -0.2) + + make_win(2, 0.5, "lensing", 0, 0.07, 0.2) + ) + + max_tests = args.max_tests or os.environ.get("CAMB_TESTS_MAX") + if max_tests: + params = params[: int(max_tests)] + return params + + +def list_files(file_dir): + return [f for f in os.listdir(file_dir) if ".ini" not in f] + + +def output_file_num(file_dir): + return len(list_files(file_dir)) + + +def makeIniFiles(): + printlog("Making test ini files...") + params = getTestParams() + ini_files = [] + base_settings = os.path.abspath(args.base_settings) + for pars in params: + name = "params_" + pars[0] + fname = os.path.join(args.ini_dir, name + ".ini") + ini_files.append(fname) + write_flat_ini_file( + fname, + [ + "output_root=" + os.path.join(out_files_dir, name), + *pars[1:], + f"DEFAULT({base_settings})", + ], + ) + apply_ini_overrides(fname) + printlog("Made test ini files.") + return ini_files + + +def getPreparedIniFiles(): + if args.make_ini: + return makeIniFiles(), None + + ini_files = getInis(args.ini_dir) + if not args.override: + return ini_files, None + + override_dir = tempfile.mkdtemp(prefix="test_ini_overrides_", dir=args.ini_dir) + prepared = [] + for ini in ini_files: + copied_ini = os.path.join(override_dir, os.path.basename(ini)) + shutil.copy(ini, copied_ini) + prepared.append(apply_ini_overrides(copied_ini)) + return prepared, override_dir + + +def get_tolerance_vector(filename, cols): + """ + Get the tolerances for the given filename. + :param filename: The name of the file to retrieve the tolerances for. + :param cols: Gives the column names. + :returns: False, when the file is to be Ignored completely; + the vector of tolerances when a pattern in the filetolmatrix matched; + an empty ColTol when no match was found. + """ + for key, val in filetolmatrix: + if fnmatch.fnmatch(filename, key): + if isinstance(val, Ignore): + return False + else: + return [val.get(t, (False, False)) for t in cols] + return ColTol() + + +def num_unequal(filename, cmpFcn): + """ + Check whether two files are numerically unequal for the given compare function. + :param filename: The base name of the files to check. + :param cmpFcn: The default comparison function. Can be overriden by the filetolmatrix. + :return: True, when the files do not match, false else. + """ + orig_name = os.path.join(resolve_from_ini_dir(args.diff_to), filename) + with open(orig_name) as f: + origMat = [[_x for _x in ln.split()] for ln in f] + # Check if the first row has one more column, which is the # + if len(origMat[0]) == len(origMat[1]) + 1: + origBase = 1 + origMat[0] = origMat[0][1:] + else: + origBase = 0 + new_name = os.path.join(args.ini_dir, args.out_files_dir, filename) + with open(new_name) as f: + newMat = [[_x for _x in ln.split()] for ln in f] + if len(newMat[0]) == len(newMat[1]) + 1: + newBase = 1 + newMat[0] = newMat[0][1:] + else: + newBase = 0 + if len(origMat) - origBase != len(newMat) - newBase: + if args.verbose_diff_output: + printlog("num rows do not match in %s: %d != %d" % (filename, len(origMat), len(newMat))) + return True + if newBase == 1: + cols = [s[0] + "x" + s[1] if len(s) == 2 and s != "nu" else s for s in newMat[0]] + else: + cols = range(len(newMat[0])) + + tolerances = get_tolerance_vector(filename, cols) + row = 0 + col = 0 + try: + if tolerances: + inifilenameparts = filename.rsplit("_", 2) + inifilename = "_".join(inifilenameparts[0:2]) if inifilenameparts[1] != "transfer" else inifilenameparts[0] + inifilename += "_params.ini" + inifilename = os.path.join(args.ini_dir, args.out_files_dir, inifilename) + if not os.path.exists(inifilename): + if "sharp_cl_params" in inifilename: + inifile = make_ini_file() + else: + printlog("ini filename does not exist: %s" % inifilename) + else: + try: + # The following split fails for *_transfer_out.* files where it not needed anyway. + inifile = make_ini_file() + inifile.readFile(inifilename) + except OSError: + printlog("Could not open ini filename: %s" % inifilename) + for o_row, n_row in zip(origMat[origBase:], newMat[newBase:]): + row += 1 + if len(o_row) != len(n_row): + if args.verbose_diff_output: + printlog("num columns do not match in %s: %d != %d" % (filename, len(o_row), len(n_row))) + return True + col = 0 + of_row = [float(f) for f in o_row] + nf_row = [] + for f in n_row: + try: + nf_row += [float(f)] + except ValueError: + sp = customsplit(f) + nf_row += [float(sp[0] + "E" + sp[1])] + oldrowdict = False + newrowdict = False + for o, n in zip(of_row, nf_row): + if isinstance(tolerances[col], Ignore): + pass + else: + cond, tols = tolerances[col] + # When the column condition is bool (True or False) or a function + # returning False, then skip this column. + if isinstance(cond, bool) or not cond(inifile): + pass + else: + if isinstance(tols, float): + if not cmpFcn(o, n, tols): + if args.verbose_diff_output: + printlog( + 'value mismatch at %d, %d ("%s") of %s: %s != %s' + % (row, col + 1, cols[col], filename, o, n) + ) + return True + elif not isinstance(tols, Ignore): + if not oldrowdict: + oldrowdict = dict(zip(cols, of_row)) + newrowdict = dict(zip(cols, nf_row)) + if isinstance(tols, list): + cand = False + for lim, rhs in tols: + if lim < newrowdict["L"]: + cand = rhs + else: + break + if isinstance(cand, float): + if not cmpFcn(o, n, cand): + if args.verbose_diff_output: + printlog( + 'value mismatch at %d, %d ("%s") of %s: %s != %s' + % (row, col + 1, cols[col], filename, o, n) + ) + return True + elif not isinstance(cand, (bool, Ignore)): + if not cand(oldrowdict, newrowdict): + if args.verbose_diff_output: + printlog( + 'value mismatch at %d, %d ("%s") of %s: %s != %s' + % (row, col + 1, cols[col], filename, o, n) + ) + return True + else: + if not tols(oldrowdict, newrowdict): + if args.verbose_diff_output: + printlog( + 'value mismatch at %d, %d ("%s") of %s: %s != %s' + % (row, col + 1, cols[col], filename, o, n) + ) + return True + col += 1 + return False + else: + # if args.verbose_diff_output: + # printlog("Skipped file %s" % (filename)) + return False + except ValueError as e: + printlog("ValueError: '%s' at %d, %d in file: %s" % (e, row, col + 1, filename)) + return True + + +def customsplit(s): + """ + Need to implement our own split, because for exponents of three digits + the 'E' marking the exponent is dropped, which is not supported by python. + :param s: The string to split. + :return: An array containing the mantissa and the exponent, or the value, when no split was possible. + """ + n = len(s) + i = n - 1 + # Split the exponent from the string by looking for ['E']('+'|'-')D+ + while i > 4: + if s[i] == "+" or s[i] == "-": + return [s[0 : i - 1], s[i:n]] + i -= 1 + return [s] + + +def textualcmp(o, n, tolerance): + """ + Do a textual comparison for numbers whose exponent is zero or greater. + The fortran code writes floating point values, with 5 significant digits + after the comma and an exponent. I.e., for numbers with a positive + exponent the usual comparison against a delta fails. + :param o: The old value. + :param n: The new value. + :param tolerance: The allowed tolerance. + :return: True, when |o - n| is greater then the tolerance allows, false else. + """ + o_s = customsplit(o) + n_s = customsplit(n) + if len(o_s) > 1 and len(n_s) > 1: + o_mantise = float(o_s[0]) + o_exp = int(o_s[1]) + n_mantise = float(n_s[0]) + n_exp = int(n_s[1]) + # Check without respect of the exponent, when that is greater zero. + if 0 <= o_exp: + if o_exp != n_exp: + # Quit when exponent difference is significantly larger + if abs(o_exp - n_exp) > 1: + return True + if o_exp > n_exp: + o_mantise *= 10.0 + else: + n_mantise *= 10.0 + return math.fabs(float(o_mantise) - float(n_mantise)) >= tolerance + return math.fabs(float(o_s[0] + "E" + o_s[1]) - float(n_s[0] + "E" + n_s[1])) >= tolerance + # In all other cases do a numerical check + return math.fabs(float(o) - float(n)) >= tolerance + + +def run_diff(): + printlog("Running diff_to...") + if args.num_diff: + defCmpFcn = lambda o, n, t: math.fabs(float(o) - float(n)) >= t + else: + defCmpFcn = normabs + out_files_dir2 = resolve_from_ini_dir(args.diff_to) + _, mismatch, errors = filecmp.cmpfiles( + out_files_dir, out_files_dir2, list(set(list_files(out_files_dir)) | set(list_files(out_files_dir2))) + ) + len_errors = len(errors) + if len_errors and len_errors != 1 and errors[0] != args.diff_to: + printlog("Missing/Extra files:") + for err in errors: + if err != args.diff_to: + printlog(" " + err) + if mismatch: + numerical_mismatch = [f for f in mismatch if num_unequal(f, defCmpFcn)] + if numerical_mismatch: + printlog("Files do not match:") + for err in numerical_mismatch: + printlog(" " + err) + len_num_mismatch = len(numerical_mismatch) + else: + len_num_mismatch = 0 + + printlog("Done with %d numerical accuracy mismatches and %d extra/missing files" % (len_num_mismatch, len_errors)) + return 1 if len_errors > 0 or len_num_mismatch > 0 else 0 + + +def main(argv=None): + global args, prog, out_files_dir + + args = build_parser().parse_args(argv) + args.ini_dir = os.path.abspath(args.ini_dir) + args.base_settings = os.path.abspath(args.base_settings) + filetolmatrix[-1][1]["*"] = (True, args.diff_tolerance) + prog = os.path.abspath(args.prog) + + os.makedirs(args.ini_dir, exist_ok=True) + out_files_dir = os.path.join(args.ini_dir, args.out_files_dir) + + if args.clean and os.path.exists(out_files_dir): + remove_tree(out_files_dir) + os.makedirs(out_files_dir, exist_ok=True) + + override_dir = None + try: + if args.diff_to: + return run_diff() + + inis, override_dir = getPreparedIniFiles() + if args.no_run_test: + return 0 + + errors = 0 + files = output_file_num(out_files_dir) + if files: + printlog("Output directory is not empty (run with --clean to force delete): %s" % out_files_dir) + return 1 + start = time.time() + error_list = [] + for ini in inis: + printlog(os.path.basename(ini) + "...") + timing, output, return_code = runScript(ini) + if return_code: + printlog("error %s" % return_code) + if output: + printlog(str(output).strip()) + nfiles = output_file_num(out_files_dir) + if nfiles > files: + msg = f"..OK, produced {nfiles - files} files in {timing:.2f}s" + else: + errors += 1 + error_list.append(os.path.basename(ini)) + msg = "..no files in %.2fs" % timing + printlog(msg) + files = nfiles + printlog(f"Done, {errors} errors in {time.time() - start:.2f}s (outputs not checked yet)") + if errors: + printlog("Fails in : %s" % error_list) + return 1 if errors else 0 + finally: + if override_dir and os.path.exists(override_dir): + remove_tree(override_dir) + close_logfile() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index f70afc38..41068b01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=77", "packaging>=24.2", "wheel"] +requires = ["setuptools>=77", "packaging>=24.2"] build-backend = "setuptools.build_meta" [project] @@ -31,7 +31,14 @@ dependencies = [ ] [project.optional-dependencies] -docs = ["sphinx>=4.0", "sphinx_rtd_theme>=1.0", "sphinxcontrib-jquery", "sphinx_markdown_builder", "jupytext"] +docs = [ + "sphinx>=4.0", + "sphinx_rtd_theme>=1.0", + "sphinxcontrib-jquery", + "sphinxcontrib-programoutput", + "sphinx_markdown_builder", + "jupytext", +] dev = ["ruff>=0.11.0", "pre-commit>=3.0.0"] [project.scripts] diff --git a/setup.py b/setup.py index a8da7119..2f3344c8 100644 --- a/setup.py +++ b/setup.py @@ -6,10 +6,14 @@ from setuptools import Command, Extension, setup from setuptools.command.build_ext import build_ext + +try: + from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel +except ImportError: + from wheel.bdist_wheel import bdist_wheel as _bdist_wheel from setuptools.command.build_py import build_py from setuptools.command.develop import develop from setuptools.command.install import install -from wheel.bdist_wheel import bdist_wheel as _bdist_wheel file_dir = os.path.abspath(os.path.dirname(__file__)) os.chdir(file_dir) @@ -92,6 +96,27 @@ def clean_dir(path, rmdir=False): os.rmdir(path) +def remove_stale_dependency_files(build_dir, expected_forutils_path): + if not os.path.isdir(build_dir): + return + + normalized_forutils_path = os.path.abspath(expected_forutils_path).replace("\\", "/") + + for name in os.listdir(build_dir): + if not name.endswith(".d"): + continue + + dep_path = os.path.join(build_dir, name) + try: + with open(dep_path, encoding="utf-8") as dep_file: + dep_contents = dep_file.read().replace("\\", "/") + except OSError: + continue + + if "/forutils/" in dep_contents and normalized_forutils_path not in dep_contents: + os.remove(dep_path) + + def make_library(cluster=False): os.chdir(os.path.join(file_dir, "fortran")) pycamb_path = ".." @@ -179,14 +204,20 @@ def make_library(cluster=False): 'Build failed - you must have "make" installed. ' 'E.g. on ubuntu install with "sudo apt install make" (or use build-essential package).' ) - get_forutils() + fpath = get_forutils() + remove_stale_dependency_files(os.path.join(file_dir, "fortran", "Releaselib"), fpath) + remove_stale_dependency_files(os.path.join(file_dir, "fortran", "Debuglib"), fpath) + if os.path.exists(lib_file) and not os.access(lib_file, os.W_OK): + os.remove(lib_file) print("Compiling source...") subprocess.call( "make python PYCAMB_OUTPUT_DIR=%s/camb/ CLUSTER_SAFE=%d" % (pycamb_path, int(cluster if not os.getenv("GITHUB_ACTIONS") else 1)), shell=True, ) - subprocess.call("chmod 755 %s" % lib_file, shell=True) + + if os.path.isfile(lib_file): + subprocess.call("chmod 755 %s" % lib_file, shell=True) if not os.path.isfile(os.path.join(pycamb_path, "camb", DLLNAME)): sys.exit("Compilation failed")