viewof sigma = Inputs.range([0.3, 2.0], {
value: 1.0,
step: 0.02,
label: "trial σ"
})
viewof nEvents = Inputs.range([200, 5000], {
value: 1000,
step: 100,
label: "N"
})
viewof regenerate = Inputs.button("Новые псевдоданные")
viewof overlays = {
const panel = html`<div class="fit-overlays-inline">
<label>показать лучший фит <input name="showFit" type="checkbox"></label>
<label>истинное σ <input name="showTruth" type="checkbox"></label>
</div>`;
const fitInput = panel.querySelector('input[name="showFit"]');
const truthInput = panel.querySelector('input[name="showTruth"]');
const sync = () => {
panel.value = {
showFit: fitInput.checked,
showTruth: truthInput.checked
};
};
const notify = () => {
sync();
panel.dispatchEvent(new Event("input", {bubbles: true}));
};
fitInput.addEventListener("change", notify);
truthInput.addEventListener("change", notify);
sync();
return panel;
}
// Gaussian random number (Box-Muller)
function randn() {
const u = 1 - Math.random();
const v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Fixed x values
x = Array.from({length: 21}, (_, i) => -2.5 + 0.25 * i)
dx = 0.25
xMin = x[0] - dx / 2
xMax = x[x.length - 1] + dx / 2
binEdges = Array.from({length: x.length + 1}, (_, i) => xMin + i * dx)
// True sigma (hidden by default)
sigmaTrue = {
regenerate;
return 0.5 + Math.random();
}
// Generate events x~N(0, sigmaTrue) and fill histogram bins
function generateCountsGaussian(s, nTot) {
const counts = Array(x.length).fill(0);
let accepted = 0;
while (accepted < nTot) {
const xi = s * randn();
if (xi < xMin || xi >= xMax) continue;
const ibin = Math.floor((xi - xMin) / dx);
counts[Math.max(0, Math.min(x.length - 1, ibin))] += 1;
accepted += 1;
}
return counts;
}
counts = {
regenerate;
nEvents;
return generateCountsGaussian(sigmaTrue, nEvents);
}
nTotal = counts.reduce((sum, n) => sum + n, 0)
countMax = Math.max(1, ...counts)
data = x.map((xi, i) => {
const n = counts[i];
return {
x: xi,
n,
y: n / countMax,
err: Math.sqrt(Math.max(n, 1)) / countMax
};
})
gaussShape = (xx, s) => Math.exp(-(xx * xx) / (2 * s * s))
normCurve = (s) => {
const w = x.map((xi) => gaussShape(xi, s));
const wMax = Math.max(...w, 1e-12);
return x.map((xi, i) => ({x: xi, y: w[i] / wMax}));
}
function erfApprox(xx) {
const sign = xx < 0 ? -1 : 1;
const xAbs = Math.abs(xx);
const p = 0.3275911;
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const t = 1 / (1 + p * xAbs);
const poly = (((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t);
const y = 1 - poly * Math.exp(-(xAbs * xAbs));
return sign * y;
}
normalCdf = (z) => 0.5 * (1 + erfApprox(z / Math.SQRT2))
binProbabilities = (s) => {
const sSafe = Math.max(s, 1e-6);
const cdf = (xx) => normalCdf(xx / sSafe);
const norm = Math.max(cdf(xMax) - cdf(xMin), 1e-12);
return x.map((_, i) => (cdf(binEdges[i + 1]) - cdf(binEdges[i])) / norm);
}
modelCounts = (s) => {
const probs = binProbabilities(s);
return probs.map((p) => nTotal * p);
}
chi2of = (s) => {
const mu = modelCounts(s);
return counts.reduce((sum, n, i) => {
const m = Math.max(mu[i], 1e-12);
if (n > 0) return sum + 2 * (m - n + n * Math.log(n / m));
return sum + 2 * m;
}, 0);
}
chi2 = chi2of(sigma)
sigmaGrid = Array.from({length: 341}, (_, i) => 0.30 + 0.005 * i)
chi2Grid = sigmaGrid.map((s) => ({sigma: s, chi2: chi2of(s)}))
best = chi2Grid.reduce((a, b) => (a.chi2 < b.chi2 ? a : b))
sigmaFit = best.sigma
chi2min = best.chi2
ndf = Math.max(data.length - 1, 1)
dchi2 = chi2 - chi2min
trialCurve = normCurve(sigma)
fitCurve = normCurve(sigmaFit)
trueCurve = normCurve(sigmaTrue)
mutable history = []
historyReset = {
counts;
mutable history = [];
return null;
}
historyUpdate = {
const h = history;
const last = h.length > 0 ? h[h.length - 1] : null;
const point = {sigma, chi2};
if (!last || Math.abs(last.sigma - point.sigma) > 1e-9) {
mutable history = [...h, point];
}
return null;
}
mainPlot = Plot.plot({
width: 620,
height: 320,
x: {label: "x"},
y: {label: "y", domain: [-0.1, 1.2]},
grid: true,
marks: [
Plot.ruleX(data, {
x: "x",
y1: (d) => d.y - d.err,
y2: (d) => d.y + d.err,
stroke: "white",
strokeWidth: 1.5
}),
Plot.dot(data, {x: "x", y: "y", r: 3.5, fill: "white", stroke: "white"}),
Plot.line(trialCurve, {x: "x", y: "y", stroke: "#ff6b6b", strokeWidth: 2}),
...(overlays.showTruth ? [
Plot.line(trueCurve, {
x: "x",
y: "y",
stroke: "#8ec5ff",
strokeWidth: 2,
strokeOpacity: 0.85
})
] : []),
...(overlays.showFit ? [
Plot.line(fitCurve, {
x: "x",
y: "y",
stroke: "#7CFC00",
strokeWidth: 2,
strokeDasharray: "6,4"
})
] : [])
]
})
profilePlot = Plot.plot({
width: 280,
height: 200,
x: {label: "σ", domain: [0.3, 2.0]},
y: {label: "χ²", domain: [0, 50]},
grid: true,
marks: [
Plot.dot(history.slice(0, -1), {
x: "sigma",
y: "chi2",
fill: "gray",
r: 2.8
}),
Plot.dot([{sigma, chi2}], {
x: "sigma",
y: "chi2",
fill: "#ff6b6b",
r: 5
}),
...(overlays.showFit ? [
Plot.line(chi2Grid, {
x: "sigma",
y: "chi2",
stroke: "#8ec5ff",
strokeOpacity: 0.75
}),
Plot.dot([{sigma: sigmaFit, chi2: chi2min}], {
x: "sigma",
y: "chi2",
fill: "#7CFC00",
r: 5
})
] : [])
]
})
fitStyle = html`
<style>
.fit-widget .observablehq--inspect {
display: none !important;
}
.fit-widget .observablehq {
margin: 0.12rem 0;
}
.fit-widget label {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 1.03em;
}
.fit-widget label > span:first-child {
min-width: 11.2rem;
}
.fit-widget input[type="number"] {
width: 6.0rem !important;
}
.fit-widget input[type="range"] {
width: 210px !important;
accent-color: #ffb347;
background: transparent !important;
}
.fit-widget input[type="range"]::-webkit-slider-runnable-track {
height: 6px;
border-radius: 999px;
background: #46637b;
}
.fit-widget input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid #1f2e3a;
background: #ffb347;
margin-top: -4px;
}
.fit-widget input[type="range"]::-moz-range-track {
height: 6px;
border: none;
border-radius: 999px;
background: #46637b;
}
.fit-widget input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid #1f2e3a;
background: #ffb347;
}
.fit-widget .fit-overlays-inline {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: nowrap;
margin: 0.1rem 0 0.45rem;
}
.fit-widget .fit-overlays-inline label {
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin: 0;
white-space: nowrap;
}
.fit-widget .fit-overlays-inline input[type="checkbox"] {
margin: 0;
}
.fit-widget .fit-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 0.8rem;
align-items: start;
}
.fit-widget .fit-main figure,
.fit-widget .fit-side figure {
margin: 0;
}
.fit-widget .fit-summary {
margin-top: 0.45rem;
font-size: 0.78em;
line-height: 1.35;
}
.fit-widget .fit-summary-label {
font-weight: 700;
}
.fit-widget .fit-summary-choice {
color: #ff6b6b;
}
.fit-widget .fit-summary-fit {
color: #7CFC00;
}
.fit-widget .fit-summary-truth {
color: #8ec5ff;
}
@media (max-width: 980px) {
.fit-widget .fit-layout {
grid-template-columns: 1fr;
}
}
</style>
`
html`
<div>
${fitStyle}
<div class="fit-layout">
<div class="fit-main">
${mainPlot}
</div>
<div class="fit-side">
${profilePlot}
</div>
</div>
<div class="fit-summary">
<div>
<span class="fit-summary-label fit-summary-choice">Ваш выбор:</span>
N = ${nEvents},
σ = ${sigma.toFixed(2)},
χ²/ndf = ${(chi2 / ndf).toFixed(2)}
</div>
${overlays.showFit ? `
<div>
<span class="fit-summary-label fit-summary-fit">Лучший фит:</span>
σ_fit = ${sigmaFit.toFixed(3)},
χ²_min/ndf = ${(chi2min / ndf).toFixed(2)}
</div>` : ``}
${overlays.showTruth ? `
<div>
<span class="fit-summary-label fit-summary-truth">Истинное σ:</span>
σ_true = ${sigmaTrue.toFixed(3)}
</div>` : ``}
</div>
</div>
`