Skip to content

feat(spectra): port the tabulated photon spectra to rust - #66

Open
LoganAMorrison wants to merge 1 commit into
masterfrom
claude/cython-to-rust/task-4.2-photon-table-family-kaon-eta-omega-eta-phi
Open

feat(spectra): port the tabulated photon spectra to rust#66
LoganAMorrison wants to merge 1 commit into
masterfrom
claude/cython-to-rust/task-4.2-photon-table-family-kaon-eta-omega-eta-phi

Conversation

@LoganAMorrison

Copy link
Copy Markdown
Owner

Summary

  • Ports the five tabulated photon extensions — _kaon, _eta, _eta_prime, _omega, _phi, seven public entry points between them — to one Rust module, kernels::photon_tables: a single dnde over seven Spectrum values, each holding an include_str!-embedded CSV parsed lazily on first use, a parent mass, and a list of (line energy, weight) pairs. The five .pyx differed only in that data. The Cython and its headers/stubs are deleted (16 files, 1,037 lines, of which 204 were commented-out dead code), along with the now-callerless hazma/spectra/_photon/path.py. Import-time file I/O is gone — the Cython paid seven np.loadtxt calls on import hazma.spectra whether or not a spectrum was ever evaluated. The CSVs stay in-repo as the source of truth; retiring the hazma.spectra._photon.data package-data glob is Phase 07's, and only half-true until then, since the sdist must still carry them for include_str! to find at build time.
  • No public value moves. Two independent measurements: against the Cython twins before deleting them, all seven entry points × six parent energies × 8,000 photon energies each — 336,000 points, 0 bitwise mismatches; and against a built origin/master, the whole public surface (12 dnde_photon_*, 2 dnde_positron_*, 2 dnde_neutrino_*, plus both models' spectra(), positron_spectra(), annihilation_cross_sections() and thermal_cross_section()) — 97 arrays / 18,694 values, bit-for-bit identical. One declared behavior change, at NaN inputs only: a NaN photon energy returned IndexError and now returns NaN (the Cython reached np.flatnonzero(lb <= x)[0] on an empty match; reproducing that inside dispatch::map_unary, which has no per-element error channel, would mean a PanicException where Python can catch an error today), and a NaN parent energy moves AssertionErrorValueError under rules.md rule 9. No finite input moves and the corpus samples no NaN abscissa.
  • Two live defects in hazma 2.1.0 surfaced and reproduced, not repaired (rules.md rule 1 — the corpus pins the shipped values, so a fix here would fail the gate governing every remaining swap). Writing five near-copies as one parameterised implementation is what exposed them, by putting the five line-weight expressions in one column. _eta_prime.pyx:107 weights its η′ → γγ line with BR where its four two-photon siblings use 2·BR, so the mode carries 0.02307 photons per decay instead of 0.04614 — 0.63% of the η′ yield, missing, all at M_η′/2 = 478.89 MeV. _phi.pyx:111,113 place both photon lines at the daughter meson's energy rather than the photon's: 656.94 MeV where 362.52 is right (φ → ηγ, ×1.81) and 959.65 where 59.82 is right (φ → η′γ, ×16.0 — 94% of the φ's rest mass in one photon). Filed as eta-prime-two-photon-line-missing-factor-two.md and phi-photon-lines-use-the-daughter-meson-energy.md, both blocked behind Phase 06 Task 6.4 — four defects now share one eventual corpus regeneration.
  • Repairs two things the deletion stranded, plus drive-by docstring fixes in the wrapper this PR already edits: test/test_core_{boost,interp}.py built their seven-table fixtures from the deleted modules' import-time globals and failed at collection; two test/parity/test_parity.py meta-tests monkeypatched hazma._core.photon with a one-kernel fake that now replaced seven real kernels instead of adding one. The wrapper's 11 pre-existing ruff D205/D412/D400 findings are fixed (same 11 on origin/master — zero delta, but preflight fails on the count), three docstrings named a photon_energies argument the signature spells photon_energy, the short- and long-kaon blocks both called their argument the "Charged kaon energy", and all seven now state their units.
  • boost::boost_integrate_linear_interp is now total — a NaN window returns NaN instead of panicking at an .expect (reachable through the hazma._core.boost probe since Task 3.4) — and boost::pairwise_sum is pub(crate), reused for the CSV column sums because numpy.sum(axis=0) is pairwise above eight terms and the φ table has ten.

Project

projects/cython-to-rust/ — Task 4.2: Photon table family (_kaon, _eta, _omega, _eta_prime, _phi).

See projects/cython-to-rust/task-notes/phase-04/task-4.2-photon-table-family.md for implementation detail, decisions, the numerical-impact measurements, and the stale-state sweep. The phase README asked this task to resolve or waive parity-corpus-pins-ill-conditioned-points.md first: waived on evidence — the port is bit-equal at every sampled point, so spectra.photon.eta[boosted_strong] had nothing for a conditioning budget to absorb. That refutes the follow-up's prediction for this family; its remaining five blocks are all scalar cross sections, so it now gates Phase 05 rather than the rest of Phase 04.

Test plan

  • scripts/agents/preflight.sh --paths <9 changed Python files> --md <8 changed markdown files>RESULT: PASS, every gate green (black, isort, ruff, the three cargo gates, pytest, import smoke, markdownlint, forbidden tokens).
  • Full suite: pytest -q1628 passed, 15 skipped, 5 warnings in 604.26s (0:10:04). Collection goes 1458 → 1643 against origin/master (pytest --collect-only -q on both trees) — +185, every one of them test/test_core_photon_tables.py, so no existing module gained or lost a test.
  • Parity corpus: pytest -q test/parity629 passed, 1 skipped, all seven tabulated cases green at their TABULATED (1e-12) budget. python test/parity/generate.py --checkcorpus OK: 41 cases / 1580 arrays match the manifest. The stored corpus is untouched.
  • Rust: cargo test --manifest-path rust/Cargo.toml --no-default-featurestest result: ok. 96 passed; 0 failed, 15 of them kernels::photon_tables::tests (folded constants against the disassembled immediates, parsed tables against NumPy's bit patterns, row counts, the three branch boundaries, the NaN guards, the tails, each line's boosted plateau, and the two reproduced defects).
  • New module: pytest -q test/test_core_photon_tables.py184 passed, 1 skipped (the skip is the charged kaon in the per-line photon-count test; it has no monochromatic line).
  • Model layer: pytest -q test/test_theory_aggregation.py — the identity gate Task 1.4 built for exactly this risk, that a swap repoints a kernel correctly but drops a branching-fraction weight.
  • Mutation checks (the new tests are not vacuous), each rebuilt and re-run: replacing boost::pairwise_sum with a sequential fold fails 6 tests, every one of them φ and none on the other six tables — the predicted signature, since only the φ table has enough mode columns to reach NumPy's pairwise path; replacing delta.mul_add(weight, result) with the unfused form fails 15 tests across all six spectra that carry a line.

🤖 Generated with Claude Code

The five tabulated photon extensions -- _kaon, _eta, _eta_prime, _omega
and _phi, seven public entry points between them -- become one Rust
module, kernels::photon_tables: a single `dnde` over seven `Spectrum`
values, each holding an include_str!-embedded CSV parsed lazily on first
use, a parent mass, and a list of (line energy, weight) pairs. The five
.pyx differed only in that data. Import-time file I/O is gone: the Cython
paid seven np.loadtxt calls on `import hazma.spectra` whether or not a
spectrum was evaluated.

No public value moves. Before the twins were deleted the port was
compared against them over 336,000 points (7 entry points x 6 parent
energies x 8,000 energies) with zero bitwise mismatches; against a built
origin/master the whole public surface is 97 arrays / 18,694 values
bit-for-bit identical. One declared behavior change, at NaN inputs only:
a NaN photon energy returned IndexError and now returns NaN -- the
Cython reached np.flatnonzero(lb <= x)[0] on an empty match, and
reproducing that inside an element-wise map would mean a PanicException
rather than a catchable error -- and a NaN parent energy moves
AssertionError to ValueError under rules.md rule 9.

Two things had to be reproduced rather than written correctly, because
the parity corpus pins them (rules.md rule 1). Both are live defects in
hazma 2.1.0 that writing five near-copies as one parameterised
implementation is what exposed, and both are filed and blocked behind
Phase 06 Task 6.4:

  * _eta_prime.pyx:107 weighted its eta' -> gamma gamma line with BR
    where its four two-photon siblings use 2*BR, so the mode carries
    0.02307 photons per decay instead of 0.04614 -- 0.63% of the eta'
    yield, missing;
  * _phi.pyx:111,113 placed both photon lines at the daughter meson's
    energy: 656.94 MeV where 362.52 is right, and 959.65 where 59.82 is
    right, a factor of 16.

Also repairs two things the deletion stranded: test_core_{boost,interp}
built their table fixtures from the deleted modules' globals and failed
at collection, and two parity meta-tests monkeypatched hazma._core.photon
with a fake that now replaced seven real kernels instead of adding one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant