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>
This commit is contained in:
Jacquin Antoine
2026-05-19 18:14:00 +02:00
parent 1e0c1a5ea5
commit 75d271c696
17 changed files with 917 additions and 224 deletions

View File

@ -4,8 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Radiacode 103 — Dashboard</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/style.css?v=2">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js"></script>
</head>
<body>
<header>
@ -27,11 +28,17 @@
<main>
<section id="tab-spectrum" class="tab-content active">
<div class="chart-container">
<button class="exit-fullscreen-btn" title="Sortir du plein écran">&#x2715;</button>
<canvas id="spectrum-chart"></canvas>
</div>
<div class="controls">
<label><input type="checkbox" id="show-difference"> Background soustrait</label>
<label><input type="checkbox" id="log-scale"> Echelle log</label>
<label><input type="checkbox" id="log-scale" checked> Echelle log</label>
<label><input type="checkbox" id="show-isotope-lines"> Raies isotopiques</label>
<label id="lines-detected-label" style="display:none"><input type="checkbox" id="lines-detected-only" checked> Détectés uniquement</label>
<label><input type="checkbox" id="show-bg-overlay"> Overlay background</label>
<button id="download-csv" class="btn-small">CSV</button>
<button id="fullscreen-btn" class="btn-small" title="Plein écran">&#x26F6;</button>
</div>
<div id="isotopes-table"></div>
</section>
@ -42,7 +49,15 @@
<section id="tab-background" class="tab-content">
<div class="bg-stats" id="bg-stats"></div>
<div class="chart-header">
<label style="font-size:0.85em;color:#888;display:flex;align-items:center;gap:4px"><input type="checkbox" id="show-bg-smooth" checked> Lissé</label>
<label style="font-size:0.85em;color:#888;display:flex;align-items:center;gap:4px"><input type="checkbox" id="show-bg-theoretical"> Théorique</label>
<label style="font-size:0.85em;color:#888;display:flex;align-items:center;gap:4px"><input type="checkbox" id="show-bg-continuum"> Continuum CsI</label>
<label style="display:none;font-size:0.85em;color:#888"><input type="checkbox" id="show-bg-reference"> Ref 24h</label>
<button class="btn-small fullscreen-btn" title="Plein écran">&#x26F6;</button>
</div>
<div class="chart-container">
<button class="exit-fullscreen-btn" title="Sortir du plein écran">&#x2715;</button>
<canvas id="background-chart"></canvas>
</div>
<div id="peaks-table"></div>
@ -54,17 +69,20 @@
<button onclick="loadCps(6)">6h</button>
<button onclick="loadCps(24)">24h</button>
<button onclick="loadCps(168)">7j</button>
<button class="btn-small fullscreen-btn" title="Plein écran">&#x26F6;</button>
</div>
<div class="chart-container">
<button class="exit-fullscreen-btn" title="Sortir du plein écran">&#x2715;</button>
<canvas id="cps-chart"></canvas>
</div>
</section>
</main>
<script src="/static/js/app.js"></script>
<script src="/static/js/spectrum.js"></script>
<script src="/static/js/history.js"></script>
<script src="/static/js/background.js"></script>
<script src="/static/js/cps.js"></script>
<script src="/static/js/isotope_lines.js?v=2"></script>
<script src="/static/js/spectrum.js?v=2"></script>
<script src="/static/js/history.js?v=2"></script>
<script src="/static/js/background.js?v=2"></script>
<script src="/static/js/cps.js?v=2"></script>
<script src="/static/js/app.js?v=2"></script>
</body>
</html>

View File

@ -1,4 +1,60 @@
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 {
@ -23,28 +79,105 @@ async function refreshBackground() {
<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 chartData = {
labels: spec.energy_kev,
datasets: [{
label: 'Background',
data: spec.counts,
borderColor: '#ff9800',
backgroundColor: 'rgba(255, 152, 0, 0.1)',
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 = {
@ -55,7 +188,7 @@ function updateBackgroundChart(spec) {
tooltip: {
callbacks: {
title: (items) => `${spec.energy_kev[items[0].dataIndex]} keV`,
label: (item) => `${item.raw.toFixed(1)} counts`
label: (item) => `${item.dataset.label}: ${item.raw.toFixed(1)} counts`
}
}
},
@ -67,7 +200,9 @@ function updateBackgroundChart(spec) {
grid: { color: '#333' },
},
y: {
title: { display: true, text: 'Comptages', color: '#888' },
type: 'logarithmic',
title: { display: true, text: 'Comptages (log)', color: '#888' },
min: 0.9,
ticks: { color: '#888' },
grid: { color: '#333' },
}
@ -100,4 +235,26 @@ function updatePeaksTable(peaks) {
container.innerHTML = html;
}
document.querySelector('[data-tab="background"]').addEventListener('click', refreshBackground);
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());