import os import numpy as np from pathlib import Path STATE_PATH = Path(os.environ.get("STATE_PATH", "/data/monitor_state.json")) CPS_LOG_PATH = Path(os.environ.get("CPS_LOG_PATH", "/data/cps_log.jsonl")) BACKGROUND_PATH = Path(os.environ.get("BACKGROUND_PATH", "/data/background_24h.npy")) BACKGROUND_SNAPSHOT_PATH = Path(os.environ.get("BACKGROUND_SNAPSHOT_PATH", "/data/background_snapshot.json")) LOG_DIR = Path(os.environ.get("LOG_DIR", "/logs")) ISOTOPE_INDEX_PATH = Path(os.environ.get("ISOTOPE_INDEX_PATH", "/models/vega_isotope_index.txt")) ENERGY_OFFSET = float(os.environ.get("ENERGY_CALIBRATION_OFFSET", "0.33")) ENERGY_SLOPE = float(os.environ.get("ENERGY_CALIBRATION_SLOPE", "2.97")) NUM_CHANNELS = 1023 # Last channel (1023) is overflow bin, excluded from display ENERGY_MIN = 30.0 # keV - detector lower limit ENERGY_MAX = 3000.0 # keV - detector upper limit (3 MeV) # CsI(Tl) non-linear response correction parameters # Matches the detector's non-proportional scintillation response CSI_NONLINEAR_ALPHA = float(os.environ.get("CSI_NONLINEAR_ALPHA", "0.37")) CSI_NONLINEAR_BETA = float(os.environ.get("CSI_NONLINEAR_BETA", "100.0")) def correct_csi_nonlinear(spectrum, num_channels=1023): """Apply inverse CsI(Tl) non-linear response correction. Remaps spectrum channels so peaks appear at their theoretical energy positions, correcting for the detector's non-proportional scintillation response that shifts low-energy peaks upward. """ alpha = CSI_NONLINEAR_ALPHA beta = CSI_NONLINEAR_BETA output_channels = np.arange(num_channels, dtype=np.float64) e_true = ENERGY_OFFSET + ENERGY_SLOPE * output_channels e_apparent = e_true * (1 + alpha * np.exp(-e_true / beta)) source_channels = (e_apparent - ENERGY_OFFSET) / ENERGY_SLOPE source_channels = np.clip(source_channels, 0, num_channels - 1.001) lower = np.floor(source_channels).astype(int) upper = np.minimum(lower + 1, num_channels - 1) frac = source_channels - lower return spectrum[lower] * (1 - frac) + spectrum[upper] * frac def energy_axis(): """Generate energy axis in keV from channel numbers, clipped to detector range.""" axis = [round(ENERGY_OFFSET + ENERGY_SLOPE * i, 2) for i in range(NUM_CHANNELS)] return [e for e in axis if ENERGY_MIN <= e <= ENERGY_MAX] def energy_mask(): """Return boolean mask of channels within detector energy range.""" return [ENERGY_MIN <= ENERGY_OFFSET + ENERGY_SLOPE * i <= ENERGY_MAX for i in range(NUM_CHANNELS)] def clip_to_range(arr): """Clip array to detector energy range using energy mask.""" mask = energy_mask() return [arr[i] for i in range(len(arr)) if mask[i]]