""" 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 (fits measured background) 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: 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