Skip to content

B modes#36

Draft
cgiovanetti wants to merge 29 commits into
mainfrom
bmodes
Draft

B modes#36
cgiovanetti wants to merge 29 commits into
mainfrom
bmodes

Conversation

@cgiovanetti

Copy link
Copy Markdown
Collaborator

Adds B modes, both tensor perturbations and the lensing spectrum. Folds in the curvature changes from #35, so that one will need to be reviewed first.

I found the slowdown from computing bessel functions was not constant across hardware and could be as large as 0.6s which I didn't think was tolerable, so I also added the tables back in as an option for users who do not need B modes and/or curvature. Will be updated in #35 as well.

Cara Giovanetti and others added 29 commits June 12, 2026 15:52
Three research reports (ABCMB flat-geometry audit, CLASS curvature
equations, CLASS hyperspherical/transfer machinery) plus the condensed
design doc for the Omega_k implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s_l hierarchy factors, curved Einstein constraints and ICs

- main.py: omega_k (=Omega_k h^2) default + derived K = -omega_k H100^2;
  closure omega_Lambda subtracts omega_k; expected_keys updated.
- species.py: Curvature(BackgroundFluid) pseudo-fluid (rho ~ a^-2, w=-1/3)
  makes H/aH_prime automatically curved; curvature_s_l() free-streaming
  coefficients; CLASS-verbatim (arXiv:1305.3261) s_l factors in the photon
  T/pol, massless-nu and massive-nu hierarchies; cot_K l_max truncations;
  s2^2 = 1-3K/k^2 factors in all adiabatic ICs.
- perturbations.py: (k^2-3K) Einstein constraints + K h'/2 in eta'
  (both get_derivatives and make_output_table copies); s2^2 in eta_ini.
- background.py: sqrt(1-K rs^2) in the rs ODE; rA_rec = sin_K(chi_rec).
- ABCMBTools.py: sin_K/cot_K helpers via entire functions of K chi^2
  (series at small argument: smooth through K=0, AD-safe branches).
- model_specs.py: Curvature registered in LCDM species; static
  curvature/omega_k_ref specs; curved k_min for the perturbation grid
  (closed: sqrt(8K) nu=3 cutoff; open: sqrt(|K|)); |K| in scale2.

All value-level changes reduce exactly to the flat equations at K = 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + curved Limber lensing

- spectrum.py: _transfer_sources extracted (shared flat/curved; s_2 factor in
  the polarization weight, = 1 exactly at K=0); Cl_one_ell consumes prebuilt
  sources; new _Cl_all_ells_curved generates Phi_l^nu(chi) by the exact
  dimensionful forward recurrence (seeds Phi_0, Phi_1 in closed form; smooth
  through K=0) in one chunked, checkpointed lax.scan over ALL integer ells,
  contracting against the LOS sources at each l — radial functions
  T0/T1/T2/E per CLASS transfer_radial_function; evanescent region clamped
  and masked at the same |Phi|~1e-10 threshold as the flat tables (cutoff
  variable q*S_K(chi), thresholds reused from the tables' lower edges);
  closed-universe l>=nu termination and open supercurvature q^2<=0 masks.
  get_Cl: static specs['curvature'] switch (False = existing flat tables).
- lensing_Cl: curved-sky Limber (q=(l+1/2)/S_K, sin_K window, 1/(S_K^2 k^3)
  measure, WKB amplitude (1-K l^2/q^2)^(-1/2)) — reduces exactly to the
  prior flat integrand at K=0. lensing_power_spectrum: curved Poisson
  k^3/(k^2-3K)^2 and Omega_k(1+z)^2 in Om(z).
- species.py truncations use the dimensionless g = tau*cot_K(tau) factor so
  the flat path keeps the original floating-point evaluation order.
- main.py: pass specs['curvature'] into SpectrumSolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n region)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e edges

The flat Bessel tables start at x = 0 for l <= 17, so interpolating their
lower edges left the low-l mask wide open and clipped deep-evanescent
recurrence garbage (|Phi| ~ 1e10) entered the transfer integral (smoke test:
ClTT ~ 1e13 vs 1e-12). Use CLASS's hyperspherical_get_xmin_from_approx
closed form for every l; it matches the table edges where they exist
(l=5000: 4899 vs 4895).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…teger-nu lattice

For closed references the angular diameter distance shrinks, so k_max is
divided by the fiducial angular rescaling S_K(chi*)/chi* (CLASS does the
same); the transfer grid's low end is resampled to the exact Delta nu = 1
integer lattice up to nu = 512 (covers the sharp ell ~ nu C_l onsets and
makes the k-trapezoid the exact discrete-mode sum there), then the flat-built
grid snapped to distinct integers. Also: Clpp comparison added to the
vs-CLASS script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r grid

(1) At exact integer nu the recurrence coefficient q^2-K(l+1)^2 is
analytically zero but numerically O(eps q^2) of either sign; a positive
sliver divided the advance by ~1e-11 and produced unmasked 1e10 garbage at
ell = nu (the -0.05 unlensed control blew up 1e7 at ell 24-54). Threshold at
1e-6 q^2 (legitimate minimum is 2 q^2/nu).
(2) Open universes: walk the transfer grid in q (the oscillation variable)
and map to k = sqrt(q^2-K), sampling the q->0 region the flat k-walk missed
(CLASS densifies low-q for open; EE ell~3 was 3.5e-3 off).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nsition style)

The abrupt Delta nu = 1 -> ~7 jump at nu = 512 left a trapezoid boundary
artifact in the oscillatory k-integrand, degrading C_l for ell whose support
crosses the junction (-0.01: 2e-2 at ell~137; -0.05: 6.5e-2 at ell~237 —
both map to nu just below the junction). Ramp the integer step linearly
over 250 points instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verse crash)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New abcmb/tensors.py isolates all tensor-sector code:
- TensorPerturbationEvolver: GW amplitude h + photon tensor
  temperature/polarization hierarchies + massless-nu tensor hierarchy
  (synchronous gauge, CLASS conventions, tensor_method=massless_approx).
- TensorSpectrumSolver: flat-space T/E/B radial functions integrated
  against the tensor sources; reuses the existing phi0/phi1/phi2 Bessel
  tables (j'' = (2 phi2 - phi0)/3), no new tables.
- get_tensor_k_axes: scalar k grids truncated at the tensor k_max.
Custom species opt into the GW source via a duck-typed tensor_rho_rel
hook; species.py is untouched.

Minimal touches to existing files (default scalar path unchanged except
Output now carries ClBB):
- spectrum.py: lensed_Cls does EE/BB mixing (xi+- from EE+-BB), get_Cl
  returns a 4-tuple and takes optional tensor_cls.
- main.py: Model gains TPE/TSS (None when tensors=False), pipeline
  wiring, Output.ClBB, and r/n_t derived params (n_t default = CLASS scc).
- model_specs.py: tensor spec defaults, incl. tensor-specific solver
  tolerances rtol_ten=1e-5/atol_ten=1e-9 (scalar large-k rtol 1e-4
  biased tensor BB low ~0.7% at l~450).

Validation (vs classy 3.3.4, fiducial LCDM + r=0.1): at shared computed
ell-nodes ABCMB matches converged CLASS sub-permille; scalar
accuracy_test.py still passes; lensed TT/EE/BB within 1%. One open item
(raw-BB ell-spline residual ~7e-3 at l=477, between sparse tensor nodes)
documented in HANDOFF_bmodes.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… test

The raw-BB test's ell=477 residual (6.9e-3 vs the 2.5e-3 bar) is CubicSpline
interpolation error over the 40-wide tensor node gap (450->490), not transfer
physics. Proven by a CLASS-internal self-interpolation test: re-splining
CLASS's own node values through the identical interpax.CubicSpline reproduces
~7e-3 at ell=477 with zero error at the nodes. Nodes are already maximally
dense (bessel_l_tab; the phi-tables exist only there), so tightening would
need new Bessel tables.

test_bb_raw restructured to assert honestly (full BB suite passes on GPU):
- recomb-tail nodes (l>=100): 2.5e-3  (clean transfer accuracy; max 1.6e-3)
- low-l nodes (3<=l<100):     5.0e-3  (known structural diff below; max 4.2e-3)
- full dense band 3<=l<=490:  1.0e-2  (interp-limited, the scalar floor; 6.9e-3)

Restructuring surfaced a new, converged ~0.4% low-l raw-BB excess vs CLASS
(peak 4.2e-3 at l=10, decaying to the recomb floor). Ruled out: solver tol
(ladder gives |A/C-1|=1.4e-5, default rtol_ten already converged),
interpolation (it is at nodes), tensor_method (CLASS default IS
massless_approximation), n_t (identical formula). A structural ABCMB-vs-CLASS
difference in the reion-bump / large-scale tensor source; deferred to a
follow-up session. Full writeup + remaining suspects + ready diagnostics in
NOTE_lowl_bb_excess.md.

diag_bb_dense_hp.py rewritten to a fast (default-precision + l_linstep=5)
self-interpolation test. New diagnostics: diag_bb_lowl_nodes.py,
diag_bb_tol_ladder.py, diag_bb_lowl_3way.py, run_bb_raw_check.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trum path

get_Cl now always uses _Cl_all_ells_curved (smooth through K=0 -> j_l), for flat and curved cosmologies alike. Removes the tabulated flat path entirely: the 79 MB bessel_tab/*.txt, the sparse-ell CubicSpline reconstruction, the Q/J/j large-x asymptotic, the module-level phi0/phi1/phi2, the Cl_one_ell method, the ells_indices/lensing_ells_indices fields, and the scipy.special.spherical_jn import (~256 lines). Drops bessel_tab from setup.cfg package_data.

This eliminates the hard l=5000 cap (raise l_top, no re-tabulation) and the flat path's one genuine approximation (the spline); the recurrence matches scipy.special.spherical_jn to ~1e-14 at K=0, so it is the more correct generator. The 'curvature' flag now only gates k-grid construction, not spectrum-path selection.

Validated (1xA100): forward finite + sane for lensing off and on (warm 9.96/10.69 s, a wash); K=0 reverse-AD final gradients finite; accuracy_test PASSED vs CLASS at the 1% gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…drop dead code

Remove the tracked notes_curvature/ directory (test scripts, design .md notes, changelog drafts; ~3700 lines, not production code). Trim verbose inline comments (ABCMBTools header, the recurrence body in spectrum.py, the CLASS-internals blocks in model_specs.py, the repeated s_l hierarchy comments in species.py) while keeping the NumPy-structured docstrings with the project's Parameters:/Returns: colon convention for autodoc.

Drop dead code: model_specs angular_rescaling_ref was set but never read, and the angular rescaling is now computed only in the closed branch where it is used.

Behavior unchanged: flat/open k-grids bit-identical, closed algebraically identical. Validated flat+open+closed forwards finite and accuracy_test PASSED (vs CLASS, 1% gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-caused ~half the low-l raw tensor-BB excess vs CLASS to the
TensorPerturbationEvolver start-time cap: lna_start was capped at -10
(z~22000), too late for the photon polarization quadrupole to settle
before recombination, inflating low-l BB by ~2e-3. Cap -10 -> -14
(BB is start-converged for t0<=-13). Improves every node l=2-450 at
zero wall-clock cost; low-l node max 4.2e-3 -> 3.1e-3, recomb max
1.6e-3 -> 0.85e-3 vs high-precision HYREC-2 CLASS. accuracy_test_bb passes.

Tightened test node bars (recomb 2.5e-3 -> 1.5e-3, low-l 5e-3 -> 4e-3)
and updated comments. Remaining residual (l=2-4 reion region + a
converged ~1e-3 recomb Pi difference) is within inter-code scatter for
raw tensor BB; full eliminations + diagnostics in NOTE_lowl_bb_excess.md
and the diag_*.py scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…to kill the l=477 interp residual

The l=477 raw-BB residual is sparse-l-node CubicSpline interpolation. Branch
worktree-curvature already replaced tables+spline with an exact hyperspherical
recurrence (every l computed) for the scalar path. Added a scoping TASK to
assess porting it into TensorSpectrumSolver: payoff = remove the l=477 residual
+ drop the tensor table dependence; cost = the tensor solver imports the
phi-tables the curvature branch deleted, and both feature branches rewrite
spectrum.py (merge effort to assess).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… Bessel recurrence

Brings the curvature branch's hyperspherical-Bessel recurrence into the
B-modes branch and re-expresses the tensor (primordial-GW) spectrum on it,
eliminating the l=477 CubicSpline interpolation residual and the tensor
solver's dependence on the bessel_tab/*.txt tables.

origin/curvature (== worktree-curvature @ 3174b25) merged wholesale: curved
background, species.Curvature, curved k-grids, ABCMBTools curved helpers, and
the 79 MB Bessel-table deletion all applied clean. Conflicts only in:
- spectrum.py: kept curvature's recurrence get_Cl, grafted the bmodes
  tensor_cls add-in and the 4-tuple EE<->BB lensed_Cls.
- main.py: unioned expected_keys (omega_k,K + r,n_t).

tensors.py TensorSpectrumSolver ported to the recurrence (_tensor_sources +
_Cl_all_ells_tensor): every integer ell 2..l_tensor_max computed exactly via
the flat (K=0) spherical-Bessel recurrence, forming the tensor T/E/B radials
from Phi_l, dPhi/k, d2Phi/k^2. Dropped the phi-table imports and the sparse-ell
CubicSpline. Curved geometry + tensors is guarded as unsupported (the tensor
radial basis is flat-only).

Validation (1xA100, LCDM + r=0.1):
- accuracy_test_bb.py PASS: raw BB at CLASS nodes 8.9e-4 (recomb) / 3.1e-3
  (low-l), sliver 491-500 8.8e-4 (was 1.4e-2); lensed TT/EE/BB <=1%. The
  l=477 residual is gone (the ~5e-3 between CLASS nodes is CLASS's own
  interpolation; node-level 8.9e-4 vs every-l 5.1e-3).
- scalar accuracy_test.py PASS (merged scalar path intact).
- reverse-AD tensors=True finite on both lensing flags.
- Forward ClBB^2 and reverse grads match the old table path to ~1e-6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dual-path plan

The curvature merge made the every-ell recurrence the scalar get_Cl for
everyone -> +0.64s (lensing=False) / +0.78s (lensing=True) on the default
tensors=False forward vs the old table+spline. Confirmed intrinsic to the
recurrence (sequential every-ell scan walk), not a bmodes bug: the pure
curvature A/B (3174b25 vs base 5eabbab) shows the same +0.64s, so the
curvature branch carries it too. Agreed fix (not yet implemented): dual
scalar path gated on self.curvature (flat->table+spline fast path,
curved->recurrence; tensor sector untouched so l=477 stays fixed). Full
writeup in HANDOFF_bmodes.md + proposed_diff_dual_scalar_path.md.

Adds timing tools prof_default_path.py / prof_scalar.py and a self-contained
diag_bb_recurrence_check.py (proves the ~5e-3 mid-node BB residual is CLASS's
own interpolation, node-level 8.9e-4 vs every-l 5.1e-3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…flag

The curvature merge made the every-ell hyperspherical-Bessel recurrence the
sole scalar get_Cl, adding a node-dependent forward cost (up to +0.64s/+0.78s
on slow nodes, ~0.1s on fast ones) to ALL forward calls incl. tensor/B-mode
and OLE runs. Gate the scalar get_Cl on the static self.curvature flag:
flat (curvature=False, the common path) -> restored tabulated-Bessel Cl_one_ell
+ sparse-ell CubicSpline (origin/main's long-validated path); curved
(omega_k != 0) -> the recurrence (required for curved radials). The tensor
sector (tensors.py) is unchanged, so the ell=477 fix is preserved.

Restored from 5eabbab: bessel_tab/*.txt, module-level table loaders + j/Q/J
lambdas, ells_indices/lensing_ells_indices fields, Cl_one_ell; setup.cfg
re-adds bessel_tab/*.txt to package_data.

Validated (1xA100, nid001205): flat-table vs curved-K0 agree to the scalar
spline floor; dual-path strictly faster than recurrence same-node (9.66 vs
9.78 / 10.39 vs 10.48 s); accuracy_test PASS, accuracy_test_bb PASS (raw BB
nodes 8.9e-4/3.1e-3 unchanged), reverse-AD finite both lensings (tensors=True).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cgiovanetti cgiovanetti linked an issue Jun 18, 2026 that may be closed by this pull request
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.

Feature request: Support for B-modes and tensor-to-scalar ratio

1 participant