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:
@ -128,19 +128,21 @@ def generate_training_batch(
|
||||
num_samples: int,
|
||||
output_dir: Path,
|
||||
detector_name: str = "radiacode_103",
|
||||
duration_range: tuple = (60, 300),
|
||||
duration_range: tuple = (30, 300),
|
||||
activity_range: tuple = (1.0, 100.0),
|
||||
single_isotope_fraction: float = 0.4,
|
||||
dual_isotope_fraction: float = 0.3,
|
||||
multi_isotope_fraction: float = 0.2,
|
||||
single_isotope_fraction: float = 0.3,
|
||||
dual_isotope_fraction: float = 0.2,
|
||||
multi_isotope_fraction: float = 0.15,
|
||||
background_only_fraction: float = 0.1,
|
||||
low_signal_fraction: float = 0.15,
|
||||
subtracted_fraction: float = 0.1,
|
||||
save_png: bool = False,
|
||||
random_seed: int = None,
|
||||
measured_background_path: str = None,
|
||||
) -> list:
|
||||
"""
|
||||
Generate a batch of training samples with various configurations.
|
||||
|
||||
|
||||
Args:
|
||||
num_samples: Total number of samples to generate
|
||||
output_dir: Output directory for spectra and labels
|
||||
@ -151,9 +153,11 @@ def generate_training_batch(
|
||||
dual_isotope_fraction: Fraction of two-isotope samples
|
||||
multi_isotope_fraction: Fraction of 3+ isotope samples
|
||||
background_only_fraction: Fraction of background-only samples
|
||||
low_signal_fraction: Fraction of low-activity samples (0.01-5 Bq)
|
||||
subtracted_fraction: Fraction of background-subtracted samples
|
||||
save_png: Whether to also save PNG images
|
||||
random_seed: Random seed for reproducibility
|
||||
|
||||
|
||||
Returns:
|
||||
List of generated spectra
|
||||
"""
|
||||
@ -181,11 +185,13 @@ def generate_training_batch(
|
||||
n_dual = int(num_samples * dual_isotope_fraction)
|
||||
n_multi = int(num_samples * multi_isotope_fraction)
|
||||
n_background = int(num_samples * background_only_fraction)
|
||||
|
||||
n_low_signal = int(num_samples * low_signal_fraction)
|
||||
n_subtracted = int(num_samples * subtracted_fraction)
|
||||
|
||||
# Adjust to ensure we hit exactly num_samples
|
||||
remaining = num_samples - (n_single + n_dual + n_multi + n_background)
|
||||
remaining = num_samples - (n_single + n_dual + n_multi + n_background + n_low_signal + n_subtracted)
|
||||
n_single += remaining
|
||||
|
||||
|
||||
total_generated = 0
|
||||
|
||||
print(f"\nGenerating {num_samples} synthetic spectra:")
|
||||
@ -193,6 +199,8 @@ def generate_training_batch(
|
||||
print(f" - Dual isotope: {n_dual}")
|
||||
print(f" - Multi isotope (3+): {n_multi}")
|
||||
print(f" - Background only: {n_background}")
|
||||
print(f" - Low signal (0.01-5 Bq): {n_low_signal}")
|
||||
print(f" - Background-subtracted: {n_subtracted}")
|
||||
print()
|
||||
|
||||
sample_num = 0
|
||||
@ -314,6 +322,77 @@ def generate_training_batch(
|
||||
|
||||
sample_num += 1
|
||||
|
||||
# Generate low-signal samples (weak sources, 0.01-5 Bq)
|
||||
print("Generating low-signal samples...")
|
||||
for i in range(n_low_signal):
|
||||
isotope = np.random.choice(isotope_pool)
|
||||
activity = np.random.uniform(0.01, 5.0)
|
||||
duration = np.random.uniform(*duration_range)
|
||||
|
||||
spectrum = generate_single_isotope_sample(
|
||||
generator,
|
||||
isotope,
|
||||
activity,
|
||||
duration,
|
||||
detector_name=detector_name,
|
||||
include_background=True,
|
||||
measured_background_path=measured_background_path,
|
||||
)
|
||||
|
||||
save_spectrum(
|
||||
spectrum,
|
||||
spectra_dir,
|
||||
save_image=True,
|
||||
image_format='npy'
|
||||
)
|
||||
del spectrum
|
||||
|
||||
sample_num += 1
|
||||
|
||||
if sample_num % 100 == 0:
|
||||
print(f" Generated {sample_num}/{num_samples} samples...")
|
||||
|
||||
# Generate background-subtracted samples (simulates inference pipeline)
|
||||
print("Generating background-subtracted samples...")
|
||||
for i in range(n_subtracted):
|
||||
num_iso = np.random.choice([1, 2, 3], p=[0.5, 0.3, 0.2])
|
||||
isotopes = np.random.choice(isotope_pool, size=num_iso, replace=False)
|
||||
activities = [np.random.uniform(0.1, 50.0) for _ in range(num_iso)]
|
||||
duration = np.random.uniform(*duration_range)
|
||||
|
||||
sources = [
|
||||
IsotopeSource(
|
||||
isotope_name=name,
|
||||
activity_bq=activity,
|
||||
include_daughters=True
|
||||
)
|
||||
for name, activity in zip(isotopes, activities)
|
||||
]
|
||||
|
||||
config = SpectrumConfig(
|
||||
duration_seconds=duration,
|
||||
sources=sources,
|
||||
include_background=True,
|
||||
subtract_background=True,
|
||||
detector_name=detector_name,
|
||||
measured_background_path=measured_background_path,
|
||||
)
|
||||
|
||||
spectrum = generator.generate_spectrum(config)
|
||||
|
||||
save_spectrum(
|
||||
spectrum,
|
||||
spectra_dir,
|
||||
save_image=True,
|
||||
image_format='npy'
|
||||
)
|
||||
del spectrum
|
||||
|
||||
sample_num += 1
|
||||
|
||||
if sample_num % 100 == 0:
|
||||
print(f" Generated {sample_num}/{num_samples} samples...")
|
||||
|
||||
total_generated = sample_num
|
||||
print(f"\nGenerated {total_generated} samples total")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user