solarFitEventsInput = Inputs.range([5000, 120000], {value: 60000, step: 1000, label: "N₀"})
solarFitS12TrueInput = Inputs.range([0.20, 0.42], {value: 0.307, step: 0.002, label: "true sin²θ₁₂"})
solarFitDmTrueInput = Inputs.range([5.0, 10.0], {value: 7.42, step: 0.05, label: "true Δm²₂₁"})
solarFitNBinsInput = Inputs.range([8, 28], {value: 18, step: 1, label: "bins"})
solarFitThresholdInput = Inputs.range([3.0, 7.0], {value: 3.0, step: 0.25, label: "Tₑ,min [MeV]"})
solarFitRegenerateInput = Inputs.button("new pseudo-data", {value: 0, reduce: () => Date.now()})
solarFitEvents = Generators.input(solarFitEventsInput)
solarFitS12True = Generators.input(solarFitS12TrueInput)
solarFitDmTrue = Generators.input(solarFitDmTrueInput)
solarFitNBins = Generators.input(solarFitNBinsInput)
solarFitThreshold = Generators.input(solarFitThresholdInput)
solarFitRegenerate = Generators.input(solarFitRegenerateInput)
solarFitB8Rows = FileAttachment("../data/b8_spectrum.csv").csv({typed: true})
solarFitCrossRows = FileAttachment("../data/masterclass1/nu_electron_recoil_cross_sections.csv").csv({typed: true})
solarFitB8Table = solarFitB8Rows
.map(row => ({E: Number(row.E_MeV), shape: Number(row.shape)}))
.sort((a, b) => a.E - b.E)
solarFitB8ShapeAt = E => {
if (E < solarFitB8Table[0].E || E > solarFitB8Table[solarFitB8Table.length - 1].E) return 0;
let lo = 0;
let hi = solarFitB8Table.length - 1;
while (hi - lo > 1) {
const mid = Math.floor((lo + hi) / 2);
if (solarFitB8Table[mid].E <= E) lo = mid;
else hi = mid;
}
const a = solarFitB8Table[lo];
const b = solarFitB8Table[hi];
const f = (E - a.E) / Math.max(b.E - a.E, 1e-12);
return a.shape + f * (b.shape - a.shape);
}
solarFitTGrid = Array.from({length: solarFitNBins}, (_, i) => {
const low = solarFitThreshold;
const high = 14.0;
const t1 = low + i * (high - low) / solarFitNBins;
const t2 = low + (i + 1) * (high - low) / solarFitNBins;
return {i, t1, t2, t: 0.5 * (t1 + t2), groups: new Map()};
})
solarFitBinWeights = {
const dE = 0.05;
const dT = 0.10;
const tLow = solarFitTGrid[0].t1;
const tHigh = solarFitTGrid[solarFitTGrid.length - 1].t2;
for (const row of solarFitCrossRows) {
const E = Number(row.E_MeV);
const T = Number(row.T_e_MeV);
if (E < 0.05 || E > 16.0) continue;
if (T + 0.5 * dT <= tLow || T - 0.5 * dT >= tHigh) continue;
const shape = solarFitB8ShapeAt(E);
if (!(shape > 0)) continue;
const dsE = Number(row.dsigma_nue_cm2_per_MeV);
const dsX = Number(row.dsigma_nux_cm2_per_MeV);
if (!(dsE > 0) && !(dsX > 0)) continue;
const cellLow = Math.max(tLow, T - 0.5 * dT);
const cellHigh = Math.min(tHigh, T + 0.5 * dT);
if (cellHigh <= cellLow) continue;
const firstBin = Math.max(0, Math.floor((cellLow - tLow) / (tHigh - tLow) * solarFitTGrid.length));
const lastBin = Math.min(
solarFitTGrid.length - 1,
Math.floor((cellHigh - 1e-12 - tLow) / (tHigh - tLow) * solarFitTGrid.length)
);
for (let ibin = firstBin; ibin <= lastBin; ++ibin) {
const bin = solarFitTGrid[ibin];
const overlap = Math.max(0, Math.min(cellHigh, bin.t2) - Math.max(cellLow, bin.t1));
if (!(overlap > 0)) continue;
const key = E.toFixed(2);
const group = bin.groups.get(key) ?? {E, sigmaE: 0, sigmaX: 0};
group.sigmaE += shape * dsE * dE * overlap;
group.sigmaX += shape * dsX * dE * overlap;
bin.groups.set(key, group);
}
}
return solarFitTGrid.map(bin => ({
i: bin.i,
t1: bin.t1,
t2: bin.t2,
t: bin.t,
weights: Array.from(bin.groups.values())
}));
}
solarFitPee = (Emev, s12, dmMicro) => {
const gfEv = 1.1663787e-23;
const cmToEvInv = 5.067730716e4;
const cmMinus3ToEv3 = 1 / Math.pow(cmToEvInv, 3);
const neCore = 6.0e25;
const ve = Math.SQRT2 * gfEv * neCore * cmMinus3ToEv3;
const dm2 = dmMicro * 1e-5;
const c2 = 1 - 2 * s12;
const s22 = 4 * s12 * (1 - s12);
const a = 2 * Emev * 1e6 * ve / dm2;
const cos2ThetaM = (c2 - a) / Math.sqrt((c2 - a) * (c2 - a) + s22);
return Math.min(0.98, Math.max(0.02, 0.5 + 0.5 * c2 * cos2ThetaM));
}
solarFitExpectedRaw = (s12, dmMicro) => solarFitBinWeights.map(bin => {
let mu = 0;
for (const row of bin.weights) {
const pee = solarFitPee(row.E, s12, dmMicro);
mu += pee * row.sigmaE + (1 - pee) * row.sigmaX;
}
return mu;
})
solarFitNoOscRaw = solarFitBinWeights.map(bin =>
bin.weights.reduce((sum, row) => sum + row.sigmaE, 0)
)
solarFitNoOscTotalRaw = solarFitNoOscRaw.reduce((sum, x) => sum + x, 0)
solarFitExpected = (s12, dmMicro) => {
const raw = solarFitExpectedRaw(s12, dmMicro);
const scale = solarFitEvents / Math.max(solarFitNoOscTotalRaw, 1e-300);
return raw.map(x => x * scale);
}
solarFitNoOscExpected = solarFitNoOscRaw.map(x => x * solarFitEvents / Math.max(solarFitNoOscTotalRaw, 1e-300))
solarFitRandn = () => {
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);
}
solarFitPoisson = lambda => {
if (lambda <= 0) return 0;
if (lambda > 80) return Math.max(0, Math.round(lambda + Math.sqrt(lambda) * solarFitRandn()));
const limit = Math.exp(-lambda);
let k = 0;
let product = 1;
do {
++k;
product *= Math.random();
} while (product > limit);
return k - 1;
}
solarFitData = {
solarFitRegenerate;
const mu = solarFitExpected(solarFitS12True, solarFitDmTrue);
return solarFitBinWeights.map((bin, i) => ({
...bin,
n: solarFitPoisson(mu[i]),
muTrue: mu[i],
muNoOsc: solarFitNoOscExpected[i]
}));
}
solarFitLogLike = (s12, dmMicro) => {
const mu = solarFitExpected(s12, dmMicro);
let ll = 0;
for (let i = 0; i < solarFitData.length; ++i) {
const lambda = Math.max(mu[i], 1e-12);
ll += solarFitData[i].n * Math.log(lambda) - lambda;
}
return ll;
}
solarFitS12Grid = Array.from({length: 61}, (_, i) => 0.20 + i * (0.42 - 0.20) / 60)
solarFitDmGrid = Array.from({length: 61}, (_, i) => 5.0 + i * (10.0 - 5.0) / 60)
solarFitResult = {
const rows = [];
let best = {s12: 0, dm: 0, ll: -Infinity};
for (let ia = 0; ia < solarFitS12Grid.length; ++ia) {
const s12 = solarFitS12Grid[ia];
for (let id = 0; id < solarFitDmGrid.length; ++id) {
const dm = solarFitDmGrid[id];
const ll = solarFitLogLike(s12, dm);
rows.push({ia, id, s12, dm, ll});
if (ll > best.ll) best = {s12, dm, ll};
}
}
const profileS12 = solarFitS12Grid.map(s12 => ({s12, q: Infinity}));
const profileDm = solarFitDmGrid.map(dm => ({dm, q: Infinity}));
for (const row of rows) {
row.q = -2 * (row.ll - best.ll);
row.like = Math.exp(-0.5 * Math.min(row.q, 16));
row.qClip = Math.min(row.q, 9);
profileS12[row.ia].q = Math.min(profileS12[row.ia].q, row.q);
profileDm[row.id].q = Math.min(profileDm[row.id].q, row.q);
}
return {rows, best, profileS12, profileDm};
}
solarFitRegion68 = solarFitResult.rows.filter(row => row.q <= 2.30)
solarFitRegion95 = solarFitResult.rows.filter(row => row.q <= 6.18)
solarFitBestExpected = solarFitExpected(solarFitResult.best.s12, solarFitResult.best.dm)
solarFitSpectrumRows = solarFitData.map((row, i) => ({...row, muBest: solarFitBestExpected[i]}))
solarFitObservedTotal = solarFitData.reduce((sum, row) => sum + row.n, 0)
solarFitTrueTotal = solarFitData.reduce((sum, row) => sum + row.muTrue, 0)
solarFitProfileS12Rows = solarFitResult.profileS12.map(row => ({...row, qPlot: Math.min(row.q, 8)}))
solarFitProfileDmRows = solarFitResult.profileDm.map(row => ({...row, qPlot: Math.min(row.q, 8)}))
solarFitPlotStyle = ({
background: "#111111",
color: "#f0f0f0",
fontSize: "13px"
})
solarFitMapPlot = Plot.plot({
width: 560,
height: 350,
marginLeft: 62,
marginBottom: 44,
style: solarFitPlotStyle,
x: {label: "Δm²₂₁ [10⁻⁵ eV²]", domain: [5.0, 10.0], grid: true},
y: {label: "sin²θ₁₂", domain: [0.20, 0.42], grid: true},
color: {scheme: "magma", domain: [0, 1]},
marks: [
Plot.dot(solarFitResult.rows, {x: "dm", y: "s12", fill: "like", r: 2.6}),
Plot.dot(solarFitRegion95, {x: "dm", y: "s12", fill: "#ffffff", fillOpacity: 0.16, r: 3.1}),
Plot.dot(solarFitRegion68, {x: "dm", y: "s12", fill: "#ffffff", fillOpacity: 0.55, r: 3.4}),
Plot.dot([{dm: solarFitDmTrue, s12: solarFitS12True}], {x: "dm", y: "s12", r: 7, fill: "#5ee08c", stroke: "#111", strokeWidth: 1.2}),
Plot.dot([{dm: solarFitResult.best.dm, s12: solarFitResult.best.s12}], {x: "dm", y: "s12", r: 7, fill: "#ffcc8a", stroke: "#111", strokeWidth: 1.2})
]
})
solarFitSpectrumPlot = Plot.plot({
width: 540,
height: 198,
marginLeft: 58,
marginBottom: 34,
style: solarFitPlotStyle,
x: {label: "recoil electron kinetic energy [MeV]", domain: [solarFitThreshold, 14], grid: true},
y: {label: "events/bin", grid: true},
marks: [
Plot.rectY(solarFitSpectrumRows, {x1: "t1", x2: "t2", y: "n", fill: "#d9d9d9", fillOpacity: 0.72}),
Plot.line(solarFitSpectrumRows, {x: "t", y: "muNoOsc", stroke: "#8ec5ff", strokeWidth: 2.0, strokeDasharray: "6,4"}),
Plot.line(solarFitSpectrumRows, {x: "t", y: "muTrue", stroke: "#5ee08c", strokeWidth: 2.3, strokeDasharray: "4,4"}),
Plot.line(solarFitSpectrumRows, {x: "t", y: "muBest", stroke: "#ffcc8a", strokeWidth: 2.8})
]
})
solarFitProfileS12Plot = Plot.plot({
width: 264,
height: 166,
marginLeft: 48,
marginBottom: 32,
style: solarFitPlotStyle,
x: {label: "sin²θ₁₂", domain: [0.20, 0.42], grid: true},
y: {label: "qₚ", domain: [0, 8], grid: true},
marks: [
Plot.line(solarFitProfileS12Rows, {x: "s12", y: "qPlot", stroke: "#ffcc8a", strokeWidth: 2.6}),
Plot.ruleY([1], {stroke: "#cccccc", strokeDasharray: "5,5"}),
Plot.ruleX([solarFitS12True], {stroke: "#5ee08c", strokeDasharray: "4,4"}),
Plot.ruleX([solarFitResult.best.s12], {stroke: "#ffcc8a"})
]
})
solarFitProfileDmPlot = Plot.plot({
width: 264,
height: 166,
marginLeft: 48,
marginBottom: 32,
style: solarFitPlotStyle,
x: {label: "Δm²₂₁ [10⁻⁵ eV²]", domain: [5.0, 10.0], grid: true},
y: {label: "qₚ", domain: [0, 8], grid: true},
marks: [
Plot.line(solarFitProfileDmRows, {x: "dm", y: "qPlot", stroke: "#8ec5ff", strokeWidth: 2.6}),
Plot.ruleY([1], {stroke: "#cccccc", strokeDasharray: "5,5"}),
Plot.ruleX([solarFitDmTrue], {stroke: "#5ee08c", strokeDasharray: "4,4"}),
Plot.ruleX([solarFitResult.best.dm], {stroke: "#ffcc8a"})
]
})
html`
<style>
.solar-fit-widget .observablehq,
.solar-fit-widget .observablehq--inspect {
color: #f2f2f2;
}
.solar-fit-controls {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.26rem 0.72rem;
align-items: center;
margin: 0.02rem 0 0.28rem;
}
.solar-fit-controls form,
.solar-fit-controls label {
margin: 0 !important;
width: 100%;
}
.solar-fit-controls label {
display: grid !important;
grid-template-columns: minmax(4.8rem, 0.75fr) 7.8rem minmax(9rem, 1.35fr);
gap: 0.34rem;
align-items: center;
font-size: 0.58em;
line-height: 1.12;
white-space: nowrap;
}
.solar-fit-controls input[type="range"] {
min-width: 0;
width: 100%;
}
.solar-fit-controls input[type="number"] {
min-width: 7.8rem !important;
width: 7.8rem !important;
color: #111111 !important;
-webkit-text-fill-color: #111111 !important;
background: #ffffff !important;
opacity: 1 !important;
font-size: 0.78em !important;
font-weight: 700;
padding: 0.08rem 0.18rem;
}
.solar-fit-controls button {
color: #111111 !important;
-webkit-text-fill-color: #111111 !important;
background: #ffffff !important;
font-size: 0.74em;
font-weight: 700;
padding: 0.16rem 0.52rem;
}
.solar-fit-control {
min-width: 0;
}
.solar-fit-action {
justify-self: start;
}
.solar-fit-layout {
display: grid;
grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
gap: 0.35rem;
align-items: start;
}
.solar-fit-right {
display: flex;
flex-direction: column;
gap: 0.16rem;
}
.solar-fit-profiles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.12rem;
}
.solar-fit-summary {
display: flex;
flex-wrap: wrap;
gap: 0.36rem 0.85rem;
color: #d8d8d8;
font-size: 0.50em;
line-height: 1.1;
margin-top: 0.16rem;
}
.solar-fit-summary b {
color: #f8f8f8;
}
</style>
<div class="solar-fit-controls">
<div class="solar-fit-control">${solarFitEventsInput}</div>
<div class="solar-fit-control">${solarFitS12TrueInput}</div>
<div class="solar-fit-control">${solarFitDmTrueInput}</div>
<div class="solar-fit-control">${solarFitNBinsInput}</div>
<div class="solar-fit-control">${solarFitThresholdInput}</div>
<div class="solar-fit-action">${solarFitRegenerateInput}</div>
</div>
<div class="solar-fit-layout">
<div>${solarFitMapPlot}</div>
<div class="solar-fit-right">
${solarFitSpectrumPlot}
<div class="solar-fit-profiles">
${solarFitProfileS12Plot}
${solarFitProfileDmPlot}
</div>
</div>
</div>
<div class="solar-fit-summary">
<span><b>green</b>: generated point</span>
<span><b>orange</b>: best fit</span>
<span><b>white</b>: q≤2.30 and q≤6.18</span>
<span>Nobs=${solarFitObservedTotal}</span>
<span>E[N]=${solarFitTrueTotal.toFixed(0)}</span>
<span>Tₑ,min=${solarFitThreshold.toFixed(2)} MeV</span>
<span>best sin²θ₁₂=${solarFitResult.best.s12.toFixed(3)}</span>
<span>best Δm²₂₁=${solarFitResult.best.dm.toFixed(2)}×10⁻⁵ eV²</span>
</div>
`