- 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>
74 lines
2.4 KiB
Python
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 |