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
+55
View File
@@ -0,0 +1,55 @@
"""Shared fixtures for exp_data tests."""
from __future__ import annotations
import numpy as np
import pytest
from exp_data.spectrum import ExpSpectrum
from exp_data.calibration import CalibrationResult
@pytest.fixture
def synthetic_spectrum():
"""Create a synthetic ExpSpectrum (4096 channels)."""
n_channels = 4096
energies = np.arange(n_channels, dtype=np.float64)
counts = np.exp(-energies / 500) * 1000 + np.random.poisson(5, n_channels)
return ExpSpectrum(
energies=energies,
counts=counts,
source="Tc99",
run_id="synthetic",
)
@pytest.fixture
def calibration_result():
"""Create a synthetic CalibrationResult (linear, E = 0.4 + 0.1*ch)."""
coeffs = np.array([0.1, 0.4]) # polyfit order: [slope, intercept]
cov = np.eye(2) * 1e-6
return CalibrationResult(
coefficients=coeffs,
order=1,
chi2_per_dof=0.5,
n_points=2,
covariance=cov,
)
@pytest.fixture
def full_spectrum():
"""Create a full ExpSpectrum with all fields populated."""
return ExpSpectrum(
energies=np.arange(4096, dtype=np.float64),
counts=np.exp(-np.arange(4096) / 500) * 1000 + np.random.poisson(5, 4096),
source="Tc99",
run_id="run_042",
date="2025-01-15",
dead_time=0.05,
live_time=3420.0,
metadata={
"calibration": {"type": "linear", "coefficients": [0.1, 0.4]},
"livetime": {"fraction": 0.95, "method": "pulser"},
},
)