""" Detector Configuration Module Contains configuration parameters for Radiacode gamma spectrometers and other detector settings. Energy calibration matches the real Radiacode 103: E(keV) = 0.33 + 2.97 * channel_index Uses 1023 channels (channel 1023 is overflow, excluded). """ from dataclasses import dataclass from typing import Dict import numpy as np @dataclass class DetectorConfig: """Configuration for a gamma spectrometer detector.""" name: str # Energy calibration: E = calibration_offset + calibration_slope * channel # Must match the real detector calibration used in inference. calibration_offset_kev: float = 0.33 calibration_slope_kev: float = 2.97 # Number of usable channels (1023 for Radiacode, channel 1023 is overflow) num_channels: int = 1023 # FWHM at 662 keV (Cs-137 reference) as fraction fwhm_at_662: float = 0.084 # 8.4% fwhm_uncertainty: float = 0.003 # ±0.3% # Detector crystal type crystal_type: str = "CsI(Tl)" # Sensitivity: counts per second at 1 μSv/h for Cs-137 sensitivity_cps_per_usvh: float = 30.0 # Detector volume in cm³ detector_volume_cm3: float = 1.0 def get_energy_bins(self) -> np.ndarray: """Get array of energy bin centers (keV) matching the real detector calibration.""" channels = np.arange(self.num_channels, dtype=np.float64) return self.calibration_offset_kev + self.calibration_slope_kev * channels def get_fwhm_at_energy(self, energy_kev: float) -> float: """ Calculate FWHM at a given energy. For scintillators, FWHM scales approximately as sqrt(E). FWHM(E) = FWHM_662 * sqrt(E/662) """ return self.fwhm_at_662 * np.sqrt(energy_kev / 662.0) * 662.0 def get_sigma_at_energy(self, energy_kev: float) -> float: """Get Gaussian sigma at a given energy.""" fwhm = self.get_fwhm_at_energy(energy_kev) return fwhm / 2.355 def energy_to_channel(self, energy_kev: float) -> int: """Convert energy in keV to channel index.""" channel = int((energy_kev - self.calibration_offset_kev) / self.calibration_slope_kev) return max(0, min(self.num_channels - 1, channel)) def channel_to_energy(self, channel: int) -> float: """Convert channel index to energy in keV.""" return self.calibration_offset_kev + self.calibration_slope_kev * channel # Pre-defined configurations for Radiacode devices RADIACODE_CONFIGS: Dict[str, DetectorConfig] = { "radiacode_101": DetectorConfig( name="Radiacode 101", fwhm_at_662=0.095, # 9.5% fwhm_uncertainty=0.004, crystal_type="CsI(Tl)", sensitivity_cps_per_usvh=30.0, detector_volume_cm3=1.0, ), "radiacode_102": DetectorConfig( name="Radiacode 102", fwhm_at_662=0.095, # 9.5% fwhm_uncertainty=0.004, crystal_type="CsI(Tl)", sensitivity_cps_per_usvh=30.0, detector_volume_cm3=1.0, ), "radiacode_103": DetectorConfig( name="Radiacode 103", fwhm_at_662=0.084, # 8.4% fwhm_uncertainty=0.003, crystal_type="CsI(Tl)", sensitivity_cps_per_usvh=30.0, detector_volume_cm3=1.0, ), "radiacode_103g": DetectorConfig( name="Radiacode 103G", fwhm_at_662=0.074, # 7.4% (GAGG crystal) fwhm_uncertainty=0.003, crystal_type="GAGG(Ce)", sensitivity_cps_per_usvh=40.0, detector_volume_cm3=1.0, ), "radiacode_110": DetectorConfig( name="Radiacode 110", fwhm_at_662=0.084, # 8.4% fwhm_uncertainty=0.003, crystal_type="CsI(Tl)", sensitivity_cps_per_usvh=77.0, detector_volume_cm3=2.5, ), } def get_default_config() -> DetectorConfig: """Get the default detector configuration (Radiacode 103).""" return RADIACODE_CONFIGS["radiacode_103"]