Files
beta_shape_pnpi/tests/physics/test_phase_space.py
T
unzhakov 4b08df3873 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)
2026-05-15 15:04:52 +03:00

100 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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"