Fix: CsI(Tl) non-linear response correction + detector calibration overhaul
Root cause of Am-241 misidentification: the Radiacode 103's CsI(Tl) crystal shifts low-energy peaks upward (59.5 keV → 71.6 keV for Am-241) due to non-proportional scintillation response. The model was trained on theoretical peak positions and couldn't match the shifted real peaks. Changes: - Add inverse CsI(Tl) non-linear correction to inference pipeline (radiacode_monitor.py, web/config.py, test_detection.py) E_apparent = E_true * (1 + 0.37 * exp(-E_true/100)) Corrects channel mapping so peaks appear at theoretical energies - Fix energy calibration: DetectorConfig now uses E = 0.33 + 2.97*ch with 1023 channels, matching the real detector (was energy_min=20, skip_first_channel=True, different channel width) - Add K-escape peaks for CsI(Tl) iodine X-ray escape (E - 28.5 keV) - Add asymmetric peak shapes for low-energy tails (< 200 keV) - Add log1p normalization in dataset and inference (replaces max-norm) - Add background-subtracted training mode (subtract_background flag) - Add low-signal augmentation (0.01-5 Bq activities, 30-300s durations) - Update docker-compose.yml: batch_size=32, duration=30-300s, CSI_NONLINEAR_ALPHA/BETA env vars for detect and web - Web dashboard: apply CsI correction to displayed spectra - Various UI fixes (Chart.js width, zoom/pan, isotope lines) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@ -49,14 +49,14 @@ class IsotopeSource:
|
||||
@dataclass
|
||||
class SpectrumConfig:
|
||||
"""Configuration for a single spectrum generation."""
|
||||
|
||||
|
||||
# Time parameters
|
||||
duration_seconds: float = 60.0
|
||||
time_interval_seconds: float = 1.0 # Each row in the spectrogram
|
||||
|
||||
|
||||
# Sources to include
|
||||
sources: List[IsotopeSource] = field(default_factory=list)
|
||||
|
||||
|
||||
# Background options
|
||||
include_background: bool = True
|
||||
background_cps: float = 5.0
|
||||
@ -64,18 +64,25 @@ class SpectrumConfig:
|
||||
include_radon: bool = True
|
||||
include_thorium: bool = True
|
||||
measured_background_path: Optional[str] = None
|
||||
|
||||
|
||||
# Background subtraction simulation
|
||||
# When True, generates a second independent background realization
|
||||
# and subtracts it from the spectrum, then clips negatives to 0.
|
||||
# This simulates what happens at inference time (measured bg subtraction).
|
||||
subtract_background: bool = False
|
||||
|
||||
# Detector configuration
|
||||
detector_name: str = "radiacode_103"
|
||||
|
||||
|
||||
# Noise options
|
||||
apply_poisson: bool = True
|
||||
apply_electronic: bool = False
|
||||
electronic_noise_sigma: float = 0.5
|
||||
|
||||
# Normalization
|
||||
|
||||
# Normalization — "log1p" preserves relative signal levels,
|
||||
# works well after background subtraction where many channels are ~0.
|
||||
normalize: bool = True
|
||||
normalization_method: str = "max" # max, sum, log, sqrt
|
||||
normalization_method: str = "log1p" # max, sum, log, sqrt, log1p
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -272,7 +279,7 @@ class SpectrumGenerator:
|
||||
all_source_isotopes.extend(src_iso)
|
||||
all_background_isotopes.extend(bg_iso)
|
||||
|
||||
# Apply noise
|
||||
# Apply noise before any subtraction (Poisson noise on raw counts)
|
||||
if config.apply_poisson:
|
||||
spectrum = apply_poisson_noise(spectrum)
|
||||
|
||||
@ -282,6 +289,24 @@ class SpectrumGenerator:
|
||||
config.electronic_noise_sigma
|
||||
)
|
||||
|
||||
# Simulate background subtraction (matches inference pipeline)
|
||||
if config.subtract_background and config.include_background:
|
||||
# Generate an independent background realization
|
||||
bg_spectrum2, _ = generate_environmental_background(
|
||||
self.energy_bins,
|
||||
config.duration_seconds,
|
||||
background_cps=config.background_cps,
|
||||
include_k40=config.include_k40,
|
||||
include_radon=config.include_radon,
|
||||
include_thorium=config.include_thorium,
|
||||
detector_config=self.detector_config,
|
||||
measured_background_path=config.measured_background_path,
|
||||
)
|
||||
if config.apply_poisson:
|
||||
bg_spectrum2 = apply_poisson_noise(bg_spectrum2)
|
||||
# Subtract and clip — same as inference: net = clip(rate - bg_rate, 0, inf)
|
||||
spectrum = np.maximum(spectrum - bg_spectrum2, 0)
|
||||
|
||||
# Normalize if requested
|
||||
if config.normalize:
|
||||
spectrum = normalize_spectrum(spectrum, config.normalization_method)
|
||||
|
||||
Reference in New Issue
Block a user