feat(utils): port the scipy special functions to rust - #59
Merged
LoganAMorrison merged 2 commits intoAug 10, 2026
Merged
Conversation
Adds rust/src/special.rs: spence, bessel_k1 and bessel_kn over the
cephes-lineage spec_math crate, PyO3-free per rules.md rule 8, plus
rust/src/special_probe.rs registering hazma._core.special so the scipy
comparison can run from Python. These are the three functions the
Cython layer cimports from scipy.special.cython_special, and the reason
pyproject.toml pins scipy>=1.13 at build time.
kn is deliberately not spec_math's cephes kn. scipy.special.kn
dispatches integer orders to kv; only k0/k1 are still cephes there, so
the faithful cephes routine misses scipy by up to 5.055e-9 relative
over x in [1e-8, 300] — four orders past this task's 1e-13 gate, and
inside the parity corpus's 1e-8 budget for thermal_cross_section, whose
prefactor squares the value. K_n is built from the upward recurrence
K_{m+1} = K_{m-1} + (2m/x) K_m on cephes k0/k1 seeds instead, tracking
scipy to 3.4e-15 across orders 0-5. ADR-0002's vendoring fallback would
have reproduced the miss rather than fixing it, so the phase file and
references/numerics-replacements.md are patched to say so.
The probe otherwise reads as three ported kernels to the parity
corpus's served-kernel predicate, which would have dropped the corpus
out of bit-equality mode for the rest of the port with nothing turning
red. cases._CORE_TEST_ONLY_MODULES exempts the submodule, and
test_test_only_core_submodules_have_no_importer makes that exemption
conditional on nothing under hazma/ importing it.
No public value changes: the only diff under hazma/ is a comment in the
non-executable _core.pyi stub, and the bare suite ran the parity corpus
in bit-equality mode across all 41 consumed entry points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bessel_kn(n, -0.0) returned NaN for every order from 2 up, where scipy returns +inf. IEEE routes -0.0 to the zero branch (-0.0 < 0.0 is false), so cephes k0/k1 hand the recurrence +inf seeds while its 2m/x term is -inf, and inf + -inf is NaN. Orders 0 and 1 return the seeds directly and were always right, which is why an edge test covering +0.0 and -1.0 did not catch it. bessel_kn now short-circuits x == 0.0 to +inf. Swept as a class rather than patched at the reported input: both signs of zero were re-measured against scipy for all three functions and for kn at orders 0-5, which is what establishes that spence, bessel_k1 and the kn seed arms were never affected. Reverting the guard alone fails one cargo test and six pytest cases, and leaves orders 0-1 passing. Also corrects the test counts in the task note, both working-memory READMEs and (on push) the PR body: they said "53 tests in 7 classes" and "15 passed (7 new)" against 53 in 8 classes and 8 new Rust tests. Every count is now taken from a command and the command is quoted beside it. Post-fix: 65 tests in 9 classes, 16 cargo tests, and a bare suite of 1154 passed / 13 skipped with the parity corpus still in bit-equality mode. Two lessons ledger entries: PR #59 added to [derived-count-not-rederived], and a new [signed-zero-lost-by-a-derived-formula]. No hazma code path reaches the fixed input — the crate is still unreferenced by any wrapper, and the only diff under hazma/ remains a comment in the non-executable _core.pyi stub. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LoganAMorrison
deleted the
claude/cython-to-rust/task-3.2-special-functions
branch
August 10, 2026 03:17
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
rust/src/special.rs—spence,bessel_k1,bessel_knover the cephes-lineagespec_mathcrate, PyO3-free perrules.mdrule 8 — plusrust/src/special_probe.rs, which registershazma._core.specialpurely so the scipy comparison can run from Python. These are the three functions the Cython layer cimports fromscipy.special.cython_special, and the reasonpyproject.tomlpinsscipy>=1.13at build time.knis deliberately notspec_math's cepheskn.scipy.special.kndispatches integer orders tokv; onlyk0/k1are still cephes there, so the faithful cephes routine misses scipy by up to 5.055e-9 relative overx ∈ [1e-8, 300](atx = 9.531, just below its ownx = 9.55branch switch). That is four orders past this task's 1e-13 gate, and it would land inside the parity corpus's 1e-8 budget forthermal_cross_section, whose prefactor squares the value — a Phase 05 swap could have shipped it inside budget and moved published numbers.Kₙis built from the upward recurrenceK_{m+1} = K_{m-1} + (2m/x)·K_mon cephesk0/k1seeds instead, tracking scipy to ≤ 3.4e-15 across orders 0–5. ADR-0002's vendoring fallback would have reproduced the miss rather than fixed it, so the phase file andreferences/numerics-replacements.mdare patched to record that its "scipy'sspence,k1,knare themselves cephes wrappers" is false forkn.cases.rust_core_kernels()counts every public callable on the extension, so registering the submodule flipped the corpus toexact=False, detail='hazma._core serves 3 kernel(s)'for the rest of the port with nothing turning red (docs/agents/lessons.md,[gate-disabled-stays-green], second instance in this project).cases._CORE_TEST_ONLY_MODULESexempts the submodule — not the names, which would also cover a future real kernel — andtest_test_only_core_submodules_have_no_importermakes the exemption conditional on nothing underhazma/importing it.hazma/is a comment block in the non-executablehazma/_core.pyistub (+8/−0).spenceandk1agree with scipy to 2.425e-15 and 1.215e-15;kn(2, ·)to 9.786e-16 over hazma's live domain. One declared divergence, outside anything hazma reaches: abovex ≈ 698scipy'sknflushes to zero whileK₂is still3.9e-305, and the recurrence keeps returning values tox ≈ 742;thermal_cross_sectionshort-circuits abovex = 300, and the boundary is pinned rather than only documented.Review round 1 (2026-08-10)
bessel_kn(n, -0.0)returnednanfor every order ≥ 2 where scipy returns+inf.-0.0 < 0.0is false, so IEEE routes negative zero to the zero branch — cephesk0/k1hand the recurrence+infseeds while its2m/xterm is-inf, andinf + -infisnan. Orders 0 and 1 return the seeds directly and were always right, which is why an edge test covering+0.0and-1.0missed it. Fixed with anx == 0.0short-circuit, and swept as a class: both signs of zero re-measured against scipy for all three functions at orders 0–5, establishing thatspence,bessel_k1and theknseed arms were never affected. No hazma code path reaches the input — the crate is still unreferenced by any wrapper.[derived-count-not-rederived], plus a new[signed-zero-lost-by-a-derived-formula].1141 passed, 14 skippedagainst this PR's1142 / 13is the parity corpus's mode signal, not a discrepancy — their environment resolved NumPy 2.5.2 where the manifest records 2.5.1, which drops the runner into budget mode and converts one test into a skip (1141 + 14 == 1142 + 13 == 1155).Project
projects/cython-to-rust/— Task 3.2: Special functions.See
projects/cython-to-rust/task-notes/phase-03/task-3.2-specfun.mdfor implementation detail, decisions, the per-sweep measurements, the eleven-mutation test-validity campaign, and the round-1 record.Two canonical patches, both in this PR:
phases/phase-03-numerics-foundation.md's Task 3.2 block gained three "criteria added during execution" bullets (thekndeviation, the bound on where the underflow criterion can hold, and keeping the served-kernel predicate sound), andreferences/numerics-replacements.mdgained the measured block. No ADR — nothing revises ADR-0002, since the recurrence is original work over cephes seeds.Test plan
scripts/agents/preflight.sh --paths "test/test_core_special.py test/parity/cases.py test/parity/test_parity.py hazma/_core.pyi"→ RESULT: PASS:markdownlint --dotwas run separately over the seven touched docs in their final state (clean); the ten substantive gates read onlyhazma/,rust/andtest/, which were final at that run.rtol = 0across all 41 consumed entry points, 179,695 pinned values.1154is1142 + 12(the newTestSignedZerocases) and the skip count is still 13, which is what proves the mode held across the round-1 fix. Environment is the corpus's capturing one (CPython 3.12.12, macOS/arm64,numpy==2.5.1,scipy==1.18.0), confirmed bytolerances.provenance(...)→Provenance(exact=True, detail='').pytest test/test_core_special.py -q→65 passed in 0.50s— 9 classes, counted withpytest --collect-only -q | awk -F'::' '{print $2}' | sort | uniq -c.cargo test --manifest-path rust/Cargo.toml --no-default-features→16 passed(9 new, counted withgrep -c '^ #\[test\]' rust/src/special.rs).rust/src/special.rs(wrongspenceconvention, cephesknfallback, swapped seeds, dropped order factor,k1→K₀, an extra recurrence step, unfolded negative order, wrong seed arm, and the removed signed-zero guard) and two against the corpus guard. All eleven were caught by the test whose name claims them. Two are worth naming: dropping the order factor initially passedcargo test— at n = 2 that factor is 1 — which is why the Rust Wronskian now also runs at ν = 2; and removing the signed-zero guard fails 1 cargo test and 6 pytest cases while orders 0–1 keep passing, which is the shape the diagnosis predicts.🤖 Generated with Claude Code