Add massive neutrino support and improve CSV/CLI output

- Add neutrino mass parameter to SpectrumConfig and CLI interface
- Update PhaseSpace component to handle neutrino mass effects properly
- Update CSV headers to show basic endpoint and all transition types
- Add --csv-verbosity option to CLI with 'full' and 'concise' options
- Fix comparison plots to properly show linear scale variations
- Add comprehensive tests for neutrino mass functionality
This commit is contained in:
2026-07-02 10:33:28 +03:00
parent 406fd57bcd
commit 876e1e3ab8
48 changed files with 5213 additions and 97 deletions
+127
View File
@@ -0,0 +1,127 @@
"""
Physics tests for neutrino mass effects on beta spectrum.
Following TDD principles: write failing tests first, then ensure implementation fixes them.
"""
import numpy as np
from beta_spectrum.components.phase_space import PhaseSpace
from beta_spectrum.spectrum import SpectrumConfig, BetaSpectrum
class TestNeutrinoMassPhaseSpaceEffects:
"""Test neutrino mass effects on phase space calculation."""
def test_phase_space_with_zero_neutrino_mass(self):
"""Test that phase space calculation works with zero neutrino mass."""
ps = PhaseSpace(W0=5.0, transition_type="A", m_nu=0.0)
W_test = np.array([2.0, 3.0, 4.0])
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"
)
def test_phase_space_with_nonzero_neutrino_mass(self):
"""Test that phase space calculation works with non-zero neutrino mass."""
ps = PhaseSpace(W0=5.0, transition_type="A", m_nu=0.1)
W_test = np.array([2.0, 3.0, 4.0]) # W0-W = 3.0, 2.0, 1.0 > m_nu=0.1
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"
)
def test_phase_space_with_neutrino_mass_near_endpoint(self):
"""Test that phase space calculation handles energies near endpoint with neutrino mass."""
ps = PhaseSpace(W0=5.0, transition_type="A", m_nu=0.1)
# 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"
)
# 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)"
)
def test_spectrum_config_with_neutrino_mass(self):
"""Test that SpectrumConfig accepts and stores neutrino mass parameter."""
config = SpectrumConfig(
Z_parent=43,
Z_daughter=44,
A_number=99,
endpoint_MeV=0.2111,
transition_type="F2",
e_step_MeV=0.001,
neutrino_mass_MeV=0.01,
)
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."""
config = SpectrumConfig(
Z_parent=43,
Z_daughter=44,
A_number=99,
endpoint_MeV=0.2111,
transition_type="F2",
e_step_MeV=0.001,
neutrino_mass_MeV=0.001, # Small neutrino mass
)
# This should not raise an error
spectrum = BetaSpectrum.from_config(config)
W, kinetic_MeV = spectrum.get_energy_grid(config)
# Calculate spectrum
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"
)
class TestNeutrinoMassCLIIntegration:
"""Test CLI integration with neutrino mass parameter."""
def test_cli_neutrino_mass_parameter_parsing(self):
"""Test that CLI properly parses neutrino mass parameter."""
from beta_spectrum.cli import _build_parser
parser = _build_parser()
args = parser.parse_args(["--nuclide", "Tc99", "--neutrino-mass", "0.01"])
assert hasattr(args, "neutrino_mass"), "CLI should have neutrino_mass argument"
assert args.neutrino_mass == 0.01, "CLI should parse neutrino_mass correctly"
def test_cli_neutrino_mass_default_value(self):
"""Test that CLI neutrino mass parameter defaults to 0.0."""
from beta_spectrum.cli import _build_parser
parser = _build_parser()
args = parser.parse_args(["--nuclide", "Tc99"])
assert args.neutrino_mass == 0.0, "CLI neutrino_mass default should be 0.0"
+6 -8
View File
@@ -74,15 +74,15 @@ 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):
"""With m_nu > 0, below the effective threshold (W₀−W < m_nu), sqrt gives NaN.
"""With m_nu > 0, below the effective threshold (W₀−W < m_nu), the spectrum should be 0.
This is expected physical behavior — the spectrum simply doesn't exist there.
This is the correct physical behavior — the spectrum doesn't exist below the kinematic threshold.
"""
ps = PhaseSpace(W0=5.0, m_nu=0.1)
@@ -94,6 +94,4 @@ class TestPhaseSpaceNeutrinoMass:
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"
assert result_below[0] == 0.0, "Below kinematic threshold, spectrum should be 0"