120 lines
4.5 KiB
Python
120 lines
4.5 KiB
Python
# 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
|