feat: add raw binary spectrum reader and data quality analysis

- exp_data/raw_reader.py: read binary MCA spectra (128-byte header,
  int32 counts), subtract cumulative spectra, Am-241 calibration
- exp_data/spectrum.py: allow negative counts for difference spectra
- scripts/analyze_raw.py: analyze 236 individual runs from cumulative
  data, compute rates and endpoint drift
- Am-241 calibration: E = -0.96 + 0.451 * ch (CAM1),
  E = 0.75 + 0.442 * ch (CAM2)
- Results: stable rate ~2M counts/run, stable endpoint ~374 keV
  (smeared Q-value), spike at runs 214-219 (acquisition glitch)
This commit is contained in:
2026-05-17 22:55:52 +03:00
parent 3c0a43ad4d
commit 5f70e3a15a
243 changed files with 690 additions and 4 deletions
+13 -4
View File
@@ -58,15 +58,24 @@ class ExpSpectrum:
"must have the same shape"
)
assert np.all(self.energies >= 0), "Energies must be non-negative"
assert np.all(self.counts >= 0), "Counts must be non-negative"
# Difference spectra (cumulative subtraction) 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.
# This is normal and expected — errors propagate correctly.
if self.errors is None:
# Poisson statistics: σ = √N (handle zero counts gracefully)
self.errors = np.sqrt(np.maximum(self.counts, 1.0))
# Poisson statistics: σ = √N for positive counts
pos_mask = self.counts >= 0
errors = np.zeros_like(self.counts)
errors[pos_mask] = np.sqrt(self.counts[pos_mask])
neg_mask = ~pos_mask
if neg_mask.any():
errors[neg_mask] = np.sqrt(np.abs(self.counts[neg_mask]))
self.errors = errors
else:
self.errors = np.asarray(self.errors, dtype=np.float64)
assert self.errors.shape == self.counts.shape
assert np.all(self.errors > 0), "Errors must be positive"
@property
def n_channels(self) -> int: