Phase 1: Removed 6 duplicate old test files (fermi, finite_size, screening, exchange, radiative, phase_space) — their tests are now in physics/ and quality/ using shared helpers from common/. Phase 2: Migrated remaining 7 old test files: - test_spectrum.py → integration/test_spectrum_pipeline.py (integration tests) - test_fitter.py → physics/test_fitter.py (fit quality tests) - test_cw_extractor.py → physics/test_cw_extractor.py (extraction accuracy) - test_multi_branch.py → physics/test_multi_branch.py (branching physics) - test_detector_response.py → physics/ + quality/ (split into physics + API) - test_nuclear_data.py → quality/test_nuclear_data.py (parsing/API) - test_logging_utils.py → quality/test_logging_utils.py (API tests) Removed 12 old flat files, created 4 new files. Total: 251 tests passing (6 skipped), down from 230 → 251 (net +21 from better coverage, not duplicates). Test structure: - tests/common/ — shared property/API helpers (eliminates ~18 duplicates) - tests/physics/ — 10 files, unique physics behavior tests - tests/quality/ — 8 files, API contract + parsing tests - tests/integration/ — 1 file, end-to-end pipeline tests
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""
|
|
API / quality tests for DetectorResponse.
|
|
|
|
Tests factory methods, repr, error handling.
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from beta_spectrum.components.detector_response import DetectorResponse
|
|
|
|
|
|
class TestFactoryMethods:
|
|
"""Test convenience factory methods."""
|
|
|
|
def test_from_gaussian_params(self):
|
|
"""from_gaussian_params should create correct DetectorResponse."""
|
|
resp = DetectorResponse.from_gaussian_params(
|
|
channel_energy_range=(0.0, 5.0),
|
|
n_channels=1024,
|
|
sigma_a=0.005,
|
|
sigma_b=0.01,
|
|
tail_fraction=0.2,
|
|
tau=0.01,
|
|
model="gaussian_tail",
|
|
fano_factor=0.12,
|
|
)
|
|
|
|
assert resp.n_channels == 1024
|
|
assert np.isclose(resp.channel_energies[0], 0.0)
|
|
assert np.isclose(resp.channel_energies[-1], 5.0)
|
|
assert resp._mode == "analytical"
|
|
assert resp.tail_fraction == 0.2
|
|
|
|
def test_from_mc_simulation(self):
|
|
"""from_mc_simulation should create tabulated DetectorResponse."""
|
|
channel_energies = np.linspace(0.0, 5.0, 512)
|
|
calib_energies = np.array([1.0, 2.0, 3.0])
|
|
response_matrix = np.zeros((3, 512))
|
|
for i, e in enumerate(calib_energies):
|
|
idx = np.argmin(np.abs(channel_energies - e))
|
|
response_matrix[i, idx] = 1.0
|
|
|
|
resp = DetectorResponse.from_mc_simulation(
|
|
channel_energies=channel_energies,
|
|
response_matrix=response_matrix,
|
|
calibration_energies=calib_energies,
|
|
)
|
|
|
|
assert resp._mode == "tabulated"
|
|
assert resp.n_channels == 512
|
|
assert len(resp.calibration_energies) == 3
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Test edge cases and error handling."""
|
|
|
|
def test_repr_analytical(self):
|
|
"""__repr__ should show analytical mode info."""
|
|
channel_energies = np.linspace(0.0, 5.0, 100)
|
|
resp = DetectorResponse(channel_energies, model="gaussian", sigma_a=0.01)
|
|
repr_str = repr(resp)
|
|
assert "analytical" in repr_str
|
|
assert "gaussian" in repr_str
|
|
|
|
def test_repr_tabulated(self):
|
|
"""__repr__ should show tabulated mode info."""
|
|
channel_energies = np.linspace(0.0, 5.0, 100)
|
|
calib_energies = np.array([1.0, 2.0])
|
|
response_matrix = np.zeros((2, 100))
|
|
resp = DetectorResponse(
|
|
channel_energies=channel_energies,
|
|
response_matrix=response_matrix,
|
|
calibration_energies=calib_energies,
|
|
)
|
|
repr_str = repr(resp)
|
|
assert "tabulated" in repr_str
|
|
|
|
def test_invalid_model(self):
|
|
"""Should reject unknown model names."""
|
|
channel_energies = np.linspace(0.0, 5.0, 100)
|
|
resp = DetectorResponse(channel_energies, model="unknown_model")
|
|
try:
|
|
resp.get_response(2.0)
|
|
assert False, "Should have raised ValueError"
|
|
except ValueError as e:
|
|
assert "Unknown model" in str(e)
|
|
|
|
def test_convolve_mismatched_lengths(self):
|
|
"""Should reject mismatched W and spectrum_values lengths."""
|
|
channel_energies = np.linspace(0.0, 5.0, 100)
|
|
resp = DetectorResponse(channel_energies, model="gaussian")
|
|
W = np.linspace(0.0, 5.0, 100)
|
|
spectrum = np.zeros(50)
|
|
try:
|
|
resp.convolve(W, spectrum)
|
|
assert False, "Should have raised ValueError"
|
|
except ValueError:
|
|
pass
|
|
|
|
def test_non_1d_input(self):
|
|
"""Should reject non-1D inputs."""
|
|
channel_energies = np.linspace(0.0, 5.0, 100)
|
|
resp = DetectorResponse(channel_energies, model="gaussian")
|
|
W = np.array([[0.0, 5.0]])
|
|
spectrum = np.array([[1.0, 1.0]])
|
|
try:
|
|
resp.convolve(W, spectrum)
|
|
assert False, "Should have raised ValueError"
|
|
except ValueError:
|
|
pass
|
|
|
|
def test_tabulated_validation_wrong_columns(self):
|
|
"""Should reject response matrix with wrong column count."""
|
|
channel_energies = np.linspace(0.0, 5.0, 100)
|
|
calib_energies = np.array([1.0, 2.0])
|
|
response_matrix = np.zeros((2, 200))
|
|
|
|
try:
|
|
DetectorResponse(
|
|
channel_energies=channel_energies,
|
|
response_matrix=response_matrix,
|
|
calibration_energies=calib_energies,
|
|
)
|
|
assert False, "Should have raised ValueError"
|
|
except ValueError:
|
|
pass
|