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