Files
radiacode/web/static/js/background.js
Jacquin Antoine 75d271c696 Background réaliste CsI(Tl) + hybridation mesuré/synthétique + dashboard continuum
- Remplace le continuum exponentiel par un modèle réaliste CsI(Tl) dans
  l'entraînement (bosse asymétrique ~110 keV + queue Compton)
- Ajoute l'injection de background mesuré (70% mesuré / 30% synthétique)
  via --measured_background et MEASURED_BACKGROUND_PATH
- Ajoute l'endpoint /api/background/continuum et le toggle "Continuum CsI"
  sur le dashboard background
- Exclut le canal 1023 (overflow bin) de l'affichage web (NUM_CHANNELS=1023)
- Corrige le lissage Gaussien du background (normalisation locale aux bords)
- Met à jour README.md, CLAUDE.md, TUTORIEL.md, TOTO.md, vega_ml/README.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:14:00 +02:00

260 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let bgChart = null;
let bgReferenceData = null;
let bgTheoreticalData = null;
let bgContinuumData = null;
async function loadBgReference() {
try {
const resp = await fetch(`${API_BASE}/api/background/reference`);
if (!resp.ok) return;
bgReferenceData = await resp.json();
} catch {}
}
async function loadBgTheoretical(cps, liveTime) {
try {
const resp = await fetch(`${API_BASE}/api/background/theoretical?cps=${cps}&live_time_s=${liveTime}`);
if (!resp.ok) return;
bgTheoreticalData = await resp.json();
} catch {}
}
async function loadBgContinuum(cps, liveTime) {
try {
const resp = await fetch(`${API_BASE}/api/background/continuum?cps=${cps}&live_time_s=${liveTime}`);
if (!resp.ok) return;
bgContinuumData = await resp.json();
} catch {}
}
/**
* Gaussian kernel smoothing.
* Convolves the data with a Gaussian kernel of given sigma (in channels).
* Preserves peak shapes while removing statistical noise.
*/
function smoothGaussian(data, sigma) {
if (!data || data.length === 0) return data;
const kernelRadius = Math.ceil(sigma * 3);
const kernel = [];
for (let i = -kernelRadius; i <= kernelRadius; i++) {
kernel.push(Math.exp(-0.5 * (i / sigma) ** 2));
}
const result = new Array(data.length);
for (let i = 0; i < data.length; i++) {
let sum = 0;
let wSum = 0;
for (let k = -kernelRadius; k <= kernelRadius; k++) {
const idx = i + k;
if (idx < 0 || idx >= data.length) continue;
const w = kernel[k + kernelRadius];
sum += data[idx] * w;
wSum += w;
}
result[i] = wSum > 0 ? sum / wSum : 0;
}
return result;
}
async function refreshBackground() {
try {
const [infoResp, specResp] = await Promise.all([
fetch(`${API_BASE}/api/background`),
fetch(`${API_BASE}/api/background/spectrum`)
]);
if (!infoResp.ok || !specResp.ok) {
document.getElementById('bg-stats').innerHTML = '<p style="color:#888">Background non disponible</p>';
return;
}
const info = await infoResp.json();
const spec = await specResp.json();
// Stats
document.getElementById('bg-stats').innerHTML = `
<div class="bg-stat"><div class="bg-stat-value">${info.elapsed_hours.toFixed(1)}h</div><div class="bg-stat-label">Durée</div></div>
<div class="bg-stat"><div class="bg-stat-value">${info.live_time_s.toFixed(0)}s</div><div class="bg-stat-label">Live time</div></div>
<div class="bg-stat"><div class="bg-stat-value">${info.total_counts.toFixed(0)}</div><div class="bg-stat-label">Coups</div></div>
<div class="bg-stat"><div class="bg-stat-value">${info.cps.toFixed(2)}</div><div class="bg-stat-label">CPS</div></div>
`;
// Load theoretical curve on first load
if (!bgTheoreticalData && spec.live_time_s > 0) {
await loadBgTheoretical(info.cps || 6.0, spec.live_time_s);
}
// Load CsI(Tl) continuum on first load
if (!bgContinuumData && spec.live_time_s > 0) {
await loadBgContinuum(info.cps || 6.0, spec.live_time_s);
}
// Chart
updateBackgroundChart(spec);
// Peaks table
updatePeaksTable(info.top_peaks || []);
// Show/hide toggles
const refToggle = document.getElementById('show-bg-reference');
if (refToggle) refToggle.parentElement.style.display = spec.reference_available ? 'flex' : 'none';
} catch {}
}
function updateBackgroundChart(spec) {
const ctx = document.getElementById('background-chart').getContext('2d');
const showRef = document.getElementById('show-bg-reference')?.checked && bgReferenceData;
const showTheory = document.getElementById('show-bg-theoretical')?.checked && bgTheoreticalData;
const showSmooth = document.getElementById('show-bg-smooth')?.checked;
const showContinuum = document.getElementById('show-bg-continuum')?.checked && bgContinuumData;
const datasets = [{
label: 'Background (live)',
data: spec.counts,
borderColor: '#ff9800',
backgroundColor: 'rgba(255, 152, 0, 0.1)',
borderWidth: 1,
pointRadius: 0,
fill: true,
}];
if (showSmooth) {
// Smoothed version of live data — sigma=8 channels (~24 keV)
// Wide enough to remove noise, narrow enough to preserve the 100 keV peak
const smoothed = smoothGaussian(spec.counts, 8);
datasets.push({
label: 'Lissé',
data: smoothed,
borderColor: 'rgba(233, 30, 99, 0.9)',
backgroundColor: 'rgba(233, 30, 99, 0.05)',
borderWidth: 2,
pointRadius: 0,
fill: false,
});
}
if (showTheory) {
datasets.push({
label: 'Théorique',
data: bgTheoreticalData.counts,
borderColor: 'rgba(76, 175, 80, 0.7)',
backgroundColor: 'rgba(76, 175, 80, 0.05)',
borderWidth: 1.5,
pointRadius: 0,
fill: true,
borderDash: [6, 3],
});
}
if (showContinuum) {
datasets.push({
label: 'Continuum CsI(Tl)',
data: bgContinuumData.counts,
borderColor: 'rgba(156, 39, 176, 0.8)',
backgroundColor: 'rgba(156, 39, 176, 0.05)',
borderWidth: 2,
pointRadius: 0,
fill: false,
borderDash: [8, 4],
});
}
if (showRef) {
const scale = spec.live_time_s > 0 && bgReferenceData.live_time_s > 0
? spec.live_time_s / bgReferenceData.live_time_s
: 1;
datasets.push({
label: `Référence 24h (×${scale.toFixed(1)})`,
data: bgReferenceData.counts.map(c => c * scale),
borderColor: 'rgba(79, 195, 247, 0.8)',
backgroundColor: 'rgba(79, 195, 247, 0.08)',
borderWidth: 1,
pointRadius: 0,
fill: true,
borderDash: [4, 2],
});
}
const chartData = {
labels: spec.energy_kev,
datasets: datasets,
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#e0e0e0' } },
tooltip: {
callbacks: {
title: (items) => `${spec.energy_kev[items[0].dataIndex]} keV`,
label: (item) => `${item.dataset.label}: ${item.raw.toFixed(1)} counts`
}
}
},
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Énergie (keV)', color: '#888' },
ticks: { color: '#888', maxTicksLimit: 20 },
grid: { color: '#333' },
},
y: {
type: 'logarithmic',
title: { display: true, text: 'Comptages (log)', color: '#888' },
min: 0.9,
ticks: { color: '#888' },
grid: { color: '#333' },
}
}
};
if (bgChart) {
bgChart.data = chartData;
bgChart.options = options;
bgChart.update();
} else {
bgChart = new Chart(ctx, { type: 'line', data: chartData, options });
}
}
function updatePeaksTable(peaks) {
const container = document.getElementById('peaks-table');
if (!peaks || peaks.length === 0) {
container.innerHTML = '<p style="color:#888;text-align:center;padding:8px;">Pas assez de données pour identifier les pics</p>';
return;
}
let html = '<h3 style="margin-bottom:8px;color:#ff9800;">Pics détectés</h3>';
peaks.forEach(p => {
html += `<div class="peak-row">
<span style="color:#4fc3f7">${p.energy_kev.toFixed(1)} keV</span>
<span>${p.counts.toFixed(0)} cts</span>
</div>`;
});
container.innerHTML = html;
}
document.querySelector('[data-tab="background"]').addEventListener('click', () => {
refreshBackground();
loadBgReference();
});
// Toggle handlers
document.getElementById('show-bg-reference')?.addEventListener('change', () => refreshBackground());
document.getElementById('show-bg-theoretical')?.addEventListener('change', () => {
if (document.getElementById('show-bg-theoretical').checked && !bgTheoreticalData) {
loadBgTheoretical(6.0, 3600).then(() => refreshBackground());
} else {
refreshBackground();
}
});
document.getElementById('show-bg-continuum')?.addEventListener('change', () => {
if (document.getElementById('show-bg-continuum').checked && !bgContinuumData) {
const info = document.getElementById('bg-stats');
loadBgContinuum(6.0, 3600).then(() => refreshBackground());
} else {
refreshBackground();
}
});
document.getElementById('show-bg-smooth')?.addEventListener('change', () => refreshBackground());