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
+119
View File
@@ -0,0 +1,119 @@
# tests/test_exchange.py — ExchangeCorrection unit tests
"""
Why test this?
--------------
The atomic exchange correction X(Z,W) can be very large for heavy nuclei at low energy
— exceeding 20% (i.e., factor of 1.2+) near threshold. This is because the emitted
electron's wave function overlaps with bound atomic electrons, modifying the phase space.
Unlike other corrections that are small perturbations (~1%), exchange is a genuine
O(α) effect that grows dramatically as p → 0 (because η = αZW/p diverges).
We test:
1. **Large correction at low energy for heavy nuclei**: At Z=80, W→1, X >> 1.
This is the hallmark of exchange — if it's small here, the fit coefficients
are wrong or the formula is broken.
2. **Approaches unity at high energy**: Exchange vanishes as W increases because
the emitted electron becomes distinguishable from bound electrons (higher momentum).
3. **No NaN/inf values**: The empirical fit involves sin() and division by Wⁱ —
we must ensure numerical stability across the full grid.
4. **Coefficients loaded correctly**: Verify that Z=2..100 are all in the CSV.
Common practice: For components with dramatic energy dependence, test both extremes
(low-energy blowup and high-energy suppression) to verify the shape is correct.
"""
import numpy as np
import pytest
from beta_spectrum.components.exchange import ExchangeCorrection
class TestExchangeLowEnergy:
"""Test exchange correction at low energies where it's most significant."""
def test_large_correction_for_heavy_nucleus_at_threshold(self):
"""For Z=80 (mercury), X >> 1 near threshold — can be > 20%.
This is the defining characteristic of the exchange correction. If this
fails, the empirical fit coefficients are wrong or the formula has a sign error.
"""
ex = ExchangeCorrection(Z=80)
low_W = np.array([1.1]) # T ≈ 50 keV — still "low" but above cutoff
result = ex(low_W)
assert result[0] > 1.0, (
f"Exchange must enhance spectrum at low energy for heavy Z: got {result[0]}"
)
def test_correction_grows_as_energy_decreases(self):
"""X(W) should increase as W → 1 (lower momentum → stronger exchange)."""
ex = ExchangeCorrection(Z=50) # Tin — moderate-heavy
low_W = np.array([1.2]) # Higher energy
lower_W = np.array([1.05]) # Lower energy (above cutoff W_cut=1.005)
s_low = ex(low_W)[0]
s_lower = ex(lower_W)[0]
assert s_lower > s_low, (
f"Exchange must increase at lower energy: {s_low} → {s_lower}"
)
class TestExchangeHighEnergy:
"""Test that exchange correction vanishes at high energy."""
def test_near_unity_at_high_energy(self):
"""At W >> 1, the emitted electron is far from atomic electrons → X → 1."""
ex = ExchangeCorrection(Z=40)
high_W = np.array([3.0, 5.0])
result = ex(high_W)
assert abs(result[0] - 1.0) < 0.5, (
f"Exchange must approach unity at high energy: got {result}"
)
class TestExchangeNumericalStability:
"""Test that the empirical fit doesn't produce NaN or inf."""
def test_no_nan_across_full_range(self):
ex = ExchangeCorrection(Z=92) # Uranium — extreme case
W_test = np.linspace(1.01, 6.0, 50)
result = ex(W_test)
assert not np.any(np.isnan(result)), "No NaN values allowed"
assert not np.any(np.isinf(result)), "No infinite values allowed"
class TestExchangeCoefficientLoading:
"""Test that coefficients are loaded correctly from CSV."""
def test_all_Z_values_available(self):
"""The CSV must contain coefficients for Z=2 through Z=100+ (per Hayen Table X)."""
# Test a few representative values across the periodic table
for Z in [6, 20, 50, 80]:
ex = ExchangeCorrection(Z=Z)
assert ex.coeffs is not None, f"Missing coefficients for Z={Z}"
def test_missing_Z_raises_error(self):
"""Requesting a Z without coefficients should raise ValueError."""
# Z=1 has no electrons to exchange with — should fail gracefully
with pytest.raises(ValueError, match="No exchange coefficients"):
ExchangeCorrection(Z=1)
class TestExchangeOutputShape:
"""Basic shape checks."""
def test_output_shape(self):
ex = ExchangeCorrection(Z=30)
for n in [1, 5, 100]:
W_test = np.linspace(1.1, 3.0, n)
result = ex(W_test)
assert result.shape == W_test.shape