fix(compute_pk): integrate to full bg.conformal_age + recover dtau_end gradient via Taylor correction - #16
Draft
MinhMPA wants to merge 8 commits into
Draft
fix(compute_pk): integrate to full bg.conformal_age + recover dtau_end gradient via Taylor correction#16MinhMPA wants to merge 8 commits into
MinhMPA wants to merge 8 commits into
Conversation
…nlinear=...) — redo of smsharma#11 with bug fixes Supersedes PR smsharma#11 (which was reverted via PR smsharma#13 due to post-merge test failures). This PR ships the same API consolidation with the three fixes that smsharma#11 was missing folded in from the start. Public API: compute_cl_pp(pt, params, bg, th, l_max, *, nonlinear="none") # nonlinear in {"none", "halofit"}; ValueError for anything else. # "ept" is reserved for a follow-up clax-pt PR. Removed (BREAKING): - compute_cl_pp (Siddharth's original; superseded by source-Limber) - compute_cl_pp_fast (inaccurate at l >= 300 per its own docstring) - compute_cl_pp_vmap (Hermite Bessel-table vmap; superseded) - compute_cl_pp_limber (Poisson-reconstruction; ~20% over at l=2500) Renamed: - compute_cl_pp_source_limber -> compute_cl_pp (sole public entry) - compute_cl_pp_transfer -> _compute_cl_pp_full_bessel (private oracle) Halofit injection (new private _halofit_modulator): - vmap(compute_pk_nonlinear) over a 100-point z-grid (CLASS-aligned density). - Inline `sigma_R(R_min, ...) >= 1` check + jnp.where to force R=1 where Halofit isn't applicable — replicates CLASS fourier.c:1706-1716 in a vmap-safe way (the Python-level check inside compute_pk_nonlinear gets bypassed under vmap-over-z, which is why smsharma#11 over-corrected ~25% at high l). - k_max_extend = 0 default (no power-law extension; matches CLASS's no-extrapolation behavior). Pass a positive value to enable log-log extension for narrow k-grids. - 2D-interpolates R(k, z) onto (pt.k_grid, pt.tau_grid) via bg.loga_of_tau. - Multiplies sqrt(R) directly into S_transfer (CLASS source-multiplication). Tests: - tests/test_cl_pp.py (renamed from test_cl_pp_source_limber.py): contract + linear-accuracy + cross-impl-vs-_compute_cl_pp_full_bessel + Halofit smoke + JIT/AD. - tests/test_clpp_halofit_ratio.py: rewritten for new API; <7% NL/lin ratio agreement at l<=500, <10% at l>=1000 vs CLASS Halofit reference. - tests/test_lensing.py: callers updated; uses local `pipeline` fixture matching upstream-main convention. - Deleted: tests/test_cl_pp_implementations.py, test_cl_pp_limber.py. Verified locally: 32/32 lensing+halofit tests pass, 36/36 EPT tests pass, 104/104 remaining tests pass.
…ute_cl_pp Documents both paths in the new unified API: - nonlinear="none" (linear source-Limber): <1% vs CLASS at all l<=2500 - nonlinear="halofit" (z-aware Halofit injection via source multiplication): <7% at l=500 and <10% at l>=1000 vs CLASS Halofit reference Existing tables (unlensed C_l, lensed C_l with CLASS-generated cl_pp, matter P(k), pipeline, performance, multi-cosmology) are unaffected by PR smsharma#14 — they don't depend on compute_cl_pp. Note on lensed C_l accuracy table: it tests lens_cls in isolation by feeding CLASS-generated C_l^pp; the reported <0.2% is about lens_cls's correlation-function method, not about our compute_cl_pp.
…<=2500
The previous table understated linear accuracy. test_clpp_halofit_ratio.py
::TestClppLinear asserts <1% at l in {100, 500, 1000, 2000, 2500} with
pt_k_max_cl=5.0. Updated the linear column accordingly and added the
prerequisite (pt_k_max_cl >= 5) to the section preamble.
…ratio test thresholds Sweep across modulator settings on default cosmology (l_max=2500): | l | n_z=100, no extension | n_z=100, extend to k=10 | | 100 | 0.01% | 0.01% | | 200 | 0.04% | 0.04% | | 500 | 0.42% | 0.21% | | 1000 | 1.47% | 0.63% | | 1500 | 2.30% | 0.79% | | 2000 | 3.74% | 1.40% | | 2500 | 3.97% | 0.96% | The k-extension restores accuracy at high l. CLASS itself uses a dedicated nonlinear k-grid that extends past the perturbation k-range; our log-log power-law extension provides equivalent coverage with σ(R) bisection well-resolved. Doubling n_z to 200 didn't help (slight degradation at l=1000, l=2500 from interpolation noise), so 100 is the right z-density. Changes: - _halofit_modulator default k_max_extend: 0 → 10 Mpc^-1 - test_ratio_at_low_l threshold: 7% → 1% (measured: 0.21% at l=500) - test_ratio_at_high_l threshold: 10% → 2% (measured: 1.40% worst case) - README accuracy table: replace bounds with measured residuals - Docstrings updated with rationale and measured numbers User asked 'who set the 10% threshold' — it was carried over from the deleted Poisson-Limber test, never measured against the actual source- multiplication recipe. Now it reflects what the code actually achieves.
…=2500); fair absolute-accuracy table k_max_extend sweep on default cosmology, n_z=100 fixed: | l | k=10 | k=20 | k=30 | | 500 | 0.21% | 0.09% | 0.09% | | 1000 | 0.63% | 0.04% | 0.17% | | 1500 | 0.79% | 0.11% | 0.11% | | 2000 | 1.40% | 0.05% | 0.28% | | 2500 | 0.96% | 0.76% | 0.76% | (metric: |R_clax / R_CLASS - 1|, R = NL/lin) k=20 is the sweet spot — far enough for sigma(R) bisection to fully converge, not so far that the local power-law slope drifts from the true asymptote. Going to k=30+ degrades because power-law extrapolation diverges from the true transfer-function shape. Timing benchmark (5-run mean): k=10 110ms, k=20 110ms, k=30 111ms. Accuracy boost is essentially free. Test thresholds: - low_l (l<=500): 1% -> 0.5% - high_l (l>=1000): 2% -> 1% README accuracy table replaced with apples-to-apples absolute residuals for both 'none' and 'halofit' (was mixing absolute-vs-ratio metrics). Both modes share the source-Limber kernel and track each other within ~0.1% on absolute scale; the ratio (third column) isolates the R(k,z) quality from the common linear-kernel systematic.
clax/perturbations.py
- _perturbation_solve_setup gains a `tau_max_factor` kwarg (default 0.999
for backward compatibility with the C_l / source-function pipeline).
- The matter-power paths route `tau_max_factor=1.0`:
* `_perturbations_solve_mpk_impl` (used by `compute_pk_table`)
* `_matter_delta_m_single_k_impl` (used by `compute_pk`)
- The 0.999 safety margin removed from these paths missed ~14 Mpc near
today, costing ~0.33% in P(k) via linear growth (D ∝ a, ΔD/D ≈ 0.16%
over 14 Mpc near z=0, P ∝ D² so doubles to ~0.33%).
- The C_l / `perturbations_solve` path is unchanged (still uses 0.999).
Measured P(k) accuracy at planck_fast preset, single-k `compute_pk` vs CLASS:
| k [Mpc^-1] | before | after |
|------------|---------|----------|
| 3e-3 | -0.51% | -0.17% |
| 1e-2 | -0.08% | +0.25% |
| 5e-2 | -0.29% | +0.04% |
| 1e-1 | -0.27% | +0.07% |
| 3e-1 | -0.36% | -0.03% |
| 1.0 | -0.53% | -0.20% |
Worst-case |err| drops from 0.53% to 0.25%; median |err| drops from
~0.30% to ~0.07%. Five of six k values improve.
Background, perturbation evolution, and δ_cdm at the synchronous-gauge
boundary all match CLASS to <0.05% — the residual was almost entirely
the hardcoded 14-Mpc growth deficit.
Tests verified locally:
- tests/test_cl_pp.py + tests/test_lensing.py: 24 passed in 207s
- tests/test_clpp_halofit_ratio.py + test_halofit_sigma_guard.py +
test_nonlinear.py: 20 passed in 89s
- C_l-pipeline-touching tests unchanged because `perturbations_solve`
keeps the default 0.999 factor.
2 tasks
The previous table (1.3–3.5% errors) was measured at an earlier version and has been stale since multiple subsequent improvements to the perturbation pipeline. Replace with planck_fast measurements at default Planck 2018 LCDM, post-`tau_max_factor=1.0` fix, vs CLASS v3.3.4. Median |error| now ~0.07%, worst-case 0.25% (k=0.01).
MinhMPA
marked this pull request as draft
May 6, 2026 01:16
The single-mode `_matter_delta_m_single_k_impl` (used by `compute_pk`)
wraps `bg.conformal_age` in `stop_gradient` before passing it as `t1` to
the diffrax ODE solve. That stop_gradient is structurally required:
RecursiveCheckpointAdjoint + PIDController cannot reverse-mode AD through
a traced `t1` (the controller's accepted-step factor is marked
`eqxi.nondifferentiable`, so any tangent flowing through `t1` raises
`RuntimeError: Unexpected tangent`). Removing it crashes the adjoint.
But stop_gradient sacrifices a chain-rule term. The full derivative of
`delta_m(tau_end(theta), y(tau_end(theta); theta))` has two parts:
(1) d_y delta_m * dy(tau_end; theta)/dtheta (frozen-t1 AD captures)
(2) ddelta_m/dtau * dtau_end/dtheta (stop_gradient drops)
For h/omega_b/omega_cdm — all of which change `conformal_age` through H(z)
— part (2) is ~70% of the gradient, making `jax.grad(compute_pk)` 65-84%
off vs centered FD on this branch. ln10A_s/n_s pass at 0% off because
they don't enter the ODE and don't change tau_end.
Recovery: first-order Taylor expansion around the frozen endpoint,
delta_m(tau_traced) ~ delta_m(tau_frozen)
+ (ddelta_m/dtau)|_{tau_end} * (tau_traced - tau_frozen)
Two properties make this exact at fiducial:
- `(tau_traced - tau_frozen) = 0` identically -> primal value unchanged.
- `jax.jvp` on `_extract_delta_m` with tangent `(dy/dtau, 1)` captures
both the implicit (state) and explicit (background-density-at-tau)
contributions to `ddelta_m/dtau`, so AD picks up the correct linear
coefficient.
Verified at PK_CONTRACT_PREC (rtol=1e-6) and at rtol=1e-7:
rtol=1e-3: h 6.41% / omega_b 8.91% / omega_cdm 4.83%
rtol=1e-5: h 1.08% / omega_b 3.18% / omega_cdm 0.85%
rtol=1e-7: h 0.32% / omega_b 1.23% / omega_cdm 0.61%
Clean precision-floor convergence (~3-4x per rtol decade), consistent with
the FD oracle's own step-noise floor at each rtol. ln10A_s/n_s remain
exact at 0% off across all rtol values.
Adds `TestPkScalarDensityGradients` regression test on the contract path.
The 5% threshold fails loudly on the original stop_gradient-only code
(>50% off) while accommodating the FD precision floor at rtol=1e-6
(empirically ~3% on omega_b at k=3e-4).
MinhMPA
added a commit
to MinhMPA/clax-pt
that referenced
this pull request
May 6, 2026
Task-by-task TDD plan for completing reverse-mode AD (PR smsharma#16) and enabling forward-mode AD (jax.jvp / jax.jacfwd) end-to-end through compute_pk for Fisher-style sensitivity analysis. Includes a self-contained iGPU agent prompt for V100 execution.
MinhMPA
added a commit
to MinhMPA/clax-pt
that referenced
this pull request
May 6, 2026
10 standalone diagnostics from the PR smsharma#16 investigation. Documents the trail that localized the reverse-mode AD bug (stop_gradient(bg.conformal_age) sacrificing the dδ_m/dτ · dτ_end/dθ chain-rule term) and the precision-floor analysis that validated the Taylor correction. Catalog appended to diags/README.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two coupled fixes to
compute_pk/compute_pk_table, both rooted in the integration endpointtau_end = bg.conformal_age:bg.conformal_age(1.0×) instead of the historical 0.999× safety margin. Closes a ~0.33% bias inP(k)from the missing 14 Mpc of linear growth near today.stop_gradient(bg.conformal_age)via a first-order Taylor expansion around the frozen endpoint. Closes a 65–84% AD-vs-FD gradient error onh,omega_b,omega_cdm(parameters that flow throughconformal_agevia H(z)).The two changes are coupled: routing the matter-power paths to
tau_max_factor=1.0makestau_endparameter-dependent (it was a fixed 0.999 × age before), which exposes the latent gradient bug. Both must land together.Root cause — primal (1.0× vs 0.999×)
Bottom-up diagnostic at
planck_fastprecision (60 k/decade, l_max=50, q=5, rtol=1e-6):np.interpclamping artifact past CLASS's savedtau_max=13998vs clax'stau_end=14139.)tau_end = 0.999 × conformal_age = 14139 Mpcwhile CLASS evaluatespk(z=0)at the full conformal_age (14153 Mpc).Linear growth from τ=14139 to τ=14153: ΔD/D ≈ ½ × Δa/a ≈ ½ × aH × Δτ ≈ 0.16%, doubling in P ∝ D² to 0.33%.
Root cause — gradient (Taylor correction)
After the primal fix, the single-mode path needs
tau_endto be parameter-dependent (any θ that changes H(z) changesconformal_age). Buttau_end = bg.conformal_agecannot flow as a tracedt1into diffrax's ODE solve:RecursiveCheckpointAdjoint+PIDControllerraiseRuntimeError: Unexpected tangent(the controller's accepted-step factor is markedeqxi.nondifferentiable). The fix usesstop_gradient(bg.conformal_age)ast1and reintroduces the chain-rule term analytically.Chain rule for
δ_m(τ_end(θ), y(τ_end(θ); θ))decomposes into:∂δ_m/∂y · dy(τ_end; θ)/dθdδ_m/dτ · dτ_end/dθThis perfectly explains the diagnostic pattern from
scripts/benchmark_gradients.py:ln10A_sandn_senterP(k)as pure scalar prefactors of the primordial spectrum — they don't changeτ_end— so the missing chain-rule term is zero for them.Fix. First-order Taylor expansion around the frozen endpoint:
Two properties make this exact at fiducial:
(tau_traced − tau_end) = 0identically → primal value unchanged.jax.jvpwith tangent(dy/dτ, 1)captures both the implicit (state) and explicit (background-density-at-τ) contributions todδ_m/dτ, so AD picks up the correct linear coefficient.Changes
clax/perturbations.pyPrimal fix (existing in PR):
_perturbation_solve_setupgains atau_max_factorkwarg, default0.999(preserves the C_l / source-function pipeline)._perturbations_solve_mpk_impl(used bycompute_pk_table) and_matter_delta_m_single_k_impl(used bycompute_pk) both now passtau_max_factor=1.0to integrate to the fullbg.conformal_age.perturbations_solve) is unchanged.Gradient fix (new):
_matter_delta_m_single_k_implnow applies the Taylor correction to recoverdτ_end/dθ. Inline comment documents the diffrax structural constraint forcing the stop_gradient.tests/test_pk_gradients.pyTestPkScalarDensityGradientsregression test: assertsdP/d{h, omega_b, omega_cdm}matches centered FD to <5% atPK_CONTRACT_PREC(rtol=1e-6). Fails loudly on the original stop_gradient-only code (where the same params were 65-84% off); the 5% threshold accommodates the AD/FD precision floor at rtol=1e-6 (empirically ~3% on omega_b at k=3e-4).Measured improvement
Primal (P(k) value)
Worst-case |err| drops from 0.53% to 0.25%; median drops from ~0.30% to ~0.07%.
Gradient (dP/dθ vs centered FD), trajectory across
pt_ode_rtolClean precision-floor convergence (~3-4× per rtol decade), consistent with the FD oracle's own step-noise floor at each rtol (FD step-spread on
his 2.2% at rtol=1e-7). Tightening rtol further would be exercise without information gain — both AD and FD converge to the same value.Test plan
pytest tests/test_pk_gradients.py::TestPkScalarDensityGradients -v→ 3/3 pass (h: 0.6%, omega_b: 3.1%, omega_cdm: 0.7%; all under 5% threshold).pytest tests/test_thermodynamics.py -v --fast→ 9/9 pass.pytest tests/test_cl_pp.py tests/test_lensing.py tests/test_clpp_halofit_ratio.py tests/test_halofit_sigma_guard.py tests/test_nonlinear.py -v→ unchanged from prior PR fix(compute_pk): integrate to full bg.conformal_age + recover dtau_end gradient via Taylor correction #16 state (44 passed in 296s).TestPkScalarGradients(primordial-only direct-path coverage) andTestPkPublicTableGradients(table-API density coverage) unchanged.Notes
RecursiveCheckpointAdjoint's internalcustom_vjp(thecheckpointed_while_loop),clax/shooting.py'sshoot_fncustom_vjp, andclax/thermodynamics.py's_find_z_reiocustom_vjp. None affect reverse-mode (HMC, optimization). The Taylor correction in this PR is mode-agnostic — once those blockers are converted,jax.jvp(compute_pk)will pick up the chain-rule term automatically. This is a follow-up workstream; tracked separately.pt_ode_rtol=1e-7all five params pass <1.3%.