Skip to content

Add SP(k) baryonic suppression model#1291

Open
jemme07 wants to merge 42 commits into
LSSTDESC:masterfrom
jemme07:feature/spk-baryons
Open

Add SP(k) baryonic suppression model#1291
jemme07 wants to merge 42 commits into
LSSTDESC:masterfrom
jemme07:feature/spk-baryons

Conversation

@jemme07
Copy link
Copy Markdown

@jemme07 jemme07 commented May 9, 2026

Overview

This PR adds the SP(k) baryon-suppression model (Salcido et al. 2023) to pyccl as a new optional baryonic wrapper:

  • pyccl.BaryonsSPK

Model references:

The wrapper keeps CCL-facing wavenumbers in Mpc^-1 and performs the internal conversion to h/Mpc required by pyspk.

What is implemented

1. SP(k) wrapper interface

  • pyccl/baryons/spk.py implements the SP(k) multiplicative correction:
P_bar(k, a) = P_DMO(k, a) * f_SPk(k, a)
  • Supported relation modes:
    • power_law
    • cosmo_power_law
    • double_power_law
    • binned
  • Strict validation of required and optional relation parameters.
  • Clear error if optional dependency is missing:
ModuleNotFoundError: BaryonsSPK requires `pyspk>=2.0.0`

2. CCL integration behavior

  • Exported through pyccl/baryons/__init__.py and documented under pyccl.baryons API pages.
  • Public suppression path: boost_factor(cosmo, k, a).
  • Power-spectrum integration path: _include_baryonic_effects(cosmo, pk).
  • Calibrated-domain policy:
    • boost_factor(...): raises an error if any requested k is above the pyspk calibrated range
    • include_baryonic_effects(...): for out-of-range k, uses suppression = 1 (no baryonic correction) to keep Pk2D stable
  • pyspk warnings are forwarded to CCL warnings and deduplicated per instance.

3. Performance behavior for iterative workflows

  • Uses cached pyspk.build_sup_model_evaluator(...) objects for repeated calls on the same effective k/h grid.
  • Cache is bounded and thread-safe (threading.Lock + LRU-style eviction).
  • Designed to avoid repeated evaluator rebuilds in parameter-inference and MCMC-like loops.

4. Documentation and packaging updates

  • Added API docs page:
    • readthedocs/api/pyccl.baryons.spk.rst
  • Added install note for optional dependency:
    • readthedocs/source/installation.rst
  • Added changelog note under Unreleased:
    • CHANGELOG.md

5. Validation tests and benchmarks

  • Unit tests in pyccl/tests/test_baryons_spk.py cover:
    • smoke tests (scalar/array input)
    • direct agreement against pyspk evaluators
    • unit-conversion transparency (Mpc^-1 to h/Mpc)
    • high-k policy checks
    • cosmology integration behavior
    • missing dependency path
    • warning deduplication behavior
  • Benchmark tests in benchmarks/test_spk_baryons.py cover:
    • reference regression for power_law
    • include-vs-boost consistency
    • relation-matrix checks across SO and supported modes
  • Added benchmark reference data:
    • benchmarks/data/spk_power_law_fk.txt
output_SPK

Validation run

Focused commands:

pytest -q pyccl/tests/test_baryons_spk.py
pytest -q benchmarks/test_spk_baryons.py

jemme07 and others added 27 commits May 5, 2026 18:42
- Introduced `BaryonsSPK` class for SP(k) baryonic suppression using `pyspk>=2.0.0`.
- Updated environment.yml to include `pyspk` dependency.
- Enhanced installation and API documentation for the new model.
- Added unit tests for `BaryonsSPK` functionality and validation.
- Lower default k_min_hmpc from 0.1 to 0.005 h/Mpc so the internal SP(k)
  grid covers the full physically relevant low-k range and flat left-
  extrapolation stays near unity (where baryonic effects vanish).

- Rewrite _include_baryonic_effects to apply SP(k) only within the
  calibrated k range [k_min_hmpc, k_max_hmpc] and calibrated redshift
  range z <= CALIBRATED_Z_MAX (queried from pyspk.constants).  CCL's
  internal Pk2D spline grid extends to k ~ 1500 h/Mpc and a ~ 0.01
  (z ~ 99), both far outside pyspk calibration; those cells are left
  at unity instead of triggering the OOB error policy or returning
  garbage values.

- Relax test_spk_matches_pyspk tolerance from atol=1e-8 to atol=1e-3.
  CCL evaluates SP(k) on a fixed 128-point internal grid then
  interpolates, while the test evaluates pyspk directly on the target
  k array; the resulting interpolation error is O(1e-4), which is well
  within the ~1% accuracy of the model.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Refactored warning handling in BaryonsSPK to use a dedicated function.
- Improved error messages for missing required parameters in relation normalization.
- Added support for out-of-bounds policy handling in the include_baryonic_effects method.
- Updated tests to cover new out-of-bounds behavior and ensure consistency with reference data.
- Introduced a new benchmark data file for SP(k) power law reference boost factors.
- Introduced a context manager to catch warnings when building the model evaluator.
- Ensured that warnings are recorded and processed after the evaluator is created.
- This change improves the robustness of the BaryonsSPK class by providing better feedback on potential issues during model evaluation.
…ounds handling

- Introduced caching for the SP(k) evaluator and its corresponding k-grid to optimize performance.
- Simplified the evaluator building process by removing unnecessary conditional checks.
- Enhanced out-of-bounds policy handling by removing warnings for internal spline grid usage.
- Updated the `boost_factor` and `include_baryonic_effects` methods to utilize the new caching mechanism.
- Modified tests to reflect changes in out-of-bounds policy behavior and ensure consistency in baryonic effects calculations.
- Updated the calculation of k_min and k_max to be in h-Mpc instead of Mpc.
- Adjusted the generation of the k_grid_hmpc to use the new h-Mpc bounds.
- Added a check to return a copy of the suppression grid if the mapped k values match the cached k grid.
- Removed k_min_mpc, k_max_mpc, n_k, and out_of_bounds_policy parameters from BaryonsSPK constructor.
- Introduced max_evaluator_cache_size to manage evaluator caching.
- Simplified evaluator caching logic using OrderedDict.
- Updated boost_factor and include_baryonic_effects methods to handle high-k requests more gracefully.
- Adjusted tests to reflect changes in out-of-bounds policy handling and removed unnecessary parameters.
- Enhanced documentation and comments for clarity.
Copy link
Copy Markdown
Collaborator

@damonge damonge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot Jaime! Looks great, just a few minor comments/questions.

Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Copy link
Copy Markdown
Author

@jemme07 jemme07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @damonge! I addressed all of your comments.

The main change was refactoring the out-of-range handling: instead of the old internal behavior, it now uses explicit user-facing options for both k and z out-of-range cases. I think this makes the behavior clearer and more consistent for users.

Hope this is useful. I’d be happy to make any follow-up tweaks you’d like.

Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
Comment thread pyccl/baryons/spk.py Outdated
@jemme07 jemme07 requested a review from damonge May 13, 2026 20:04
@damonge
Copy link
Copy Markdown
Collaborator

damonge commented May 14, 2026

Thanks! I think the linter is failing. Maybe check flake8?

@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 25822586014

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-0.3%) to 97.205%

Details

  • Coverage decreased (-0.3%) from the base build.
  • Patch coverage: 24 uncovered changes across 1 file (193 of 217 lines covered, 88.94%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
pyccl/baryons/spk.py 216 192 88.89%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 7013
Covered Lines: 6817
Line Coverage: 97.21%
Coverage Strength: 0.97 hits per line

💛 - Coveralls

@damonge
Copy link
Copy Markdown
Collaborator

damonge commented May 14, 2026

Also, a bunch of tests seem to be failing.

@damonge
Copy link
Copy Markdown
Collaborator

damonge commented May 14, 2026

oh, the failed tests are just to do with coveralls for linux. I can fix those on a different PR, so you can ignore those. Maybe just fix the linting.

@jemme07
Copy link
Copy Markdown
Author

jemme07 commented May 14, 2026

Thanks! I think the linter is failing. Maybe check flake8?

Thanks! I fixed all lint issues. They were all line-length related. I usually run Ruff locally because it is faster, but Flake8 in this repo uses a different line-length setting, so I aligned everything to that.

I also improved some documentation.

oh, the failed tests are just to do with coveralls for linux. I can fix those on a different PR, so you can ignore those. Maybe just fix the linting.

For the Coveralls/Linux failures, I think they are related to parallel coverage reporting in CI. I made a quick fix, so let’s see if that resolves it.

Copy link
Copy Markdown
Author

@jemme07 jemme07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All donde @damonge . Ready for review again.

@jemme07
Copy link
Copy Markdown
Author

jemme07 commented May 15, 2026

Hey David @damonge, a unit test check failed on macOS (Python 3.11) but it's unrelated to the SP(k) implementation. The failing test (test_caching_lru) checks the caching behaviour but I think in mac the fast calls can vary by more than the allowed tolerance.

I think we can fix this if we check the cache hit counter directly instead of relying on timing. Something like:

hits_before = func.cache_info.hits
timeit_(sigma8=s8_arr[2])
assert func.cache_info.hits == hits_before + 1

Happy to include it here or leave it for a separate PR... whatever you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants