feat: experimental data calibration pipeline with ROOT I/O

- Fix chi2 calculation bug in calibration.py (remove incorrect
  coeffs[::-1] reversal in np.polyval)
- Fix broken beta_spectrum/cli.py (remove orphaned detector response
  add_argument calls and unused imports)
- Add ExpSpectrum.apply_calibration() and EnergyCalibrator.from_am241_spectrum()
- Add LivetimeDetermination class for pulser-based livetime
- Add SpectrumPipeline orchestration (read→subtract→calibrate→livetime→validate)
- Add ROOT I/O with PyROOT primary + uproot fallback
- Add bs_exp CLI with process, livetime, export subcommands
- Add exp_calibration notebook with pre-calibration visualization
- Add comprehensive tests (73 passing)
- Add bs_exp console script and [root] extra to pyproject.toml
- Remove obsolete detector_response module and unused notebooks
This commit is contained in:
2026-05-21 16:01:25 +03:00
parent caa4a77ebd
commit 5c2b12c0e6
27 changed files with 3065 additions and 773 deletions
+16 -8
View File
@@ -26,12 +26,13 @@ class TestExpSpectrumBasic:
expected = np.sqrt([10.0, 20.0, 30.0])
np.testing.assert_allclose(spectrum.errors, expected)
def test_zero_counts_get_error_one(self):
def test_zero_counts_get_zero_error(self):
"""Zero counts produce zero error (not sqrt(1))."""
spectrum = ExpSpectrum(
energies=np.array([0.0, 1.0]),
counts=np.array([0.0, 100.0]),
)
assert spectrum.errors[0] == 1.0 # sqrt(1) for zero counts
assert spectrum.errors[0] == 0.0 # sqrt(0) = 0
def test_shape_mismatch_raises(self):
with pytest.raises(AssertionError):
@@ -47,12 +48,19 @@ class TestExpSpectrumBasic:
counts=np.array([10.0, 20.0]),
)
def test_negative_counts_raises(self):
with pytest.raises(AssertionError):
ExpSpectrum(
energies=np.array([0.0, 1.0]),
counts=np.array([10.0, -5.0]),
)
def test_negative_counts_allowed_for_difference_spectra(self):
"""Negative counts are allowed — they arise in cumulative subtraction.
Difference spectra can have negative counts in channels where the true
count is zero and Poisson noise causes the earlier cumulative to slightly
exceed the later one.
"""
spectrum = ExpSpectrum(
energies=np.array([0.0, 1.0]),
counts=np.array([10.0, -5.0]),
)
# Should not raise; errors use abs(counts) for negative values
assert spectrum.errors[1] == np.sqrt(5.0)
def test_total_counts(self):
spectrum = ExpSpectrum(