Files
radiacode/web/app/theoretical_bg.py
Jacquin Antoine c764a5c264 Dash web: crosshair, zoom/pan X, scale log/lin, continuum extraction, background resume
- Tooltip entier (intersect:false) + ligne verticale crosshair sur tous les graphes
- Zoom molette/pinch sur l'axe X, pan souris, limites clamped 30-3000 keV
- Toggle échelle log/linéaire onglet Background
- Extraction continuum détecteur (isotope peaks subtracted + Gaussian smoothing)
- Reprise snapshot précédent au démarrage capture_background.py
- Suppression refs "Théorique" et "Bruit capteur" de l'interface
- Plugin chartjs-plugin-zoom + hammerjs via CDN
- Fix Chart constructor spread operator

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 23:26:28 +02:00

74 lines
2.4 KiB
Python

"""
CsI(Tl) detector response continuum for Radiacode 103.
Models ONLY the detector's noise continuum. Photopeaks from environmental
isotopes depend on measurement location and are NOT included.
Auto-calibrated from measured background using smoothing spline (GCV)
when available. Falls back to a simple parametric model otherwise.
"""
import numpy as np
from app.config import ENERGY_OFFSET, ENERGY_SLOPE, NUM_CHANNELS
def _get_continuum_cps():
"""Try to load calibrated spline continuum from measured data."""
try:
from app.bg_calibration import load_or_calibrate
calibrated = load_or_calibrate()
if calibrated and "continuum_cps" in calibrated:
return np.array(calibrated["continuum_cps"])
except Exception:
pass
return None
def generate_continuum_only(cps: float = 6.0, live_time_s: float = 3600.0):
"""Detector response continuum only (no photopeaks, no noise)."""
channels = np.arange(NUM_CHANNELS, dtype=np.float64)
energy_axis = ENERGY_OFFSET + ENERGY_SLOPE * channels
total_counts = cps * live_time_s
# Try calibrated spline first
continuum_cps = _get_continuum_cps()
if continuum_cps is not None and len(continuum_cps) == NUM_CHANNELS:
# Scale calibrated CPS to match requested total counts
continuum = continuum_cps.copy()
if continuum.sum() > 0:
continuum *= total_counts / continuum.sum()
else:
# Fallback: simple parametric model
continuum = _fallback_continuum(energy_axis, total_counts)
return {
"energy_kev": [round(float(E), 2) for E in energy_axis],
"counts": [round(float(c), 1) for c in continuum],
"cps": round(cps, 2),
"live_time_s": round(live_time_s, 1),
}
def _fallback_continuum(energy_axis, total_counts):
"""Simple parametric fallback when no measured data available."""
E = energy_axis
# Asymmetric hump
hump_center, sigma_left, tail_decay_right = 110.0, 40.0, 100.0
left = np.exp(-0.5 * ((E - hump_center) / sigma_left) ** 2)
right = np.exp(-(E - hump_center) / tail_decay_right)
hump = np.where(E <= hump_center, left, right)
# Housing absorption
absorption = 1.0 * (1.0 - np.exp(-E / 20.0))
# Compton tail
compton = 0.5 / (np.maximum(E, 1.0) + 15.0) ** 1.3
continuum = (hump + compton) * absorption
if continuum.sum() > 0 and total_counts > 0:
continuum *= total_counts / continuum.sum()
return continuum