feat(radiative): implement O(Z*alpha^2) correction and improve numerical stability
- Add Z (nuclear charge) and A (mass number) parameters to RadiativeCorrection - Implement O(Z*alpha^2) finite nuclear size correction per Sirlin 1987: - Energy-dependent part: ln(m_p) - (5/12)*ln(2W) + 43/18 - Nuclear-structure-dependent part: Delta_F(Z,A) from modified Gaussian model - Add small-beta Taylor expansion for numerical stability at threshold (beta < 1e-6) - Extract _safe_beta() and _safe_tanh_inv_beta_over_beta() helper methods - Remove unused log_wright_bessel import - Add 14 new tests: Z-dependence, numerical stability, A parameter coverage - Update docs/06-radiative-corrections.md with new API usage examples All 70 tests pass. 14 new tests in 3 new test classes.
This commit is contained in:
+183
-13
@@ -26,7 +26,6 @@ Common practice: For functions with known singularities, always test both "handl
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.radiative import RadiativeCorrection
|
||||
|
||||
@@ -41,9 +40,9 @@ class TestRadiativeBasicProperties:
|
||||
W_test = np.array([2.0])
|
||||
result = rc(W_test)
|
||||
|
||||
assert 0.5 < result[0] < 3.0, (
|
||||
f"Radiative correction should be near unity: got {result[0]}"
|
||||
)
|
||||
assert (
|
||||
0.5 < result[0] < 3.0
|
||||
), f"Radiative correction should be near unity: got {result[0]}"
|
||||
|
||||
def test_positive_values(self):
|
||||
"""R(W,W₀) must always be positive — it's a multiplicative factor."""
|
||||
@@ -70,7 +69,9 @@ class TestRadiativeEndpointHandling:
|
||||
result = rc(W_near_end)
|
||||
|
||||
assert not np.isnan(result[0]), "Resummed correction must be finite at endpoint"
|
||||
assert not np.isinf(result[0]), "Resummed correction must not diverge at endpoint"
|
||||
assert not np.isinf(
|
||||
result[0]
|
||||
), "Resummed correction must not diverge at endpoint"
|
||||
|
||||
def test_standard_mode_diverges(self):
|
||||
"""Without resummation, δᵣ → −∞ as W → W₀ (logarithmic divergence).
|
||||
@@ -81,16 +82,16 @@ class TestRadiativeEndpointHandling:
|
||||
"""
|
||||
rc = RadiativeCorrection(W0=5.0, use_endpoint_resummation=False)
|
||||
|
||||
W_far = np.array([4.9]) # ΔW = 0.1
|
||||
W_far = np.array([4.9]) # ΔW = 0.1
|
||||
W_close = np.array([4.99]) # ΔW = 0.01
|
||||
|
||||
val_far = rc(W_far)[0]
|
||||
val_close = rc(W_close)[0]
|
||||
|
||||
# The correction should decrease (become more negative) near endpoint
|
||||
assert val_close < val_far, (
|
||||
"Standard mode must show divergent behavior near endpoint"
|
||||
)
|
||||
assert (
|
||||
val_close < val_far
|
||||
), "Standard mode must show divergent behavior near endpoint"
|
||||
|
||||
|
||||
class TestRadiativeEnergyDependence:
|
||||
@@ -112,9 +113,9 @@ class TestRadiativeEnergyDependence:
|
||||
r_near = rc(near_end_W)[0]
|
||||
|
||||
# δᵣ becomes more negative (or less positive) toward endpoint → R decreases
|
||||
assert r_near <= r_mid, (
|
||||
f"Correction factor should decrease near endpoint: {r_mid} → {r_near}"
|
||||
)
|
||||
assert (
|
||||
r_near <= r_mid
|
||||
), f"Correction factor should decrease near endpoint: {r_mid} → {r_near}"
|
||||
|
||||
|
||||
class TestRadiativeOutputShape:
|
||||
@@ -162,6 +163,175 @@ class TestRadiativeResummationSwitch:
|
||||
|
||||
# The key test: resummed stays close to 1 while standard diverges
|
||||
# We verify they produce different values (exact threshold depends on implementation)
|
||||
diff_near = abs(rc_resummed(W_near)[0] - rc_standard(W_near)[0])
|
||||
assert not np.isnan(rc_resummed(W_near)[0]), "Resummed mode must be finite"
|
||||
|
||||
assert diff_far > 0, "Even far from endpoint there should be some difference"
|
||||
|
||||
|
||||
class TestRadiativeZDependence:
|
||||
"""Test Z-dependent O(Z*alpha^2) correction from Sirlin 1987."""
|
||||
|
||||
def test_z_parameter_accepted(self):
|
||||
"""RadiativeCorrection must accept Z (daughter nuclear charge) parameter."""
|
||||
for Z in [1, 20, 50, 92]:
|
||||
rc = RadiativeCorrection(W0=5.0, Z=Z)
|
||||
assert rc.Z == Z
|
||||
|
||||
def test_correction_increases_with_Z(self):
|
||||
"""Higher Z produces larger O(Z*alpha^2) correction.
|
||||
|
||||
The O(Z*alpha^2) term scales linearly with Z, so R should be
|
||||
systematically larger for higher-Z nuclei at the same W and W0.
|
||||
"""
|
||||
rc_low = RadiativeCorrection(W0=5.0, Z=20, use_endpoint_resummation=True)
|
||||
rc_high = RadiativeCorrection(W0=5.0, Z=80, use_endpoint_resummation=True)
|
||||
|
||||
W_test = np.array([3.0])
|
||||
r_low = rc_low(W_test)[0]
|
||||
r_high = rc_high(W_test)[0]
|
||||
|
||||
assert (
|
||||
r_high > r_low
|
||||
), f"Higher Z should give larger correction: Z=20 -> {r_low:.6f}, Z=80 -> {r_high:.6f}"
|
||||
|
||||
def test_z_zero_reduces_to_alpha_only(self):
|
||||
"""With Z=0, only O(alpha) correction remains — should match standard Sirlin function."""
|
||||
rc_z0 = RadiativeCorrection(W0=5.0, Z=0, use_endpoint_resummation=True)
|
||||
rc_standard = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
|
||||
W_test = np.array([2.0, 3.0, 4.0])
|
||||
r_z0 = rc_z0(W_test)
|
||||
r_std = rc_standard(W_test)
|
||||
|
||||
np.testing.assert_allclose(
|
||||
r_z0,
|
||||
r_std,
|
||||
rtol=1e-10,
|
||||
err_msg="Z=0 should produce same result as Z-unspecified (alpha-only)",
|
||||
)
|
||||
|
||||
def test_z_correction_magnitude_reasonable(self):
|
||||
"""For Z=92 (uranium), O(Z*alpha^2) should be ~0.5-2% of total correction."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=92, use_endpoint_resummation=True)
|
||||
|
||||
W_test = np.array([3.0])
|
||||
result = rc(W_test)
|
||||
|
||||
assert (
|
||||
0.9 < result[0] < 3.0
|
||||
), f"Z=92 correction should be reasonable: got {result[0]}"
|
||||
|
||||
def test_different_Z_different_results(self):
|
||||
"""Different Z values must produce measurably different corrections."""
|
||||
rc_z1 = RadiativeCorrection(W0=5.0, Z=1, use_endpoint_resummation=True)
|
||||
rc_z50 = RadiativeCorrection(W0=5.0, Z=50, use_endpoint_resummation=True)
|
||||
|
||||
W_test = np.linspace(1.5, 4.5, 10)
|
||||
r_z1 = rc_z1(W_test)
|
||||
r_z50 = rc_z50(W_test)
|
||||
|
||||
diff = np.abs(r_z50 - r_z1)
|
||||
assert (
|
||||
np.max(diff) > 1e-4
|
||||
), f"Z=1 and Z=50 should give different results: max diff = {np.max(diff)}"
|
||||
|
||||
|
||||
class TestRadiativeNumericalStability:
|
||||
"""Test numerical stability at extreme beta values."""
|
||||
|
||||
def test_low_energy_no_nan(self):
|
||||
"""At threshold (W -> 1), beta -> 0. Small-beta Taylor expansion must prevent NaN."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=50, use_endpoint_resummation=True)
|
||||
|
||||
W_threshold = np.array([1.0001])
|
||||
result = rc(W_threshold)
|
||||
|
||||
assert not np.isnan(result[0]), "Low energy must not produce NaN"
|
||||
assert not np.isinf(result[0]), "Low energy must not produce inf"
|
||||
assert result[0] > 0, "Correction must be positive at threshold"
|
||||
|
||||
def test_ultra_low_energy_stable(self):
|
||||
"""W = 1.00001 (beta ~ 0.014) — deep in small-beta regime."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=92, use_endpoint_resummation=True)
|
||||
|
||||
W_ultra_low = np.array([1.00001])
|
||||
result = rc(W_ultra_low)
|
||||
|
||||
assert not np.isnan(result[0]), "Ultra-low energy must be stable"
|
||||
assert not np.isinf(result[0]), "Ultra-low energy must not diverge"
|
||||
|
||||
def test_high_energy_no_nan(self):
|
||||
"""At high energy (W -> W0), beta -> 1. arctanh must be handled stably."""
|
||||
rc = RadiativeCorrection(W0=10.0, Z=50, use_endpoint_resummation=True)
|
||||
|
||||
W_high = np.array([9.0])
|
||||
result = rc(W_high)
|
||||
|
||||
assert not np.isnan(result[0]), "High energy must not produce NaN"
|
||||
assert not np.isinf(result[0]), "High energy must not produce inf"
|
||||
|
||||
def test_full_range_no_nan(self):
|
||||
"""Entire energy range from threshold to endpoint must be free of NaN/inf."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=92, use_endpoint_resummation=True)
|
||||
|
||||
W_full = np.linspace(1.0001, 4.9999, 200)
|
||||
result = rc(W_full)
|
||||
|
||||
assert not np.any(np.isnan(result)), "No NaN anywhere in spectrum"
|
||||
assert not np.any(np.isinf(result)), "No inf anywhere in spectrum"
|
||||
assert np.all(result > 0), "All values must be positive"
|
||||
|
||||
def test_low_Z_low_energy_stable(self):
|
||||
"""Light nucleus at low energy — tests both small Z and small beta."""
|
||||
rc = RadiativeCorrection(W0=2.0, Z=1, use_endpoint_resummation=True)
|
||||
|
||||
W_test = np.array([1.001])
|
||||
result = rc(W_test)
|
||||
|
||||
assert not np.isnan(result[0])
|
||||
assert not np.isinf(result[0])
|
||||
|
||||
def test_high_Z_high_energy_stable(self):
|
||||
"""Heavy nucleus at high energy — tests both large Z and beta near 1."""
|
||||
rc = RadiativeCorrection(W0=10.0, Z=92, use_endpoint_resummation=True)
|
||||
|
||||
W_test = np.array([9.5])
|
||||
result = rc(W_test)
|
||||
|
||||
assert not np.isnan(result[0])
|
||||
assert not np.isinf(result[0])
|
||||
|
||||
|
||||
class TestRadiativeAParameter:
|
||||
"""Test A (mass number) parameter for nuclear model."""
|
||||
|
||||
def test_a_parameter_accepted(self):
|
||||
"""RadiativeCorrection must accept A (mass number) parameter."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=50, A=120)
|
||||
assert rc.Z == 50
|
||||
assert rc.A == 120
|
||||
|
||||
def test_a_affects_nuclear_model_correction(self):
|
||||
"""A parameter affects the nuclear-structure-dependent part of O(Z*alpha^2)."""
|
||||
rc_a100 = RadiativeCorrection(
|
||||
W0=5.0, Z=50, A=100, use_endpoint_resummation=True
|
||||
)
|
||||
rc_a140 = RadiativeCorrection(
|
||||
W0=5.0, Z=50, A=140, use_endpoint_resummation=True
|
||||
)
|
||||
|
||||
W_test = np.array([3.0])
|
||||
r_a100 = rc_a100(W_test)[0]
|
||||
r_a140 = rc_a140(W_test)[0]
|
||||
|
||||
assert (
|
||||
r_a100 != r_a140
|
||||
), f"Different A values must give different results: A=100 -> {r_a100}, A=140 -> {r_a140}"
|
||||
|
||||
def test_default_a_is_none(self):
|
||||
"""Without A specified, nuclear model correction should still work (A=None)."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=50, use_endpoint_resummation=True)
|
||||
assert rc.A is None
|
||||
W_test = np.array([3.0])
|
||||
result = rc(W_test)
|
||||
assert not np.isnan(result[0])
|
||||
|
||||
Reference in New Issue
Block a user