oscExposureInput = Inputs.range([300, 100000], {value: 2500, step: 100, label: "N₀"})
oscATrueInput = Inputs.range([0.00, 0.20], {value: 0.085, step: 0.005, label: "A₀"})
oscDmTrueInput = Inputs.range([1.6, 3.4], {value: 2.50, step: 0.05, label: "Δm²₀"})
oscNBinsInput = Inputs.range([8, 80], {value: 18, step: 1, label: "Nbins"})
oscRegenerateInput = Inputs.button("Новая стат", {value: 0, reduce: () => Date.now()})
oscExposure = Generators.input(oscExposureInput)
oscATrue = Generators.input(oscATrueInput)
oscDmTrue = Generators.input(oscDmTrueInput)
oscNBins = Generators.input(oscNBinsInput)
oscRegenerate = Generators.input(oscRegenerateInput)
oscBaselineKm = 1.6
oscBins = Array.from({length: oscNBins}, (_, i) => {
const e1 = 1.8 + i * (6.2 / oscNBins);
const e2 = 1.8 + (i + 1) * (6.2 / oscNBins);
return {i, e1, e2, e: 0.5 * (e1 + e2), width: e2 - e1};
})
oscFluxComponents = [
{fraction: 0.58, coeffs: [4.367, -4.577, 2.100, -0.5294, 0.06186, -0.002777]}, // 235U, Huber
{fraction: 0.07, coeffs: [0.4833, 0.1927, -0.1283, -0.006762, 0.002233, -0.0001536]}, // 238U, Mueller
{fraction: 0.30, coeffs: [4.757, -5.392, 2.563, -0.6596, 0.07820, -0.003536]}, // 239Pu, Huber
{fraction: 0.05, coeffs: [2.990, -2.882, 1.278, -0.3343, 0.03905, -0.001754]} // 241Pu, Huber
]
oscIsotopeFlux = (E, coeffs) => {
let poly = 0;
let power = 1;
for (const a of coeffs) {
poly += a * power;
power *= E;
}
return Math.exp(poly);
}
oscFlux = E => oscFluxComponents.reduce(
(sum, c) => sum + c.fraction * oscIsotopeFlux(E, c.coeffs),
0
)
oscCrossSection = E => {
const ee = E - 1.293;
const pe = Math.sqrt(Math.max(0, ee * ee - 0.511 * 0.511));
return Math.max(0, ee * pe);
}
oscSurvival = (Emev, A, dmMilli) => {
const dm2 = dmMilli * 1e-3;
const phase = 1.267 * dm2 * oscBaselineKm / (Emev / 1000);
const s = Math.sin(phase);
return Math.max(1e-4, 1 - A * s * s);
}
oscBaseTotal = oscBins.reduce((sum, b) => sum + oscFlux(b.e) * oscCrossSection(b.e) * b.width, 0)
oscExpected = (A, dmMilli) => {
const scale = oscExposure / oscBaseTotal;
return oscBins.map(b => scale * oscFlux(b.e) * oscCrossSection(b.e) * oscSurvival(b.e, A, dmMilli) * b.width);
}
oscRandn = () => {
const u1 = Math.max(Math.random(), 1e-12);
const u2 = Math.random();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
oscPoisson = lambda => {
if (lambda <= 0) return 0;
if (lambda > 80) return Math.max(0, Math.round(lambda + Math.sqrt(lambda) * oscRandn()));
const limit = Math.exp(-lambda);
let k = 0;
let product = 1;
do {
++k;
product *= Math.random();
} while (product > limit);
return k - 1;
}
oscData = {
oscRegenerate;
const mu = oscExpected(oscATrue, oscDmTrue);
return oscBins.map((b, i) => ({...b, n: oscPoisson(mu[i]), muTrue: mu[i]}));
}
oscLogLike = (A, dmMilli) => {
const mu = oscExpected(A, dmMilli);
let ll = 0;
for (let i = 0; i < oscData.length; ++i) {
const lambda = Math.max(mu[i], 1e-9);
ll += oscData[i].n * Math.log(lambda) - lambda;
}
return ll;
}
oscAGrid = Array.from({length: 61}, (_, i) => i * 0.20 / 60)
oscDmGrid = Array.from({length: 61}, (_, i) => 1.6 + i * (3.4 - 1.6) / 60)
oscFit = {
const rows = [];
let best = {A: 0, dm: 0, ll: -Infinity};
for (const A of oscAGrid) {
for (const dm of oscDmGrid) {
const ll = oscLogLike(A, dm);
rows.push({A, dm, ll});
if (ll > best.ll) best = {A, dm, ll};
}
}
for (const row of rows) {
row.q = -2 * (row.ll - best.ll);
row.relative = Math.exp(-0.5 * Math.min(row.q, 20));
}
const profileA = oscAGrid.map(A => ({
A,
q: Math.min(...rows.filter(row => Math.abs(row.A - A) < 1e-12).map(row => row.q))
}));
const profileDm = oscDmGrid.map(dm => ({
dm,
q: Math.min(...rows.filter(row => Math.abs(row.dm - dm) < 1e-12).map(row => row.q))
}));
return {rows, best, profileA, profileDm};
}
oscRegion68 = oscFit.rows.filter(row => row.q <= 2.30)
oscRegion95 = oscFit.rows.filter(row => row.q <= 6.18)
oscObservedTotal = oscData.reduce((sum, row) => sum + row.n, 0)
oscMuNoOsc = oscExpected(0, oscDmTrue)
oscMuBest = oscExpected(oscFit.best.A, oscFit.best.dm)
oscSpectrumRows = oscData.map((row, i) => ({...row, muNoOsc: oscMuNoOsc[i], muBest: oscMuBest[i]}))
oscMapPlot = Plot.plot({
width: 700,
height: 430,
marginLeft: 64,
marginBottom: 50,
x: {label: "Δm² × 10³, эВ²", domain: [1.6, 3.4], grid: true},
y: {label: "A = sin² 2θ", domain: [0, 0.20], grid: true},
color: {scheme: "magma", domain: [0, 1]},
marks: [
Plot.dot(oscFit.rows, {x: "dm", y: "A", fill: "relative", r: 3.4}),
Plot.dot(oscRegion95, {x: "dm", y: "A", fill: "#ffffff", fillOpacity: 0.14, r: 3.6}),
Plot.dot(oscRegion68, {x: "dm", y: "A", fill: "#ffffff", fillOpacity: 0.55, r: 3.9}),
Plot.dot([{dm: oscDmTrue, A: oscATrue}], {x: "dm", y: "A", r: 8, fill: "#00d084", stroke: "#111", strokeWidth: 1.5}),
Plot.dot([{dm: oscFit.best.dm, A: oscFit.best.A}], {x: "dm", y: "A", r: 8, fill: "#ffb347", stroke: "#111", strokeWidth: 1.5})
]
})
oscSpectrumPlot = Plot.plot({
width: 700,
height: 205,
marginLeft: 66,
marginBottom: 30,
x: {label: "энергия, МэВ", domain: [1.8, 8.0], grid: true},
y: {label: "события в бине", grid: true},
marks: [
Plot.rectY(oscSpectrumRows, {x1: "e1", x2: "e2", y: "n", fill: "#d9d9d9", fillOpacity: 0.75}),
Plot.line(oscSpectrumRows, {x: "e", y: "muNoOsc", stroke: "#8fd3ff", strokeWidth: 2.4, strokeDasharray: "6,4"}),
Plot.line(oscSpectrumRows, {x: "e", y: "muTrue", stroke: "#00d084", strokeWidth: 2.5, strokeDasharray: "5,4"}),
Plot.line(oscSpectrumRows, {x: "e", y: "muBest", stroke: "#ffb347", strokeWidth: 3})
]
})
oscProfileAPlot = Plot.plot({
width: 700,
height: 205,
marginLeft: 66,
marginBottom: 30,
x: {label: "A = sin² 2θ", domain: [0, 0.20], grid: true},
y: {label: "q_p(A)", domain: [0, 9], grid: true},
marks: [
Plot.line(oscFit.profileA, {x: "A", y: "q", stroke: "#ffb347", strokeWidth: 3}),
Plot.ruleY([1], {stroke: "#999", strokeDasharray: "5,5"}),
Plot.ruleX([oscATrue], {stroke: "#00d084", strokeDasharray: "5,4"}),
Plot.ruleX([oscFit.best.A], {stroke: "#ffb347"})
]
})
oscProfileDmPlot = Plot.plot({
width: 700,
height: 205,
marginLeft: 66,
marginBottom: 30,
x: {label: "Δm² × 10³, эВ²", domain: [1.6, 3.4], grid: true},
y: {label: "q_p(Δm²)", domain: [0, 9], grid: true},
marks: [
Plot.line(oscFit.profileDm, {x: "dm", y: "q", stroke: "#4ea1ff", strokeWidth: 3}),
Plot.ruleY([1], {stroke: "#999", strokeDasharray: "5,5"}),
Plot.ruleX([oscDmTrue], {stroke: "#00d084", strokeDasharray: "5,4"}),
Plot.ruleX([oscFit.best.dm], {stroke: "#ffb347"})
]
})
html`
<style>
.osc-widget input[type="number"] {
min-width: 6.8rem !important;
width: 6.8rem !important;
}
.osc-widget input[type="range"] {
min-width: 17rem;
}
.osc-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 0.75rem;
align-items: start;
justify-items: center;
}
.osc-controls {
display: grid;
grid-template-columns: 1fr;
gap: 0.12rem;
width: 520px;
margin-bottom: 0.35rem;
}
.osc-controls form,
.osc-controls label {
margin: 0 !important;
}
.osc-controls button {
font-size: 0.82em;
padding: 0.15rem 0.45rem;
}
.osc-right {
display: flex;
flex-direction: column;
gap: 0.03rem;
}
</style>
<div class="clt-panel" style="margin-top:0.3rem;">
<div class="osc-layout">
<div>
<div class="osc-controls">
${oscExposureInput}
${oscATrueInput}
${oscDmTrueInput}
${oscNBinsInput}
${oscRegenerateInput}
</div>
${oscMapPlot}
</div>
<div class="osc-right">
${oscSpectrumPlot}
${oscProfileAPlot}
${oscProfileDmPlot}
</div>
</div>
<div class="clt-summary" style="font-size:0.66em; line-height:1.08; margin-top:0.02rem;">
<span>зелёная — генерация</span>
<span>оранжевая — максимум</span>
<span>N = ${oscObservedTotal}</span>
<span>Nbins = ${oscNBins}</span>
<span>Â = ${oscFit.best.A.toFixed(3)}, Δm̂² = ${oscFit.best.dm.toFixed(2)}·10⁻³ эВ²</span>
</div>
</div>
`