docs: update documentation files and fix linting issues
This commit is contained in:
@@ -36,54 +36,49 @@ import pytest
|
||||
@pytest.mark.api
|
||||
def _test_component_is_callable(component, component_name="Component"):
|
||||
"""Component must be callable with array input."""
|
||||
assert callable(component), (
|
||||
f"{component_name}: component is not callable"
|
||||
)
|
||||
assert callable(component), f"{component_name}: component is not callable"
|
||||
|
||||
W = np.array([1.5, 2.0, 3.0])
|
||||
result = component(W)
|
||||
|
||||
assert isinstance(result, np.ndarray), (
|
||||
f"{component_name}: __call__ returned {type(result)}, "
|
||||
f"expected np.ndarray"
|
||||
)
|
||||
assert result.shape == W.shape, (
|
||||
f"{component_name}: shape mismatch {W.shape} → {result.shape}"
|
||||
f"{component_name}: __call__ returned {type(result)}, " f"expected np.ndarray"
|
||||
)
|
||||
assert (
|
||||
result.shape == W.shape
|
||||
), f"{component_name}: shape mismatch {W.shape} → {result.shape}"
|
||||
|
||||
|
||||
@pytest.mark.api
|
||||
def _test_component_repr(component, component_name="Component"):
|
||||
"""Component must have a non-empty repr."""
|
||||
repr_str = repr(component)
|
||||
assert isinstance(repr_str, str), (
|
||||
f"{component_name}: repr returned {type(repr_str)}"
|
||||
)
|
||||
assert len(repr_str) > 0, (
|
||||
f"{component_name}: repr is empty string"
|
||||
)
|
||||
assert isinstance(
|
||||
repr_str, str
|
||||
), f"{component_name}: repr returned {type(repr_str)}"
|
||||
assert len(repr_str) > 0, f"{component_name}: repr is empty string"
|
||||
# repr should contain the class name
|
||||
assert component_name in repr_str or component.__class__.__name__ in repr_str, (
|
||||
f"{component_name}: repr should contain class name. Got: {repr_str}"
|
||||
)
|
||||
assert (
|
||||
component_name in repr_str or component.__class__.__name__ in repr_str
|
||||
), f"{component_name}: repr should contain class name. Got: {repr_str}"
|
||||
|
||||
|
||||
@pytest.mark.api
|
||||
def _test_component_list_input(component, component_name="Component"):
|
||||
"""Component must handle Python list input (numpy converts it)."""
|
||||
result = component([1.5, 2.0, 3.0])
|
||||
assert isinstance(result, np.ndarray), (
|
||||
f"{component_name}: list input returned {type(result)}"
|
||||
)
|
||||
assert isinstance(
|
||||
result, np.ndarray
|
||||
), f"{component_name}: list input returned {type(result)}"
|
||||
|
||||
|
||||
@pytest.mark.api
|
||||
def _test_component_empty_input(component, component_name="Component"):
|
||||
"""Component must handle empty array input gracefully."""
|
||||
result = component(np.array([]))
|
||||
assert isinstance(result, np.ndarray), (
|
||||
f"{component_name}: empty input returned {type(result)}"
|
||||
)
|
||||
assert result.shape == (0,), (
|
||||
f"{component_name}: empty input should return empty array"
|
||||
)
|
||||
assert isinstance(
|
||||
result, np.ndarray
|
||||
), f"{component_name}: empty input returned {type(result)}"
|
||||
assert result.shape == (
|
||||
0,
|
||||
), f"{component_name}: empty input should return empty array"
|
||||
|
||||
@@ -10,20 +10,18 @@ seconds to complete. They are intended for CI/full test runs, not for
|
||||
fast feedback during development.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
|
||||
from beta_spectrum.components.fermi import FermiFunction
|
||||
from beta_spectrum.components.phase_space import PhaseSpace
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fermi function: random Z, A, W combinations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(
|
||||
Z=st.integers(min_value=1, max_value=92),
|
||||
A=st.integers(min_value=1, max_value=300),
|
||||
@@ -56,8 +54,7 @@ def test_fermi_monotonic_for_fixed_Z(Z: int):
|
||||
|
||||
diffs = np.diff(result)
|
||||
assert np.all(diffs <= 0), (
|
||||
f"Fermi function not monotonically decreasing for Z={Z}: "
|
||||
f"diffs = {diffs}"
|
||||
f"Fermi function not monotonically decreasing for Z={Z}: " f"diffs = {diffs}"
|
||||
)
|
||||
|
||||
|
||||
@@ -65,6 +62,7 @@ def test_fermi_monotonic_for_fixed_Z(Z: int):
|
||||
# Phase space: random W0, W combinations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given(
|
||||
W0=st.floats(min_value=2.0, max_value=10.0, allow_infinity=False, allow_nan=False),
|
||||
W=st.floats(min_value=1.01, max_value=5.0, allow_infinity=False, allow_nan=False),
|
||||
@@ -91,7 +89,9 @@ def test_phase_space_threshold_at_W1(W0: float):
|
||||
ps = PhaseSpace(W0=W0)
|
||||
result = ps(np.array([1.0]))
|
||||
|
||||
assert result[0] == 0.0, f"Phase space should be 0 at W=1 for W0={W0:.2f}, got {result[0]}"
|
||||
assert (
|
||||
result[0] == 0.0
|
||||
), f"Phase space should be 0 at W=1 for W0={W0:.2f}, got {result[0]}"
|
||||
|
||||
|
||||
@given(
|
||||
@@ -103,4 +103,6 @@ def test_phase_space_endpoint_at_W0(W0: float):
|
||||
ps = PhaseSpace(W0=W0)
|
||||
result = ps(np.array([W0]))
|
||||
|
||||
assert result[0] == 0.0, f"Phase space should be 0 at W=W0={W0:.2f}, got {result[0]}"
|
||||
assert (
|
||||
result[0] == 0.0
|
||||
), f"Phase space should be 0 at W=W0={W0:.2f}, got {result[0]}"
|
||||
|
||||
@@ -52,11 +52,11 @@ or only quality tests (`pytest -m quality`) for fast feedback during development
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property tests: positivity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.property
|
||||
def _test_positive_values(component, W, component_name="Component"):
|
||||
"""Component must return positive values at all W in the physical range.
|
||||
@@ -82,9 +82,9 @@ def _test_positive_values(component, W, component_name="Component"):
|
||||
f"W range: [{W_arr.min():.3f}, {W_arr.max():.3f}], "
|
||||
f"result range: [{np.nanmin(result):.6f}, {np.nanmax(result):.6f}]"
|
||||
)
|
||||
assert not np.any(np.isinf(result)), (
|
||||
f"{component_name}: Inf values found in output."
|
||||
)
|
||||
assert not np.any(
|
||||
np.isinf(result)
|
||||
), f"{component_name}: Inf values found in output."
|
||||
assert np.all(result > 0), (
|
||||
f"{component_name}: non-positive values found. "
|
||||
f"Min = {np.min(result):.10f} at W = {W_arr[np.argmin(result)][0]:.4f}"
|
||||
@@ -92,7 +92,9 @@ def _test_positive_values(component, W, component_name="Component"):
|
||||
|
||||
|
||||
@pytest.mark.property
|
||||
def _test_non_negative_values(component, W, component_name="Component", allow_zero=True):
|
||||
def _test_non_negative_values(
|
||||
component, W, component_name="Component", allow_zero=True
|
||||
):
|
||||
"""Component must return non-negative values (allows zero at physical boundaries).
|
||||
|
||||
Use this for components that can legitimately be zero (e.g., phase_space
|
||||
@@ -106,17 +108,16 @@ def _test_non_negative_values(component, W, component_name="Component", allow_ze
|
||||
W_arr = np.asarray(W, dtype=float)
|
||||
result = component(W_arr)
|
||||
|
||||
assert not np.any(np.isnan(result)), (
|
||||
f"{component_name}: NaN values found in output."
|
||||
)
|
||||
assert not np.any(np.isinf(result)), (
|
||||
f"{component_name}: Inf values found in output."
|
||||
)
|
||||
assert not np.any(
|
||||
np.isnan(result)
|
||||
), f"{component_name}: NaN values found in output."
|
||||
assert not np.any(
|
||||
np.isinf(result)
|
||||
), f"{component_name}: Inf values found in output."
|
||||
|
||||
if allow_zero:
|
||||
assert np.all(result >= 0), (
|
||||
f"{component_name}: negative values found. "
|
||||
f"Min = {np.min(result):.10f}"
|
||||
f"{component_name}: negative values found. " f"Min = {np.min(result):.10f}"
|
||||
)
|
||||
else:
|
||||
assert np.all(result > 0), (
|
||||
@@ -129,15 +130,16 @@ def _test_non_negative_values(component, W, component_name="Component", allow_ze
|
||||
# Property tests: shape preservation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.quality
|
||||
def _test_output_shape_preserved(component, W, component_name="Component"):
|
||||
"""Output shape must match input shape — no flattening or reshaping."""
|
||||
W_arr = np.asarray(W, dtype=float)
|
||||
result = component(W_arr)
|
||||
|
||||
assert isinstance(result, np.ndarray), (
|
||||
f"{component_name}: output is {type(result)}, expected np.ndarray"
|
||||
)
|
||||
assert isinstance(
|
||||
result, np.ndarray
|
||||
), f"{component_name}: output is {type(result)}, expected np.ndarray"
|
||||
assert result.shape == W_arr.shape, (
|
||||
f"{component_name}: shape mismatch. "
|
||||
f"Input {W_arr.shape} → Output {result.shape}"
|
||||
@@ -152,15 +154,14 @@ def _test_scalar_input_returns_array(component, W_scalar, component_name="Compon
|
||||
f"{component_name}: single-element input returned {type(result)}, "
|
||||
f"expected np.ndarray"
|
||||
)
|
||||
assert result.ndim == 1, (
|
||||
f"{component_name}: expected 1D output, got {result.ndim}D"
|
||||
)
|
||||
assert result.ndim == 1, f"{component_name}: expected 1D output, got {result.ndim}D"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property tests: numerical stability
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.quality
|
||||
def _test_no_nan_across_range(component, W, component_name="Component"):
|
||||
"""No NaN values across the entire energy range."""
|
||||
@@ -208,6 +209,7 @@ def _test_full_range_stability(component, W, component_name="Component"):
|
||||
# Property tests: type correctness
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.quality
|
||||
def _test_output_is_ndarray(component, W, component_name="Component"):
|
||||
"""Output must be numpy.ndarray for any valid input."""
|
||||
@@ -224,6 +226,7 @@ def _test_output_is_ndarray(component, W, component_name="Component"):
|
||||
# Parametrized test templates for quick component integration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_positive_test(component_class, test_params, component_name=None):
|
||||
"""Factory function to create a parametrized positivity test.
|
||||
|
||||
@@ -258,7 +261,7 @@ def make_positive_test(component_class, test_params, component_name=None):
|
||||
@pytest.mark.parametrize("W", W_values)
|
||||
def test_positive(component_params, W_arr):
|
||||
comp = component_class(**component_params)
|
||||
test_positive_values(comp, W_arr, component_name)
|
||||
_test_positive_values(comp, W_arr, component_name)
|
||||
|
||||
return test_positive
|
||||
|
||||
@@ -278,6 +281,6 @@ def make_stability_test(component_class, test_params, component_name=None):
|
||||
@pytest.mark.parametrize("W", W_values)
|
||||
def test_stability(component_params, W_arr):
|
||||
comp = component_class(**component_params)
|
||||
test_full_range_stability(comp, W_arr, component_name)
|
||||
_test_full_range_stability(comp, W_arr, component_name)
|
||||
|
||||
return test_stability
|
||||
|
||||
@@ -178,6 +178,7 @@ class TestDeclarativeDetectorResponse:
|
||||
|
||||
detector = BetaSpectrum.create_detector_from_config(config)
|
||||
|
||||
from beta_spectrum.components.detector_response import DetectorResponse
|
||||
assert isinstance(detector, DetectorResponse)
|
||||
assert detector.n_channels == 512
|
||||
assert detector._mode == "analytical"
|
||||
|
||||
@@ -13,7 +13,9 @@ except ImportError:
|
||||
DetectorResponse = None # type: ignore[misc, assignment]
|
||||
|
||||
|
||||
@pytest.mark.skipif(DetectorResponse is None, reason="DetectorResponse not yet implemented")
|
||||
@pytest.mark.skipif(
|
||||
DetectorResponse is None, reason="DetectorResponse not yet implemented"
|
||||
)
|
||||
class TestAnalyticalGaussianResponse:
|
||||
"""Test pure Gaussian detector response."""
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ class TestExchangePhysics:
|
||||
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]}"
|
||||
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):
|
||||
@@ -35,7 +37,9 @@ class TestExchangePhysics:
|
||||
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}"
|
||||
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):
|
||||
@@ -43,7 +47,9 @@ class TestExchangePhysics:
|
||||
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}"
|
||||
assert (
|
||||
abs(result[0] - 1.0) < 0.5
|
||||
), f"Exchange must approach unity at high energy: {result}"
|
||||
|
||||
|
||||
class TestExchangeErrorHandling:
|
||||
|
||||
@@ -75,8 +75,8 @@ class TestFermiPhysicalBehavior:
|
||||
"""
|
||||
ff = FermiFunction(Z=92, A=238)
|
||||
|
||||
W_low = np.array([1.01]) # Near threshold
|
||||
W_mid = np.array([3.0]) # Mid-range
|
||||
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]
|
||||
|
||||
@@ -660,9 +660,9 @@ class TestMultiBranchEdgeCases:
|
||||
W, _ = spectrum.get_energy_grid(config)
|
||||
|
||||
values = spectrum(W)
|
||||
assert np.all(np.isfinite(values)), (
|
||||
"High-Z multi-branch must not produce NaN/inf"
|
||||
)
|
||||
assert np.all(
|
||||
np.isfinite(values)
|
||||
), "High-Z multi-branch must not produce NaN/inf"
|
||||
|
||||
def test_no_branches_defaults_to_single(self):
|
||||
"""No branches list should default to single-branch mode."""
|
||||
@@ -727,9 +727,9 @@ class TestMultiBranchEdgeCases:
|
||||
contrib = universal_product * branch.intensity * bs[i]
|
||||
|
||||
# Branch contribution should not exceed total
|
||||
assert np.all(contrib <= total + 1e-10), (
|
||||
f"Branch {i} contribution exceeds total at some points"
|
||||
)
|
||||
assert np.all(
|
||||
contrib <= total + 1e-10
|
||||
), f"Branch {i} contribution exceeds total at some points"
|
||||
|
||||
# Branch contribution should be non-negative
|
||||
assert np.all(contrib >= 0), f"Branch {i} contribution has negative values"
|
||||
@@ -779,6 +779,6 @@ class TestMultiBranchEdgeCases:
|
||||
assert integral > 0, f"Branch {i} has zero integral"
|
||||
|
||||
# Sum of branch integrals should equal total
|
||||
assert np.isclose(sum(integrals), total_integral, rtol=1e-6), (
|
||||
"Sum of branch integrals should equal total integral"
|
||||
)
|
||||
assert np.isclose(
|
||||
sum(integrals), total_integral, rtol=1e-6
|
||||
), "Sum of branch integrals should equal total integral"
|
||||
|
||||
@@ -20,12 +20,12 @@ class TestNeutrinoMassPhaseSpaceEffects:
|
||||
result = ps(W_test)
|
||||
|
||||
# Should not produce NaN values
|
||||
assert not np.any(np.isnan(result)), (
|
||||
"Phase space with m_nu=0 should not produce NaN values"
|
||||
)
|
||||
assert np.all(result > 0), (
|
||||
"Phase space with m_nu=0 should be positive in physical range"
|
||||
)
|
||||
assert not np.any(
|
||||
np.isnan(result)
|
||||
), "Phase space with m_nu=0 should not produce NaN values"
|
||||
assert np.all(
|
||||
result > 0
|
||||
), "Phase space with m_nu=0 should be positive in physical range"
|
||||
|
||||
def test_phase_space_with_nonzero_neutrino_mass(self):
|
||||
"""Test that phase space calculation works with non-zero neutrino mass."""
|
||||
@@ -34,12 +34,12 @@ class TestNeutrinoMassPhaseSpaceEffects:
|
||||
result = ps(W_test)
|
||||
|
||||
# Should not produce NaN values for energies above threshold
|
||||
assert not np.any(np.isnan(result)), (
|
||||
"Phase space with m_nu>0 should not produce NaN values above threshold"
|
||||
)
|
||||
assert np.all(result > 0), (
|
||||
"Phase space with m_nu>0 should be positive above threshold"
|
||||
)
|
||||
assert not np.any(
|
||||
np.isnan(result)
|
||||
), "Phase space with m_nu>0 should not produce NaN values above threshold"
|
||||
assert np.all(
|
||||
result > 0
|
||||
), "Phase space with m_nu>0 should be positive above threshold"
|
||||
|
||||
def test_phase_space_with_neutrino_mass_near_endpoint(self):
|
||||
"""Test that phase space calculation handles energies near endpoint with neutrino mass."""
|
||||
@@ -48,18 +48,18 @@ class TestNeutrinoMassPhaseSpaceEffects:
|
||||
# Test energies where W0-W > m_nu (should be finite)
|
||||
W_above_threshold = np.array([4.8]) # W0-W = 0.2 > m_nu=0.1
|
||||
result_above = ps(W_above_threshold)
|
||||
assert not np.any(np.isnan(result_above)), (
|
||||
"Should produce finite value above threshold"
|
||||
)
|
||||
assert not np.any(
|
||||
np.isnan(result_above)
|
||||
), "Should produce finite value above threshold"
|
||||
|
||||
# Test energies where W0-W < m_nu (should be handled properly)
|
||||
# With our fix, this should return 0 instead of NaN
|
||||
W_below_threshold = np.array([4.95]) # W0-W = 0.05 < m_nu=0.1
|
||||
result_below = ps(W_below_threshold)
|
||||
# After our fix, NaN values should be replaced with 0
|
||||
assert np.all(result_below == 0.0) or np.all(np.isnan(result_below)), (
|
||||
"Should handle below-threshold energies properly (either 0 or NaN)"
|
||||
)
|
||||
assert np.all(result_below == 0.0) or np.all(
|
||||
np.isnan(result_below)
|
||||
), "Should handle below-threshold energies properly (either 0 or NaN)"
|
||||
|
||||
def test_spectrum_config_with_neutrino_mass(self):
|
||||
"""Test that SpectrumConfig accepts and stores neutrino mass parameter."""
|
||||
@@ -72,9 +72,9 @@ class TestNeutrinoMassPhaseSpaceEffects:
|
||||
e_step_MeV=0.001,
|
||||
neutrino_mass_MeV=0.01,
|
||||
)
|
||||
assert config.neutrino_mass_MeV == 0.01, (
|
||||
"SpectrumConfig should store neutrino mass parameter"
|
||||
)
|
||||
assert (
|
||||
config.neutrino_mass_MeV == 0.01
|
||||
), "SpectrumConfig should store neutrino mass parameter"
|
||||
|
||||
def test_beta_spectrum_with_neutrino_mass(self):
|
||||
"""Test that BetaSpectrum works with neutrino mass in configuration."""
|
||||
@@ -96,12 +96,12 @@ class TestNeutrinoMassPhaseSpaceEffects:
|
||||
result = spectrum(W)
|
||||
|
||||
# Should not produce NaN values
|
||||
assert not np.any(np.isnan(result)), (
|
||||
"BetaSpectrum with neutrino mass should not produce NaN values"
|
||||
)
|
||||
assert np.all(result >= 0), (
|
||||
"BetaSpectrum with neutrino mass should be non-negative"
|
||||
)
|
||||
assert not np.any(
|
||||
np.isnan(result)
|
||||
), "BetaSpectrum with neutrino mass should not produce NaN values"
|
||||
assert np.all(
|
||||
result >= 0
|
||||
), "BetaSpectrum with neutrino mass should be non-negative"
|
||||
|
||||
|
||||
class TestNeutrinoMassCLIIntegration:
|
||||
|
||||
@@ -74,9 +74,9 @@ class TestPhaseSpaceNeutrinoMass:
|
||||
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"
|
||||
)
|
||||
assert (
|
||||
val_massive < val_zero
|
||||
), "Massive neutrino must suppress spectrum near endpoint"
|
||||
|
||||
@pytest.mark.physics
|
||||
def test_massive_neutrino_below_threshold(self):
|
||||
|
||||
@@ -21,20 +21,24 @@ 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"
|
||||
|
||||
@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_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"
|
||||
assert (
|
||||
val_close < val_far
|
||||
), "Standard mode must show divergent behavior near endpoint"
|
||||
|
||||
|
||||
class TestRadiativeEnergyDependence:
|
||||
@@ -51,7 +55,9 @@ class TestRadiativeEnergyDependence:
|
||||
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}"
|
||||
assert (
|
||||
r_near <= r_mid
|
||||
), f"Correction factor should decrease near endpoint: {r_mid} → {r_near}"
|
||||
|
||||
|
||||
class TestRadiativeResummationSwitch:
|
||||
@@ -73,8 +79,12 @@ class TestRadiativeResummationSwitch:
|
||||
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}"
|
||||
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):
|
||||
@@ -87,9 +97,9 @@ class TestRadiativeResummationSwitch:
|
||||
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}"
|
||||
)
|
||||
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])
|
||||
@@ -121,7 +131,9 @@ class TestRadiativeZDependence:
|
||||
|
||||
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)}"
|
||||
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):
|
||||
@@ -129,7 +141,9 @@ class TestRadiativeZDependence:
|
||||
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]}"
|
||||
assert (
|
||||
0.9 < result[0] < 3.0
|
||||
), f"Z=92 correction should be reasonable: {result[0]}"
|
||||
|
||||
|
||||
class TestRadiativeAParameter:
|
||||
@@ -138,8 +152,12 @@ class TestRadiativeAParameter:
|
||||
@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)
|
||||
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], (
|
||||
|
||||
@@ -13,7 +13,9 @@ except ImportError:
|
||||
DetectorResponse = None # type: ignore[misc, assignment]
|
||||
|
||||
|
||||
@pytest.mark.skipif(DetectorResponse is None, reason="DetectorResponse not yet implemented")
|
||||
@pytest.mark.skipif(
|
||||
DetectorResponse is None, reason="DetectorResponse not yet implemented"
|
||||
)
|
||||
class TestFactoryMethods:
|
||||
"""Test convenience factory methods."""
|
||||
|
||||
@@ -56,7 +58,9 @@ class TestFactoryMethods:
|
||||
assert len(resp.calibration_energies) == 3
|
||||
|
||||
|
||||
@pytest.mark.skipif(DetectorResponse is None, reason="DetectorResponse not yet implemented")
|
||||
@pytest.mark.skipif(
|
||||
DetectorResponse is None, reason="DetectorResponse not yet implemented"
|
||||
)
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and error handling."""
|
||||
|
||||
|
||||
@@ -18,15 +18,20 @@ class TestExchangeAPI:
|
||||
result = ex(np.array([2.0]))
|
||||
assert isinstance(result, np.ndarray)
|
||||
|
||||
|
||||
class TestExchangeProperties:
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[2.0],
|
||||
np.linspace(1.01, 6.0, 50),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[2.0],
|
||||
np.linspace(1.01, 6.0, 50),
|
||||
],
|
||||
)
|
||||
def test_positive(self, W):
|
||||
ex = ExchangeCorrection(Z=92)
|
||||
from tests.common.test_property_tests import _test_positive_values
|
||||
|
||||
_test_positive_values(ex, W, component_name="ExchangeCorrection")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -35,13 +40,18 @@ class TestExchangeProperties:
|
||||
ex = ExchangeCorrection(Z=30)
|
||||
W = np.linspace(1.1, 3.0, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(ex, W, component_name="ExchangeCorrection")
|
||||
|
||||
@pytest.mark.quality
|
||||
@pytest.mark.parametrize("W", [
|
||||
np.linspace(1.01, 6.0, 50),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
np.linspace(1.01, 6.0, 50),
|
||||
],
|
||||
)
|
||||
def test_no_nan(self, W):
|
||||
ex = ExchangeCorrection(Z=92)
|
||||
from tests.common.test_property_tests import _test_no_nan_across_range
|
||||
|
||||
_test_no_nan_across_range(ex, W, component_name="ExchangeCorrection")
|
||||
|
||||
@@ -26,19 +26,24 @@ class TestFermiAPI:
|
||||
ff = FermiFunction(Z=20, A=40)
|
||||
assert "FermiFunction" in repr(ff)
|
||||
|
||||
|
||||
class TestFermiProperties:
|
||||
"""Shared property tests for FermiFunction."""
|
||||
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[1.5],
|
||||
[1.1, 2.0, 3.0],
|
||||
np.linspace(1.01, 5.0, 20),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[1.5],
|
||||
[1.1, 2.0, 3.0],
|
||||
np.linspace(1.01, 5.0, 20),
|
||||
],
|
||||
)
|
||||
def test_positive(self, W):
|
||||
"""F₀ must be positive everywhere — it's a probability enhancement factor."""
|
||||
ff = FermiFunction(Z=92, A=238) # Uranium — extreme case
|
||||
from tests.common.test_property_tests import _test_positive_values
|
||||
|
||||
_test_positive_values(ff, W, component_name="FermiFunction")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -48,17 +53,22 @@ class TestFermiProperties:
|
||||
ff = FermiFunction(Z=20, A=40)
|
||||
W = np.linspace(1.1, 3.0, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(ff, W, component_name="FermiFunction")
|
||||
|
||||
@pytest.mark.quality
|
||||
@pytest.mark.parametrize("W", [
|
||||
np.linspace(1.01, 6.0, 50),
|
||||
np.linspace(1.001, 10.0, 100),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
np.linspace(1.01, 6.0, 50),
|
||||
np.linspace(1.001, 10.0, 100),
|
||||
],
|
||||
)
|
||||
def test_no_nan(self, W):
|
||||
"""For very heavy nuclei, the loggamma implementation must not overflow."""
|
||||
ff = FermiFunction(Z=92, A=238)
|
||||
from tests.common.test_property_tests import _test_no_nan_across_range
|
||||
|
||||
_test_no_nan_across_range(ff, W, component_name="FermiFunction")
|
||||
|
||||
@pytest.mark.quality
|
||||
|
||||
@@ -23,15 +23,20 @@ class TestFiniteSizeL0API:
|
||||
fs = FiniteSizeL0(Z=20, A=40)
|
||||
assert "FiniteSizeL0" in repr(fs)
|
||||
|
||||
|
||||
class TestFiniteSizeL0Properties:
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[2.0],
|
||||
np.linspace(1.05, 6.0, 20),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[2.0],
|
||||
np.linspace(1.05, 6.0, 20),
|
||||
],
|
||||
)
|
||||
def test_positive(self, W):
|
||||
fs = FiniteSizeL0(Z=92, A=238)
|
||||
from tests.common.test_property_tests import _test_positive_values
|
||||
|
||||
_test_positive_values(fs, W, component_name="FiniteSizeL0")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -40,6 +45,7 @@ class TestFiniteSizeL0Properties:
|
||||
fs = FiniteSizeL0(Z=20, A=40)
|
||||
W = np.linspace(1.1, 3.0, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(fs, W, component_name="FiniteSizeL0")
|
||||
|
||||
|
||||
@@ -59,13 +65,17 @@ class TestChargeDistributionUAPI:
|
||||
|
||||
class TestChargeDistributionUProperties:
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[2.0],
|
||||
np.linspace(1.1, 5.0, 10),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[2.0],
|
||||
np.linspace(1.1, 5.0, 10),
|
||||
],
|
||||
)
|
||||
def test_non_negative(self, W):
|
||||
u = ChargeDistributionU(Z=20, A=40)
|
||||
from tests.common.test_property_tests import _test_non_negative_values
|
||||
|
||||
_test_non_negative_values(u, W, component_name="ChargeDistributionU")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -74,4 +84,5 @@ class TestChargeDistributionUProperties:
|
||||
u = ChargeDistributionU(Z=20, A=40)
|
||||
W = np.linspace(1.1, 3.0, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(u, W, component_name="ChargeDistributionU")
|
||||
|
||||
@@ -21,19 +21,24 @@ class TestPhaseSpaceAPI:
|
||||
result = ps(np.array([3.0]))
|
||||
assert isinstance(result, np.ndarray)
|
||||
|
||||
|
||||
class TestPhaseSpaceProperties:
|
||||
"""Shared property tests for PhaseSpace."""
|
||||
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[1.5],
|
||||
[1.1, 2.0, 3.0],
|
||||
np.linspace(1.01, 4.9, 20),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[1.5],
|
||||
[1.1, 2.0, 3.0],
|
||||
np.linspace(1.01, 4.9, 20),
|
||||
],
|
||||
)
|
||||
def test_non_negative(self, W):
|
||||
"""Phase space must be non-negative (zero at boundaries)."""
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
from tests.common.test_property_tests import _test_non_negative_values
|
||||
|
||||
_test_non_negative_values(ps, W, component_name="PhaseSpace")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -43,15 +48,20 @@ class TestPhaseSpaceProperties:
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
W = np.linspace(1.1, 4.9, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(ps, W, component_name="PhaseSpace")
|
||||
|
||||
@pytest.mark.quality
|
||||
@pytest.mark.parametrize("W", [
|
||||
np.linspace(1.01, 4.99, 50),
|
||||
np.linspace(1.001, 4.999, 100),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
np.linspace(1.01, 4.99, 50),
|
||||
np.linspace(1.001, 4.999, 100),
|
||||
],
|
||||
)
|
||||
def test_stability(self, W):
|
||||
"""Phase space must be stable across the full energy range."""
|
||||
ps = PhaseSpace(W0=5.0)
|
||||
from tests.common.test_property_tests import _test_full_range_stability
|
||||
|
||||
_test_full_range_stability(ps, W, component_name="PhaseSpace")
|
||||
|
||||
@@ -39,13 +39,17 @@ class TestRadiativeAPI:
|
||||
|
||||
class TestRadiativeProperties:
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[2.0],
|
||||
np.linspace(1.1, 4.9, 20),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[2.0],
|
||||
np.linspace(1.1, 4.9, 20),
|
||||
],
|
||||
)
|
||||
def test_positive(self, W):
|
||||
rc = RadiativeCorrection(W0=5.0)
|
||||
from tests.common.test_property_tests import _test_positive_values
|
||||
|
||||
_test_positive_values(rc, W, component_name="RadiativeCorrection")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -54,6 +58,7 @@ class TestRadiativeProperties:
|
||||
rc = RadiativeCorrection(W0=5.0)
|
||||
W = np.linspace(1.1, 4.9, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(rc, W, component_name="RadiativeCorrection")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -63,17 +68,22 @@ class TestRadiativeProperties:
|
||||
rc = RadiativeCorrection(W0=W0)
|
||||
W = np.linspace(1.1, min(9.9, W0 - 0.01), 20)
|
||||
from tests.common.test_property_tests import _test_no_nan_across_range
|
||||
|
||||
_test_no_nan_across_range(rc, W, component_name=f"RadiativeCorrection(W0={W0})")
|
||||
|
||||
@pytest.mark.quality
|
||||
@pytest.mark.parametrize("W", [
|
||||
np.linspace(1.0001, 4.9999, 200),
|
||||
np.linspace(1.001, 9.99, 100),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
np.linspace(1.0001, 4.9999, 200),
|
||||
np.linspace(1.001, 9.99, 100),
|
||||
],
|
||||
)
|
||||
def _test_full_range_stability(self, W):
|
||||
"""Entire energy range from threshold to endpoint must be free of NaN/inf."""
|
||||
rc = RadiativeCorrection(W0=5.0, Z=92, use_endpoint_resummation=True)
|
||||
from tests.common.test_property_tests import _test_full_range_stability
|
||||
|
||||
_test_full_range_stability(rc, W, component_name="RadiativeCorrection(Z=92)")
|
||||
|
||||
@pytest.mark.quality
|
||||
|
||||
@@ -26,16 +26,21 @@ class TestScreeningAPI:
|
||||
sc = ScreeningCorrection(ff)
|
||||
assert "Screening" in repr(sc)
|
||||
|
||||
|
||||
class TestScreeningProperties:
|
||||
@pytest.mark.property
|
||||
@pytest.mark.parametrize("W", [
|
||||
[2.0],
|
||||
np.linspace(1.02, 3.0, 20),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"W",
|
||||
[
|
||||
[2.0],
|
||||
np.linspace(1.02, 3.0, 20),
|
||||
],
|
||||
)
|
||||
def test_positive(self, W):
|
||||
ff = FermiFunction(Z=40, A=95)
|
||||
sc = ScreeningCorrection(ff)
|
||||
from tests.common.test_property_tests import _test_positive_values
|
||||
|
||||
_test_positive_values(sc, W, component_name="ScreeningCorrection")
|
||||
|
||||
@pytest.mark.quality
|
||||
@@ -45,4 +50,5 @@ class TestScreeningProperties:
|
||||
sc = ScreeningCorrection(ff)
|
||||
W = np.linspace(1.1, 3.0, n)
|
||||
from tests.common.test_property_tests import _test_output_shape_preserved
|
||||
|
||||
_test_output_shape_preserved(sc, W, component_name="ScreeningCorrection")
|
||||
|
||||
@@ -56,9 +56,9 @@ class TestNeutrinoMassPhaseSpace:
|
||||
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"
|
||||
)
|
||||
assert (
|
||||
val_massive < val_zero
|
||||
), "Massive neutrino must suppress spectrum near endpoint"
|
||||
|
||||
|
||||
class TestCLIArguments:
|
||||
|
||||
Reference in New Issue
Block a user