Add pytest test suite (56 tests) for all spectrum components
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# tests/test_radiative.py — RadiativeCorrection unit tests
|
||||
|
||||
"""
|
||||
Why test this?
|
||||
--------------
|
||||
The outer radiative correction δᵣ(W,W₀) accounts for bremsstrahlung photons emitted
|
||||
along with the beta electron. It's typically a small positive correction (~0.1–1%) but has
|
||||
a logarithmic singularity at W → W₀ (endpoint) that must be handled by soft-photon resummation.
|
||||
|
||||
Key physics:
|
||||
- δᵣ > 0 generally: virtual photon loops and real bremsstrahlung enhance the rate.
|
||||
- The divergence as ln(W₀−W) is unphysical — it's cancelled by detector resolution effects.
|
||||
Our implementation uses Eq.(53) resummation to tame this.
|
||||
|
||||
We test:
|
||||
|
||||
1. **No NaN/inf at endpoint**: With resummation, R(W→W₀) must be finite. Without it,
|
||||
the logarithm diverges — we verify both modes work correctly.
|
||||
2. **Small correction magnitude**: δᵣ is O(α/π) ≈ 0.001–0.01, so R = 1+δᵣ should be
|
||||
close to unity (within a factor of ~2).
|
||||
3. **Endpoint resummation produces finite values**: The key feature that distinguishes
|
||||
this implementation from the standard formula.
|
||||
|
||||
Common practice: For functions with known singularities, always test both "handled" and
|
||||
"unhandled" modes to verify the fix works correctly. This is exactly what `use_endpoint_resummation` does here.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.radiative import RadiativeCorrection
|
||||
|
||||
|
||||
class TestRadiativeBasicProperties:
|
||||
"""Test fundamental properties of the radiative correction."""
|
||||
|
||||
def test_near_unity_correction(self):
|
||||
"""δᵣ is O(α/π) ≈ 0.001, so R = 1+δᵣ should be within ~2x unity."""
|
||||
rc = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
|
||||
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]}"
|
||||
)
|
||||
|
||||
def test_positive_values(self):
|
||||
"""R(W,W₀) must always be positive — it's a multiplicative factor."""
|
||||
rc = RadiativeCorrection(W0=5.0)
|
||||
W_test = np.linspace(1.1, 4.9, 20)
|
||||
result = rc(W_test)
|
||||
|
||||
assert np.all(result > 0), "Radiative correction must be positive"
|
||||
|
||||
|
||||
class TestRadiativeEndpointHandling:
|
||||
"""Test how endpoint divergence is handled."""
|
||||
|
||||
def test_resummation_produces_finite_values_at_endpoint(self):
|
||||
"""With resummation enabled, R(W₀−ε) must be finite for small ε.
|
||||
|
||||
Without Eq.(53), ln(ΔW) → −∞ as ΔW → 0. The resummed version replaces this
|
||||
with (ΔW)^t(β) which stays finite. This is the most critical test of our implementation.
|
||||
"""
|
||||
rc = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
|
||||
# Very close to endpoint: W₀ − ε where ε = 0.001
|
||||
W_near_end = np.array([4.999])
|
||||
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"
|
||||
|
||||
def test_standard_mode_diverges(self):
|
||||
"""Without resummation, δᵣ → −∞ as W → W₀ (logarithmic divergence).
|
||||
|
||||
We verify this by checking that values get more negative near the endpoint.
|
||||
This confirms the standard formula is implemented correctly — the divergence
|
||||
itself proves the singularity exists and needs handling.
|
||||
"""
|
||||
rc = RadiativeCorrection(W0=5.0, use_endpoint_resummation=False)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
class TestRadiativeEnergyDependence:
|
||||
"""Test how the radiative correction varies across energy."""
|
||||
|
||||
def test_correction_increases_toward_endpoint(self):
|
||||
"""δᵣ grows as W → W₀ due to ln(W₀−W) divergence (even with resummation).
|
||||
|
||||
The soft-photon emission probability increases when less phase space is left
|
||||
for the electron — this is a universal feature of bremsstrahlung.
|
||||
We check that R = 1+δᵣ decreases toward endpoint because δᵣ becomes negative.
|
||||
"""
|
||||
rc = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
|
||||
mid_W = np.array([3.0])
|
||||
near_end_W = np.array([4.8])
|
||||
|
||||
r_mid = rc(mid_W)[0]
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
class TestRadiativeOutputShape:
|
||||
"""Basic shape/type checks."""
|
||||
|
||||
def test_output_shape(self):
|
||||
rc = RadiativeCorrection(W0=5.0)
|
||||
for n in [1, 5, 100]:
|
||||
W_test = np.linspace(1.1, 4.9, n)
|
||||
result = rc(W_test)
|
||||
assert result.shape == W_test.shape
|
||||
|
||||
def test_no_nan_for_various_W0(self):
|
||||
"""Different endpoint energies should all produce valid output."""
|
||||
for W0 in [2.0, 5.0, 10.0]:
|
||||
rc = RadiativeCorrection(W0=W0)
|
||||
W_test = np.linspace(1.1, min(9.9, W0 - 0.01), 20)
|
||||
result = rc(W_test)
|
||||
|
||||
assert not np.any(np.isnan(result)), f"NaN for W0={W0}"
|
||||
|
||||
|
||||
class TestRadiativeResummationSwitch:
|
||||
"""Test that the resummation flag actually changes behavior."""
|
||||
|
||||
def test_resummed_differs_from_standard(self):
|
||||
"""Near endpoint, resummed and standard modes should give different results.
|
||||
|
||||
At high energy (far from endpoint) they converge — both reduce to the same δᵣ ≈ α/π × [constant].
|
||||
Near W₀ the difference grows because resummation replaces ln(ΔW) with a finite power law.
|
||||
"""
|
||||
rc_resummed = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
rc_standard = RadiativeCorrection(W0=5.0, use_endpoint_resummation=False)
|
||||
|
||||
# Far from endpoint: both should be similar (both finite)
|
||||
W_far = np.array([3.0])
|
||||
diff_far = abs(rc_resummed(W_far)[0] - rc_standard(W_far)[0])
|
||||
|
||||
# Near endpoint: check that values are not identical
|
||||
# With resummation, R stays near 1; without it, δᵣ diverges negatively
|
||||
W_near = np.array([4.99])
|
||||
r_resummed = rc_resummed(W_near)[0]
|
||||
|
||||
assert not np.isnan(r_resummed), "Resummed mode must be finite"
|
||||
|
||||
# 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 diff_far > 0, "Even far from endpoint there should be some difference"
|
||||
Reference in New Issue
Block a user