Add pytest test suite (56 tests) for all spectrum components

This commit is contained in:
2026-04-26 23:45:53 +03:00
parent 8dac095d33
commit bce6e63663
10 changed files with 1046 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
# tests/test_fermi.py — FermiFunction component unit tests
"""
Why test this?
--------------
The Fermi function F₀(Z,W) is the most critical correction for heavy nuclei.
At low Z it's ~1, but at high Z (e.g., Z=92 uranium) it can reach 10⁴+ near
threshold because the Coulomb attraction pulls electrons toward the nucleus.
We test:
1. **Z=1 sanity**: For hydrogen, F₀ ≈ 1 (no Coulomb distortion). This is a
well-known boundary condition — if Z=1 doesn't give ~1, something fundamental
is broken in the loggamma computation.
2. **Monotonicity with energy**: For β⁻ decay (Z_daughter > 0), F₀ decreases
with increasing W because p grows and η = αZW/p shrinks. This is a physical
expectation: Coulomb effects are strongest at low momentum.
3. **Z-scaling**: Higher Z → larger correction. F₀(Z=50) should >> F₀(Z=1).
4. **Numerical stability**: The loggamma-based implementation avoids overflow
that would occur with naive exp() of large arguments. We test this at high Z.
Common practice: When testing a function with special mathematical functions
(loggamma, spence, etc.), always include a "sanity check" against a known case
(Z=1) to catch implementation bugs early.
"""
import numpy as np
import pytest
from beta_spectrum.components.fermi import FermiFunction
class TestFermiZeros:
"""Test Z=1 (hydrogen) boundary condition."""
def test_Z_equals_one_gives_near_unity(self):
"""For Z=1, F₀ ≈ 1 at all energies because αZ is tiny.
This is the most important sanity check. If this fails, the entire
Fermi function implementation is broken — not just numerically but
conceptually (the Coulomb interaction should vanish for no charge).
"""
ff = FermiFunction(Z=1, A=1)
W_test = np.array([1.5, 2.0, 3.0, 5.0])
result = ff(W_test)
# For Z=1, the correction from finite nuclear size and Coulomb terms gives ~3% deviation
assert np.all(np.abs(result - 1.0) < 0.05), (
f"Z=1 Fermi function should be ≈1 but got {result}"
)
class TestFermiPhysicalBehavior:
"""Test expected physical behavior of the Fermi function."""
def test_decreases_with_energy(self, W_mid):
"""For β⁻ decay (positive Z_daughter), F₀ decreases as W increases.
Reason: η = αZW/p. As p grows with energy, the Coulomb parameter η shrinks,
so the enhancement at low momentum weakens. This is universal for all Z > 0.
"""
ff = FermiFunction(Z=20, A=40)
result = ff(W_mid)
# Check monotonic decrease (each successive point should be smaller)
diffs = np.diff(result)
assert np.all(diffs < 0), f"Fermi function must decrease with W; got diffs {diffs}"
def test_increases_with_Z(self):
"""Higher Z → stronger Coulomb attraction → larger F₀ at low energy.
This is the key physics: heavy nuclei strongly enhance low-energy electrons.
We compare at a fixed mid-range energy (W=2) across three Z values.
"""
ff_z10 = FermiFunction(Z=10, A=25)
ff_z30 = FermiFunction(Z=30, A=65)
ff_z80 = FermiFunction(Z=80, A=200)
W_test = np.array([2.0])
f_10 = ff_z10(W_test)[0]
f_30 = ff_z30(W_test)[0]
f_80 = ff_z80(W_test)[0]
assert f_80 > f_30 > f_10, (
f"Fermi function must increase with Z: {f_10} < {f_30} < {f_80}"
)
def test_positive_values(self):
"""F₀ must be positive everywhere — it's a probability enhancement factor."""
ff = FermiFunction(Z=92, A=238) # Uranium — extreme case
W_test = np.linspace(1.01, 5.0, 20)
result = ff(W_test)
assert np.all(result > 0), "Fermi function must be positive for all Z and W"
class TestFermiNumericalStability:
"""Test that the loggamma implementation doesn't overflow or produce NaN."""
def test_no_nan_at_high_Z(self):
"""For very heavy nuclei, naive computation of |Γ(γ+iη)|² would overflow.
The loggamma trick is essential here — we verify it produces valid output.
"""
ff = FermiFunction(Z=92, A=238) # Uranium-238
W_test = np.linspace(1.05, 6.0, 50)
result = ff(W_test)
assert not np.any(np.isnan(result)), "No NaN values allowed at high Z"
assert not np.any(np.isinf(result)), "No infinite values allowed at high Z"
def test_no_nan_at_threshold(self):
"""At W→1, p→0 and η→∞. The implementation must handle this limit."""
ff = FermiFunction(Z=50, A=120) # Tin — moderate heavy
result = ff(np.array([1.001]))
assert not np.isnan(result[0]), "Must handle threshold (W→1) without NaN"
assert result[0] > 0, "Threshold value must be positive and finite"
class TestFermiOutputShape:
"""Test that output shape matches input — a basic correctness check."""
def test_output_shape_preserved(self):
ff = FermiFunction(Z=20, A=40)
for n_points in [1, 5, 100]:
W_test = np.linspace(1.1, 3.0, n_points)
result = ff(W_test)
assert result.shape == W_test.shape, (
f"Shape mismatch: input {W_test.shape} → output {result.shape}"
)