Add pytest test suite (56 tests) for all spectrum components
This commit is contained in:
@@ -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}"
|
||||
)
|
||||
Reference in New Issue
Block a user