test: create new test architecture with shared property/API tests
- Add tests/common/ with parametrized property tests (positivity, shape, NaN, stability) - Add tests/physics/ with unique physics behavior tests for 6 correction components - Add tests/quality/ with API contract tests for 6 correction components - Add pytest markers in pyproject.toml: property, physics, api, quality, integration - New tests: 94 tests passing (physics + quality + common helpers) - Old tests in tests/ root still present — migration in progress - Total: 261 tests collected (230 old + 94 new - 63 duplicates removed) The shared helpers eliminate 18 duplicate test patterns across correction components. Each component now has: - tests/physics/test_<name>.py — unique physics behavior - tests/quality/test_api_<name>.py — API contract + shared property checks - tests/common/test_property_tests.py — shared helpers (imported, not duplicated)
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Physics tests for ExchangeCorrection.
|
||||
|
||||
Shared property tests in tests/common/test_property_tests.py.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.exchange import ExchangeCorrection
|
||||
|
||||
|
||||
class TestExchangePhysics:
|
||||
"""Test expected physical behavior of the exchange correction."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_large_correction_heavy_nucleus(self):
|
||||
"""For Z=80 (mercury), X >> 1 near threshold — can be > 20%.
|
||||
|
||||
This is the defining characteristic of the exchange correction.
|
||||
"""
|
||||
ex = ExchangeCorrection(Z=80)
|
||||
low_W = np.array([1.1])
|
||||
result = ex(low_W)
|
||||
assert result[0] > 1.0, f"Exchange must enhance spectrum at low energy for heavy Z: {result[0]}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_grows_as_energy_decreases(self):
|
||||
"""X(W) should increase as W → 1 (lower momentum → stronger exchange)."""
|
||||
ex = ExchangeCorrection(Z=50)
|
||||
|
||||
low_W = np.array([1.2])
|
||||
lower_W = np.array([1.05])
|
||||
|
||||
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}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_vanishes_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: {result}"
|
||||
|
||||
|
||||
class TestExchangeErrorHandling:
|
||||
"""Test error handling for invalid inputs."""
|
||||
|
||||
@pytest.mark.api
|
||||
def test_missing_Z_raises_error(self):
|
||||
"""Z=1 has no electrons to exchange with — should fail gracefully."""
|
||||
with pytest.raises(ValueError, match="No exchange coefficients"):
|
||||
ExchangeCorrection(Z=1)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Physics tests for FermiFunction — unique physical behavior.
|
||||
|
||||
Shared property tests (positivity, shape, stability) are in
|
||||
tests/common/test_property_tests.py and are NOT duplicated here.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.fermi import FermiFunction
|
||||
|
||||
|
||||
class TestFermiPhysicalBehavior:
|
||||
"""Test expected physical behavior of the Fermi function."""
|
||||
|
||||
def test_Z_equals_one_boundary(self):
|
||||
"""For Z=1, F₀ ≈ 1 at all energies because αZ is tiny.
|
||||
|
||||
This is the most important sanity check. If this fails, the entire
|
||||
Fermi function implementation is broken — not just numerically but
|
||||
conceptually (the Coulomb interaction should vanish for no charge).
|
||||
"""
|
||||
ff = FermiFunction(Z=1, A=1)
|
||||
W_test = np.array([1.5, 2.0, 3.0, 5.0])
|
||||
result = ff(W_test)
|
||||
|
||||
# For Z=1, the correction from finite nuclear size and Coulomb terms gives ~3% deviation
|
||||
assert np.all(
|
||||
np.abs(result - 1.0) < 0.05
|
||||
), f"Z=1 Fermi function should be ≈1 but got {result}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_decreases_with_energy(self):
|
||||
"""For β⁻ decay (positive Z_daughter), F₀ decreases as W increases.
|
||||
|
||||
Reason: η = αZW/p. As p grows with energy, the Coulomb parameter η shrinks,
|
||||
so the enhancement at low momentum weakens. This is universal for all Z > 0.
|
||||
"""
|
||||
ff = FermiFunction(Z=20, A=40)
|
||||
W_test = np.linspace(1.1, 2.0, 5)
|
||||
result = ff(W_test)
|
||||
|
||||
# Check monotonic decrease (each successive point should be smaller)
|
||||
diffs = np.diff(result)
|
||||
assert np.all(
|
||||
diffs < 0
|
||||
), f"Fermi function must decrease with W; got diffs {diffs}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_Z_scaling(self):
|
||||
"""Higher Z → stronger Coulomb attraction → larger F₀ at low energy.
|
||||
|
||||
This is the key physics: heavy nuclei strongly enhance low-energy electrons.
|
||||
We compare at a fixed mid-range energy (W=2) across three Z values.
|
||||
"""
|
||||
ff_z10 = FermiFunction(Z=10, A=25)
|
||||
ff_z30 = FermiFunction(Z=30, A=65)
|
||||
ff_z80 = FermiFunction(Z=80, A=200)
|
||||
|
||||
W_test = np.array([2.0])
|
||||
|
||||
f_10 = ff_z10(W_test)[0]
|
||||
f_30 = ff_z30(W_test)[0]
|
||||
f_80 = ff_z80(W_test)[0]
|
||||
|
||||
assert (
|
||||
f_80 > f_30 > f_10
|
||||
), f"Fermi function must increase with Z: {f_10} < {f_30} < {f_80}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_uranium_threshold_enhancement(self):
|
||||
"""For uranium (Z=92), F₀ at threshold should be orders of magnitude
|
||||
larger than at mid-energy due to strong Coulomb attraction.
|
||||
"""
|
||||
ff = FermiFunction(Z=92, A=238)
|
||||
|
||||
W_low = np.array([1.01]) # Near threshold
|
||||
W_mid = np.array([3.0]) # Mid-range
|
||||
|
||||
f_low = ff(W_low)[0]
|
||||
f_mid = ff(W_mid)[0]
|
||||
|
||||
assert f_low > f_mid * 10, (
|
||||
f"Uranium threshold enhancement should be large: "
|
||||
f"F(1.01)={f_low:.1f} vs F(3.0)={f_mid:.1f}"
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Physics tests for FiniteSizeL0 and ChargeDistributionU.
|
||||
|
||||
Shared property tests in tests/common/test_property_tests.py.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.finite_size import FiniteSizeL0, ChargeDistributionU
|
||||
|
||||
|
||||
class TestFiniteSizeL0Physics:
|
||||
"""Tests for the L₀ finite nuclear size correction."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_near_unity_for_light_nucleus(self):
|
||||
"""For Z=6 (carbon), L₀ should be very close to 1 — within a few percent."""
|
||||
fs = FiniteSizeL0(Z=6, A=12)
|
||||
W_test = np.array([2.0])
|
||||
result = fs(W_test)
|
||||
|
||||
assert (
|
||||
abs(result[0] - 1.0) < 0.05
|
||||
), f"L₀ for Z=6 should be near unity but got {result[0]}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_correction_increases_with_Z(self):
|
||||
"""Higher Z → larger αZ term in L₀ expansion → larger deviation from 1."""
|
||||
fs_z6 = FiniteSizeL0(Z=6, A=12)
|
||||
fs_z30 = FiniteSizeL0(Z=30, A=70)
|
||||
fs_z80 = FiniteSizeL0(Z=80, A=200)
|
||||
|
||||
W_test = np.array([2.5])
|
||||
|
||||
dev_6 = abs(fs_z6(W_test)[0] - 1.0)
|
||||
dev_30 = abs(fs_z30(W_test)[0] - 1.0)
|
||||
dev_80 = abs(fs_z80(W_test)[0] - 1.0)
|
||||
|
||||
assert (
|
||||
dev_80 > dev_30 > dev_6
|
||||
), f"Deviation from unity must increase with Z: {dev_6}, {dev_30}, {dev_80}"
|
||||
|
||||
|
||||
class TestChargeDistributionUPhysics:
|
||||
"""Tests for the U charge distribution correction."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_near_unity_for_light_nucleus(self):
|
||||
"""For Z=6, U is essentially 1."""
|
||||
u = ChargeDistributionU(Z=6, A=12)
|
||||
result = u(np.array([2.0]))
|
||||
assert abs(result[0] - 1.0) < 0.01
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_always_above_unity(self):
|
||||
"""U = 1 + (1/5)(αZW R)² is always ≥ 1 since the squared term is positive."""
|
||||
u = ChargeDistributionU(Z=20, A=40)
|
||||
W_test = np.linspace(1.1, 5.0, 10)
|
||||
result = u(W_test)
|
||||
assert np.all(result >= 1.0), "U must be ≥ 1 for all W (squared term)"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_increases_with_Z(self):
|
||||
"""Higher Z → larger U correction."""
|
||||
u_z20 = ChargeDistributionU(Z=20, A=40)
|
||||
u_z60 = ChargeDistributionU(Z=60, A=150)
|
||||
W_test = np.array([3.0])
|
||||
assert u_z60(W_test)[0] > u_z20(W_test)[0]
|
||||
|
||||
|
||||
class TestFiniteSizeCombinedPhysics:
|
||||
"""Combined L₀ × U physics tests."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_combined_near_unity_for_carbon(self):
|
||||
"""For carbon-12, both corrections are tiny. The product should be ~1."""
|
||||
l0 = FiniteSizeL0(Z=6, A=12)
|
||||
u = ChargeDistributionU(Z=6, A=12)
|
||||
|
||||
W_test = np.linspace(1.5, 4.0, 10)
|
||||
combined = l0(W_test) * u(W_test)
|
||||
|
||||
assert np.all(
|
||||
np.abs(combined - 1.0) < 0.05
|
||||
), f"Combined finite-size for carbon should be near unity: {combined}"
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Physics tests for PhaseSpace — unique physical behavior.
|
||||
|
||||
Shared property tests (positivity, shape, stability) are in
|
||||
tests/common/test_property_tests.py and are NOT duplicated here.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.phase_space import PhaseSpace
|
||||
|
||||
|
||||
class TestPhaseSpaceBasicProperties:
|
||||
"""Test fundamental mathematical properties of the phase space factor."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_threshold_vanishes(self):
|
||||
"""At threshold (W=1), momentum p = sqrt(1-1) = 0 → spectrum must be 0.
|
||||
|
||||
This is a hard physical constraint. If this fails, the entire calculator
|
||||
produces wrong results at low energy where corrections are largest.
|
||||
"""
|
||||
ps = PhaseSpace(W0=3.0)
|
||||
result = ps(np.array([1.0]))
|
||||
assert result[0] == 0.0, "Phase space must be exactly zero at W=1 (threshold)"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_endpoint_vanishes(self):
|
||||
"""At W=W₀, the neutrino energy vanishes → spectrum ∝ (W₀−W)² = 0."""
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
result = ps(np.array([4.9999]))
|
||||
assert result[0] < 1e-4, "Phase space must approach zero near endpoint"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_endpoint_exact_zero(self):
|
||||
"""Exactly at W=W₀ the spectrum is zero."""
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
result = ps(np.array([5.0]))
|
||||
assert result[0] == 0.0
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_positive_in_physical_range(self):
|
||||
"""Phase space must be strictly positive for all physical energies (1 < W < W₀)."""
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
W_test = np.linspace(1.1, 4.9, 10)
|
||||
result = ps(W_test)
|
||||
assert np.all(result > 0), "Phase space must be positive in the physical region"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_midpoint_significant(self):
|
||||
"""At W ≈ (1+W₀)/2, spectrum should have a significant non-zero value."""
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
result = ps(np.array([3.0]))
|
||||
assert result[0] > 1.0, "Midpoint should give substantial phase space"
|
||||
|
||||
|
||||
class TestPhaseSpaceNeutrinoMass:
|
||||
"""Test m_nu > 0 branch — how neutrino mass modifies the spectrum."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_massive_neutrino_suppresses_endpoint(self):
|
||||
"""A non-zero m_nu suppresses the spectrum near endpoint more than m_nu=0.
|
||||
|
||||
This is the observable used by KATRIN and similar experiments to set limits
|
||||
on neutrino mass. The effect is tiny but must be captured correctly.
|
||||
"""
|
||||
ps_zero = PhaseSpace(W0=5.0, m_nu=0.0)
|
||||
ps_massive = PhaseSpace(W0=5.0, m_nu=0.1) # larger mass for visibility
|
||||
|
||||
W_near_endpoint = np.array([4.8]) # W₀−W = 0.2 > m_nu=0.1
|
||||
|
||||
val_zero = ps_zero(W_near_endpoint)[0]
|
||||
val_massive = ps_massive(W_near_endpoint)[0]
|
||||
|
||||
assert not np.isnan(val_massive), "Should produce finite value"
|
||||
assert (
|
||||
val_massive < val_zero
|
||||
), "Massive neutrino must suppress spectrum near endpoint"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_massive_neutrino_below_threshold(self):
|
||||
"""With m_nu > 0, below the effective threshold (W₀−W < m_nu), sqrt gives NaN.
|
||||
|
||||
This is expected physical behavior — the spectrum simply doesn't exist there.
|
||||
"""
|
||||
ps = PhaseSpace(W0=5.0, m_nu=0.1)
|
||||
|
||||
W_above = np.array([4.85]) # W₀ − W = 0.15 > 0.1 — still above threshold
|
||||
result_above = ps(W_above)
|
||||
|
||||
assert not np.isnan(result_above[0]), "Above threshold should be finite"
|
||||
|
||||
W_deep_below = np.array([4.95]) # W₀ − W = 0.05 < m_nu=0.1 — below threshold
|
||||
result_below = ps(W_deep_below)
|
||||
|
||||
assert np.isnan(
|
||||
result_below[0]
|
||||
), "Below kinematic threshold, spectrum should be NaN"
|
||||
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Physics tests for RadiativeCorrection.
|
||||
|
||||
Shared property tests in tests/common/test_property_tests.py.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.radiative import RadiativeCorrection
|
||||
|
||||
|
||||
class TestRadiativeEndpointHandling:
|
||||
"""Test how endpoint divergence is handled."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_resummation_finite_at_endpoint(self):
|
||||
"""With resummation enabled, R(W₀−ε) must be finite for small ε."""
|
||||
rc = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
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"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_standard_mode_diverges(self):
|
||||
"""Without resummation, δᵣ → −∞ as W → W₀ (logarithmic divergence)."""
|
||||
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]
|
||||
|
||||
assert val_close < val_far, "Standard mode must show divergent behavior near endpoint"
|
||||
|
||||
|
||||
class TestRadiativeEnergyDependence:
|
||||
"""Test how the radiative correction varies across energy."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_correction_grows_toward_endpoint(self):
|
||||
"""δᵣ grows as W → W₀ due to ln(W₀−W) divergence."""
|
||||
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]
|
||||
|
||||
assert r_near <= r_mid, f"Correction factor should decrease near endpoint: {r_mid} → {r_near}"
|
||||
|
||||
|
||||
class TestRadiativeResummationSwitch:
|
||||
"""Test that the resummation flag actually changes behavior."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_resummed_differs_from_standard(self):
|
||||
"""Near endpoint (within delta_cut), resummed and standard modes should differ."""
|
||||
rc_resummed = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
rc_standard = RadiativeCorrection(W0=5.0, use_endpoint_resummation=False)
|
||||
|
||||
# Far from endpoint: both identical
|
||||
W_far = np.array([3.0])
|
||||
assert np.isclose(rc_resummed(W_far)[0], rc_standard(W_far)[0], rtol=1e-10)
|
||||
|
||||
# Near endpoint: resummed stays finite, standard diverges
|
||||
W_near = np.array([4.9995])
|
||||
r_resummed = rc_resummed(W_near)[0]
|
||||
r_standard = rc_standard(W_near)[0]
|
||||
|
||||
assert not np.isnan(r_resummed), "Resummed mode must be finite"
|
||||
assert not np.isnan(r_standard), "Standard mode must be finite (mask protects endpoint)"
|
||||
assert r_resummed > r_standard, f"Near endpoint: resummed={r_resummed} > standard={r_standard}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_resummation_only_near_endpoint(self):
|
||||
"""Per spec, resummation only applies when (W0 - W) < delta_cut."""
|
||||
rc_resummed = RadiativeCorrection(W0=5.0, use_endpoint_resummation=True)
|
||||
rc_standard = RadiativeCorrection(W0=5.0, use_endpoint_resummation=False)
|
||||
|
||||
# delta_cut defaults to 1e-3, so W0 - W = 0.01 > delta_cut
|
||||
W_far = np.array([4.99])
|
||||
r_resummed_far = rc_resummed(W_far)[0]
|
||||
r_standard_far = rc_standard(W_far)[0]
|
||||
|
||||
assert np.isclose(r_resummed_far, r_standard_far, rtol=1e-10), (
|
||||
f"Far from endpoint: resummed={r_resummed_far}, standard={r_standard_far}"
|
||||
)
|
||||
|
||||
# Very near endpoint (delta_W < delta_cut): resummed is finite and larger
|
||||
W_near = np.array([4.9995])
|
||||
r_resummed_near = rc_resummed(W_near)[0]
|
||||
r_standard_near = rc_standard(W_near)[0]
|
||||
|
||||
assert not np.isnan(r_resummed_near)
|
||||
assert not np.isnan(r_standard_near)
|
||||
assert r_resummed_near > r_standard_near
|
||||
|
||||
|
||||
class TestRadiativeZDependence:
|
||||
"""Test Z-dependent O(Z*alpha^2) correction from Sirlin 1987."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_correction_increases_with_Z(self):
|
||||
"""Higher Z produces larger O(Z*alpha^2) correction."""
|
||||
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])
|
||||
assert rc_high(W_test)[0] > rc_low(W_test)[0]
|
||||
|
||||
@pytest.mark.physics
|
||||
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)
|
||||
diff = np.abs(rc_z50(W_test) - rc_z1(W_test))
|
||||
assert np.max(diff) > 1e-4, f"Z=1 and Z=50 should differ: max diff = {np.max(diff)}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_z_correction_magnitude_reasonable(self):
|
||||
"""For Z=92 (uranium), O(Z*alpha^2) should be reasonable."""
|
||||
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: {result[0]}"
|
||||
|
||||
|
||||
class TestRadiativeAParameter:
|
||||
"""Test A (mass number) parameter for nuclear model."""
|
||||
|
||||
@pytest.mark.physics
|
||||
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])
|
||||
assert rc_a100(W_test)[0] != rc_a140(W_test)[0], (
|
||||
f"Different A values must differ: A=100 -> {rc_a100(W_test)[0]}, "
|
||||
f"A=140 -> {rc_a140(W_test)[0]}"
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Physics tests for ScreeningCorrection.
|
||||
|
||||
Shared property tests in tests/common/test_property_tests.py.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from beta_spectrum.components.fermi import FermiFunction
|
||||
from beta_spectrum.components.screening import ScreeningCorrection
|
||||
|
||||
|
||||
class TestScreeningPhysics:
|
||||
"""Test expected physical behavior of the screening correction."""
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_vanishes_at_high_energy(self):
|
||||
"""At high energies (W >> V₀), S(W) → 1. The logistic switch should be ~0 here."""
|
||||
ff = FermiFunction(Z=19, A=40)
|
||||
sc = ScreeningCorrection(ff)
|
||||
|
||||
high_W = np.array([3.0, 5.0])
|
||||
result = sc(high_W)
|
||||
|
||||
assert (
|
||||
abs(result[0] - 1.0) < 0.1
|
||||
), f"Screening must approach unity at high energy: got {result}"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_reduces_at_low_energy(self):
|
||||
"""At low energy, screening reduces the effective Coulomb field → S(W) < 1.
|
||||
|
||||
Screening electrons partially cancel the nuclear charge, reducing F₀ at threshold.
|
||||
"""
|
||||
ff = FermiFunction(Z=30, A=70)
|
||||
sc = ScreeningCorrection(ff)
|
||||
|
||||
low_W = np.array([1.05])
|
||||
mid_W = np.array([2.0])
|
||||
|
||||
s_low = sc(low_W)[0]
|
||||
s_mid = sc(mid_W)[0]
|
||||
|
||||
assert s_low < 1.1, f"Low-energy screening should suppress: {s_low}"
|
||||
assert abs(s_mid - 1.0) < abs(
|
||||
s_low - 1.0
|
||||
), "Correction must decrease toward unity as W increases"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_custom_V0_affects_result(self):
|
||||
"""If V₀ is explicitly provided, it should change the screening behavior."""
|
||||
ff = FermiFunction(Z=20, A=40)
|
||||
sc_default = ScreeningCorrection(ff) # auto-estimated V₀
|
||||
sc_custom = ScreeningCorrection(ff, V0=0.1)
|
||||
|
||||
low_W = np.array([1.05])
|
||||
result_default = sc_default(low_W)[0]
|
||||
result_custom = sc_custom(low_W)[0]
|
||||
|
||||
assert (
|
||||
abs(result_default - result_custom) > 0
|
||||
), "Custom V₀ must change the screening behavior"
|
||||
Reference in New Issue
Block a user