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>
127 lines
4.4 KiB
JavaScript
127 lines
4.4 KiB
JavaScript
let cpsChart = null;
|
|
|
|
async function loadCps(hours = 24) {
|
|
// Highlight active button
|
|
document.querySelectorAll('.controls button').forEach(b => b.style.opacity = '0.6');
|
|
const activeBtn = [...document.querySelectorAll('.controls button')].find(
|
|
b => b.textContent.includes(hours <= 24 ? `${hours}h` : `${hours/24}j`) || (hours === 168 && b.textContent === '7j')
|
|
);
|
|
if (activeBtn) activeBtn.style.opacity = '1';
|
|
|
|
try {
|
|
const resp = await fetch(`${API_BASE}/api/cps/timeline?hours=${hours}`);
|
|
if (!resp.ok) return;
|
|
const data = await resp.json();
|
|
|
|
if (!data.data_points || data.data_points.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const labels = data.data_points.map(d => d.ts);
|
|
const cpsValues = data.data_points.map(d => d.cps);
|
|
|
|
updateCpsChart(labels, cpsValues);
|
|
} catch {}
|
|
}
|
|
|
|
function updateCpsChart(labels, values) {
|
|
const ctx = document.getElementById('cps-chart').getContext('2d');
|
|
|
|
const chartData = {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'CPS',
|
|
data: values,
|
|
borderColor: '#4caf50',
|
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
|
borderWidth: 1.5,
|
|
pointRadius: 0,
|
|
fill: true,
|
|
tension: 0.3,
|
|
}]
|
|
};
|
|
|
|
const panRange = cpsChart?._panRange;
|
|
const xMin = panRange ? panRange[0] : labels[0];
|
|
const xMax = panRange ? panRange[1] : labels[labels.length - 1];
|
|
const options = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: 'index', intersect: false },
|
|
plugins: {
|
|
legend: { labels: { color: '#e0e0e0' } },
|
|
tooltip: {
|
|
enabled: true,
|
|
mode: 'index',
|
|
intersect: false,
|
|
filter: (item) => item.raw != null,
|
|
callbacks: {
|
|
title: (items) => {
|
|
const d = new Date(items[0].parsed.x / 1000);
|
|
return d.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
},
|
|
label: (item) => `${item.dataset.label}: ${item.raw.toFixed(2)}`
|
|
}
|
|
},
|
|
zoom: {
|
|
zoom: {
|
|
wheel: { enabled: true },
|
|
pinch: { enabled: true },
|
|
drag: { enabled: false },
|
|
mode: 'x',
|
|
onZoomComplete: () => {
|
|
document.getElementById('reset-zoom-cps').style.display = 'inline-block';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
min: xMin,
|
|
max: xMax,
|
|
time: {
|
|
tooltipFormat: 'dd/MM HH:mm',
|
|
displayFormats: { minute: 'HH:mm', hour: 'HH:mm', day: 'dd/MM' }
|
|
},
|
|
title: { display: true, text: 'Temps', color: '#888' },
|
|
ticks: { color: '#888', maxTicksLimit: 12 },
|
|
grid: { color: '#333' },
|
|
},
|
|
y: {
|
|
title: { display: true, text: 'CPS', color: '#888' },
|
|
ticks: { color: '#888' },
|
|
grid: { color: '#333' },
|
|
beginAtZero: true,
|
|
}
|
|
}
|
|
};
|
|
|
|
if (cpsChart) {
|
|
cpsChart.data = chartData;
|
|
cpsChart.options = options;
|
|
cpsChart.update();
|
|
} else {
|
|
// Chart.js needs the date adapter for time axis
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js';
|
|
script.onload = () => {
|
|
cpsChart = new Chart(ctx, { type: 'line', data: chartData, ...options });
|
|
enablePan(cpsChart, 'reset-zoom-cps', labels[0], labels[labels.length - 1]);
|
|
};
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
|
|
// Reset zoom — restore full time range
|
|
document.getElementById('reset-zoom-cps')?.addEventListener('click', () => {
|
|
if (cpsChart) {
|
|
cpsChart.resetZoom();
|
|
delete cpsChart._panRange;
|
|
const labels = cpsChart.data.labels;
|
|
cpsChart.options.scales.x.min = labels[0];
|
|
cpsChart.options.scales.x.max = labels[labels.length - 1];
|
|
cpsChart.update();
|
|
document.getElementById('reset-zoom-cps').style.display = 'none';
|
|
}
|
|
}); |