The demo below runs Bayesian optimization on a noisy one-dimensional function. The blue curve is the true (hidden) function; the orange curve is the Gaussian process surrogate the algorithm has built from the evaluations so far. The green band shows uncertainty — wider means less explored.
<p class="hint">The <b>blue curve</b> is the hidden true function. The <b>orange curve</b>
is the Gaussian process surrogate. The <b>green band</b> is uncertainty (±2σ).
Dots are evaluations so far. Click <b>Next evaluation</b> to let the algorithm
pick the point with highest Expected Improvement.</p>
<canvas id="chart" width="560" height="220"></canvas>
<div class="info" id="info">Press "Next evaluation" to start.</div>
<div class="btns">
<button id="btn-next" type="button">Next evaluation</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; }
.info { font-size: .92rem; font-weight: 600; min-height: 1.4em; margin: .5rem 0; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bayesian Optimization demo — 1-D Gaussian Process surrogate with
// Expected Improvement acquisition function.
//
// True function: f(x) = sin(3x) + x*cos(x) + small Gaussian noise
// We want to find the MINIMUM.
// GP uses a squared-exponential (RBF) kernel.
// Expected Improvement (EI) picks the next x.
const W = 560, H = 220;
const XMIN = 0, XMAX = 6.5;
const NOISE = 0.12; // observation noise std-dev
const LENGTHSCALE = 0.8; // RBF kernel length-scale
const SIGNAL_VAR = 1.5; // RBF kernel signal variance
const NOISE_VAR = NOISE * NOISE;
const NX = 300; // grid resolution for plotting
// True objective (unknown to the algorithm, known to us for display)
function trueF(x) {
return Math.sin(3 * x) + x * Math.cos(x);
}
// Noisy observation
function observe(x) {
return trueF(x) + NOISE * randn();
}
// Box-Muller normal sample
function randn() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// RBF kernel
function kernel(x1, x2) {
const d = x1 - x2;
return SIGNAL_VAR * Math.exp(-0.5 * d * d / (LENGTHSCALE * LENGTHSCALE));
}
// Build covariance matrix K(X,X) + noise*I
function buildK(xs) {
const n = xs.length;
const K = [];
for (let i = 0; i < n; i++) {
K[i] = [];
for (let j = 0; j < n; j++) {
K[i][j] = kernel(xs[i], xs[j]) + (i === j ? NOISE_VAR : 0);
}
}
return K;
}
// Cholesky decomposition L s.t. K = L * L^T
function cholesky(K) {
const n = K.length;
const L = Array.from({length: n}, () => new Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j <= i; j++) {
let s = K[i][j];
for (let k = 0; k < j; k++) s -= L[i][k] * L[j][k];
L[i][j] = (i === j) ? Math.sqrt(Math.max(s, 1e-10)) : s / L[j][j];
}
}
return L;
}
// Solve L * y = b (forward substitution)
function solveL(L, b) {
const n = b.length;
const y = new Array(n);
for (let i = 0; i < n; i++) {
let s = b[i];
for (let j = 0; j < i; j++) s -= L[i][j] * y[j];
y[i] = s / L[i][i];
}
return y;
}
// Solve L^T * x = y (back substitution)
function solveLT(L, y) {
const n = y.length;
const x = new Array(n);
for (let i = n - 1; i >= 0; i--) {
let s = y[i];
for (let j = i + 1; j < n; j++) s -= L[j][i] * x[j];
x[i] = s / L[i][i];
}
return x;
}
// GP posterior mean and variance at a set of test points
function gpPredict(trainX, trainY, testX) {
const n = trainX.length;
if (n === 0) {
// Prior: mean 0, var = SIGNAL_VAR
return testX.map(() => ({ mean: 0, std: Math.sqrt(SIGNAL_VAR) }));
}
const K = buildK(trainX);
const L = cholesky(K);
const alpha = solveLT(L, solveL(L, trainY));
return testX.map(xs => {
// k_* = [k(xs, x1), ..., k(xs, xn)]
const kstar = trainX.map(x => kernel(xs, x));
// mean = kstar^T * alpha
let mean = 0;
for (let i = 0; i < n; i++) mean += kstar[i] * alpha[i];
// variance = k(xs,xs) - kstar^T * (K^{-1} kstar)
const v = solveL(L, kstar);
let var_ = kernel(xs, xs);
for (let i = 0; i < n; i++) var_ -= v[i] * v[i];
var_ = Math.max(var_, 0);
return { mean, std: Math.sqrt(var_) };
});
}
// Standard normal CDF (approximation)
function normCDF(z) {
const t = 1 / (1 + 0.2316419 * Math.abs(z));
const poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const p = 1 - (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * z * z) * poly;
return z >= 0 ? p : 1 - p;
}
// Standard normal PDF
function normPDF(z) {
return (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * z * z);
}
// Expected Improvement (minimization)
function ei(mean, std, fBest) {
if (std < 1e-10) return 0;
const z = (fBest - mean) / std;
return (fBest - mean) * normCDF(z) + std * normPDF(z);
}
// ── State ─────────────────────────────────────────────────────────────────
let obsX = [], obsY = [];
let evalCount = 0;
// Build x-grid for plotting/acquisition
const grid = [];
for (let i = 0; i < NX; i++) grid.push(XMIN + (XMAX - XMIN) * i / (NX - 1));
// Pre-compute true function on grid (for display)
const trueGrid = grid.map(trueF);
const trueMin = Math.min(...trueGrid);
const trueMinX = grid[trueGrid.indexOf(trueMin)];
// ── Drawing ───────────────────────────────────────────────────────────────
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
canvas.width = W * DPR; canvas.height = H * DPR;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.scale(DPR, DPR);
const PAD = { top: 12, right: 14, bottom: 32, left: 42 };
const PW = W - PAD.left - PAD.right;
const PH = H - PAD.top - PAD.bottom;
function toCanvasX(x) { return PAD.left + (x - XMIN) / (XMAX - XMIN) * PW; }
function toCanvasY(y, ymin, ymax) { return PAD.top + PH - (y - ymin) / (ymax - ymin) * PH; }
function drawAll() {
ctx.clearRect(0, 0, W, H);
// GP prediction on grid
const preds = gpPredict(obsX, obsY, grid);
const means = preds.map(p => p.mean);
const stds = preds.map(p => p.std);
// y axis range: union of true + GP ±2σ
const allY = [...trueGrid, ...means.map((m, i) => m + 2 * stds[i]),
...means.map((m, i) => m - 2 * stds[i]), ...obsY];
const yMin = Math.min(...allY) - 0.3;
const yMax = Math.max(...allY) + 0.3;
// Uncertainty band ±2σ
ctx.beginPath();
ctx.moveTo(toCanvasX(grid[0]), toCanvasY(means[0] + 2 * stds[0], yMin, yMax));
for (let i = 1; i < NX; i++)
ctx.lineTo(toCanvasX(grid[i]), toCanvasY(means[i] + 2 * stds[i], yMin, yMax));
for (let i = NX - 1; i >= 0; i--)
ctx.lineTo(toCanvasX(grid[i]), toCanvasY(means[i] - 2 * stds[i], yMin, yMax));
ctx.closePath();
ctx.fillStyle = 'rgba(34,197,94,0.15)';
ctx.fill();
// True function (blue)
ctx.beginPath();
ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 1.8;
ctx.setLineDash([6, 4]);
grid.forEach((x, i) => {
const cx = toCanvasX(x), cy = toCanvasY(trueGrid[i], yMin, yMax);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
ctx.setLineDash([]);
// GP mean (orange)
ctx.beginPath();
ctx.strokeStyle = '#f97316'; ctx.lineWidth = 2;
grid.forEach((x, i) => {
const cx = toCanvasX(x), cy = toCanvasY(means[i], yMin, yMax);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
// Axes
ctx.strokeStyle = '#94a3b8'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + PH);
ctx.lineTo(PAD.left + PW, PAD.top + PH);
ctx.stroke();
// y=0 line
if (yMin < 0 && yMax > 0) {
const y0 = toCanvasY(0, yMin, yMax);
ctx.strokeStyle = '#cbd5e1'; ctx.lineWidth = 0.8;
ctx.setLineDash([3, 3]);
ctx.beginPath(); ctx.moveTo(PAD.left, y0); ctx.lineTo(PAD.left + PW, y0); ctx.stroke();
ctx.setLineDash([]);
}
// Axis labels
ctx.fillStyle = '#64748b'; ctx.font = '11px system-ui';
ctx.textAlign = 'center';
[0, 1, 2, 3, 4, 5, 6].forEach(v => {
if (v <= XMAX) {
const cx = toCanvasX(v);
ctx.fillText(v, cx, PAD.top + PH + 18);
}
});
ctx.textAlign = 'right';
[-3, -2, -1, 0, 1, 2, 3, 4].forEach(v => {
if (v >= yMin - 0.1 && v <= yMax + 0.1) {
ctx.fillText(v, PAD.left - 5, toCanvasY(v, yMin, yMax) + 4);
}
});
// Observed points
obsX.forEach((x, i) => {
const cx = toCanvasX(x), cy = toCanvasY(obsY[i], yMin, yMax);
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557'; ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
});
// EI acquisition: find next point (highlight)
if (obsX.length > 0) {
const fBest = Math.min(...obsY);
let bestEI = -Infinity, bestIdx = 0;
for (let i = 0; i < NX; i++) {
const v = ei(means[i], stds[i], fBest);
if (v > bestEI) { bestEI = v; bestIdx = i; }
}
const nx = grid[bestIdx];
const cx = toCanvasX(nx);
ctx.strokeStyle = '#16a34a'; ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(cx, PAD.top); ctx.lineTo(cx, PAD.top + PH); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#16a34a'; ctx.font = '10px system-ui'; ctx.textAlign = 'left';
ctx.fillText('next →', cx + 3, PAD.top + 12);
}
// Legend
ctx.font = '10px system-ui'; ctx.textAlign = 'left';
const lx = PAD.left + 8, ly = PAD.top + 14;
ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 1.8; ctx.setLineDash([5, 3]);
ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 20, ly); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#2563eb'; ctx.fillText('true f(x)', lx + 24, ly + 4);
ctx.strokeStyle = '#f97316'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(lx, ly + 14); ctx.lineTo(lx + 20, ly + 14); ctx.stroke();
ctx.fillStyle = '#f97316'; ctx.fillText('GP surrogate', lx + 24, ly + 18);
}
function pickNext() {
const preds = gpPredict(obsX, obsY, grid);
const means = preds.map(p => p.mean);
const stds = preds.map(p => p.std);
let xNext;
if (obsX.length === 0) {
// First point: random
xNext = XMIN + Math.random() * (XMAX - XMIN);
} else {
const fBest = Math.min(...obsY);
let bestEI = -Infinity, bestIdx = 0;
for (let i = 0; i < NX; i++) {
const v = ei(means[i], stds[i], fBest);
if (v > bestEI) { bestEI = v; bestIdx = i; }
}
xNext = grid[bestIdx];
}
const yNext = observe(xNext);
obsX.push(xNext);
obsY.push(yNext);
evalCount++;
return { xNext, yNext };
}
function updateInfo() {
const info = document.getElementById('info');
if (obsX.length === 0) {
info.textContent = 'Press "Next evaluation" to start.';
return;
}
const bestIdx = obsY.indexOf(Math.min(...obsY));
const bestX = obsX[bestIdx].toFixed(3);
const bestY = obsY[bestIdx].toFixed(3);
const trueMinStr = trueMin.toFixed(3);
info.textContent = fmt('Eval #%evalCount% — best so far: f(%bestX%) = %bestY% | true min ≈ %trueMinStr% at x ≈ %trueMinX%', { evalCount: evalCount, bestX: bestX, bestY: bestY, trueMinStr: trueMinStr, trueMinX: trueMinX.toFixed(3) });
}
document.getElementById('btn-next').addEventListener('click', () => {
pickNext();
drawAll();
updateInfo();
});
document.getElementById('btn-reset').addEventListener('click', () => {
obsX = []; obsY = []; evalCount = 0;
drawAll();
updateInfo();
});
drawAll();
updateInfo();
// Self-test (runs at load, not shown to user): verify GP and EI are working.
(function selfTest() {
// With a known observation, GP mean at that x should be close to observed y.
const tx = [2.0], ty = [trueF(2.0)];
const pred = gpPredict(tx, ty, [2.0]);
const meanAt2 = pred[0].mean;
const stdAt2 = pred[0].std;
// mean at observed point should be very close to observed value
if (Math.abs(meanAt2 - ty[0]) > 0.5) {
console.error('GP self-test FAILED: mean at obs point =', meanAt2, 'expected ~', ty[0]);
} else {
console.log('GP self-test passed: mean at obs =', meanAt2.toFixed(4), '(expected ~', ty[0].toFixed(4) + ')');
}
// Std at observed point should be low (noise-conditioned)
if (stdAt2 > 0.5) {
console.error('GP self-test FAILED: std at obs point too large =', stdAt2);
} else {
console.log('GP self-test passed: std at obs =', stdAt2.toFixed(4), '(expected low)');
}
// EI at a new point should be > 0 when we can improve
const testEI = ei(-5, 0.5, ty[0]); // mean << fBest => big EI
if (testEI <= 0) {
console.error('EI self-test FAILED: expected positive EI, got', testEI);
} else {
console.log('EI self-test passed: EI =', testEI.toFixed(4));
}
})();
<p class="hint">The <b>blue curve</b> is the hidden true function. The <b>orange curve</b>
is the Gaussian process surrogate. The <b>green band</b> is uncertainty (±2σ).
Dots are evaluations so far. Click <b>Next evaluation</b> to let the algorithm
pick the point with highest Expected Improvement.</p>
<canvas id="chart" width="560" height="220"></canvas>
<div class="info" id="info">Press "Next evaluation" to start.</div>
<div class="btns">
<button id="btn-next" type="button">Next evaluation</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; }
.info { font-size: .92rem; font-weight: 600; min-height: 1.4em; margin: .5rem 0; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Bayesian Optimization demo — 1-D Gaussian Process surrogate with
// Expected Improvement acquisition function.
//
// True function: f(x) = sin(3x) + x*cos(x) + small Gaussian noise
// We want to find the MINIMUM.
// GP uses a squared-exponential (RBF) kernel.
// Expected Improvement (EI) picks the next x.
const W = 560, H = 220;
const XMIN = 0, XMAX = 6.5;
const NOISE = 0.12; // observation noise std-dev
const LENGTHSCALE = 0.8; // RBF kernel length-scale
const SIGNAL_VAR = 1.5; // RBF kernel signal variance
const NOISE_VAR = NOISE * NOISE;
const NX = 300; // grid resolution for plotting
// True objective (unknown to the algorithm, known to us for display)
function trueF(x) {
return Math.sin(3 * x) + x * Math.cos(x);
}
// Noisy observation
function observe(x) {
return trueF(x) + NOISE * randn();
}
// Box-Muller normal sample
function randn() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// RBF kernel
function kernel(x1, x2) {
const d = x1 - x2;
return SIGNAL_VAR * Math.exp(-0.5 * d * d / (LENGTHSCALE * LENGTHSCALE));
}
// Build covariance matrix K(X,X) + noise*I
function buildK(xs) {
const n = xs.length;
const K = [];
for (let i = 0; i < n; i++) {
K[i] = [];
for (let j = 0; j < n; j++) {
K[i][j] = kernel(xs[i], xs[j]) + (i === j ? NOISE_VAR : 0);
}
}
return K;
}
// Cholesky decomposition L s.t. K = L * L^T
function cholesky(K) {
const n = K.length;
const L = Array.from({length: n}, () => new Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j <= i; j++) {
let s = K[i][j];
for (let k = 0; k < j; k++) s -= L[i][k] * L[j][k];
L[i][j] = (i === j) ? Math.sqrt(Math.max(s, 1e-10)) : s / L[j][j];
}
}
return L;
}
// Solve L * y = b (forward substitution)
function solveL(L, b) {
const n = b.length;
const y = new Array(n);
for (let i = 0; i < n; i++) {
let s = b[i];
for (let j = 0; j < i; j++) s -= L[i][j] * y[j];
y[i] = s / L[i][i];
}
return y;
}
// Solve L^T * x = y (back substitution)
function solveLT(L, y) {
const n = y.length;
const x = new Array(n);
for (let i = n - 1; i >= 0; i--) {
let s = y[i];
for (let j = i + 1; j < n; j++) s -= L[j][i] * x[j];
x[i] = s / L[i][i];
}
return x;
}
// GP posterior mean and variance at a set of test points
function gpPredict(trainX, trainY, testX) {
const n = trainX.length;
if (n === 0) {
// Prior: mean 0, var = SIGNAL_VAR
return testX.map(() => ({ mean: 0, std: Math.sqrt(SIGNAL_VAR) }));
}
const K = buildK(trainX);
const L = cholesky(K);
const alpha = solveLT(L, solveL(L, trainY));
return testX.map(xs => {
// k_* = [k(xs, x1), ..., k(xs, xn)]
const kstar = trainX.map(x => kernel(xs, x));
// mean = kstar^T * alpha
let mean = 0;
for (let i = 0; i < n; i++) mean += kstar[i] * alpha[i];
// variance = k(xs,xs) - kstar^T * (K^{-1} kstar)
const v = solveL(L, kstar);
let var_ = kernel(xs, xs);
for (let i = 0; i < n; i++) var_ -= v[i] * v[i];
var_ = Math.max(var_, 0);
return { mean, std: Math.sqrt(var_) };
});
}
// Standard normal CDF (approximation)
function normCDF(z) {
const t = 1 / (1 + 0.2316419 * Math.abs(z));
const poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const p = 1 - (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * z * z) * poly;
return z >= 0 ? p : 1 - p;
}
// Standard normal PDF
function normPDF(z) {
return (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * z * z);
}
// Expected Improvement (minimization)
function ei(mean, std, fBest) {
if (std < 1e-10) return 0;
const z = (fBest - mean) / std;
return (fBest - mean) * normCDF(z) + std * normPDF(z);
}
// ── State ─────────────────────────────────────────────────────────────────
let obsX = [], obsY = [];
let evalCount = 0;
// Build x-grid for plotting/acquisition
const grid = [];
for (let i = 0; i < NX; i++) grid.push(XMIN + (XMAX - XMIN) * i / (NX - 1));
// Pre-compute true function on grid (for display)
const trueGrid = grid.map(trueF);
const trueMin = Math.min(...trueGrid);
const trueMinX = grid[trueGrid.indexOf(trueMin)];
// ── Drawing ───────────────────────────────────────────────────────────────
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
canvas.width = W * DPR; canvas.height = H * DPR;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.scale(DPR, DPR);
const PAD = { top: 12, right: 14, bottom: 32, left: 42 };
const PW = W - PAD.left - PAD.right;
const PH = H - PAD.top - PAD.bottom;
function toCanvasX(x) { return PAD.left + (x - XMIN) / (XMAX - XMIN) * PW; }
function toCanvasY(y, ymin, ymax) { return PAD.top + PH - (y - ymin) / (ymax - ymin) * PH; }
function drawAll() {
ctx.clearRect(0, 0, W, H);
// GP prediction on grid
const preds = gpPredict(obsX, obsY, grid);
const means = preds.map(p => p.mean);
const stds = preds.map(p => p.std);
// y axis range: union of true + GP ±2σ
const allY = [...trueGrid, ...means.map((m, i) => m + 2 * stds[i]),
...means.map((m, i) => m - 2 * stds[i]), ...obsY];
const yMin = Math.min(...allY) - 0.3;
const yMax = Math.max(...allY) + 0.3;
// Uncertainty band ±2σ
ctx.beginPath();
ctx.moveTo(toCanvasX(grid[0]), toCanvasY(means[0] + 2 * stds[0], yMin, yMax));
for (let i = 1; i < NX; i++)
ctx.lineTo(toCanvasX(grid[i]), toCanvasY(means[i] + 2 * stds[i], yMin, yMax));
for (let i = NX - 1; i >= 0; i--)
ctx.lineTo(toCanvasX(grid[i]), toCanvasY(means[i] - 2 * stds[i], yMin, yMax));
ctx.closePath();
ctx.fillStyle = 'rgba(34,197,94,0.15)';
ctx.fill();
// True function (blue)
ctx.beginPath();
ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 1.8;
ctx.setLineDash([6, 4]);
grid.forEach((x, i) => {
const cx = toCanvasX(x), cy = toCanvasY(trueGrid[i], yMin, yMax);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
ctx.setLineDash([]);
// GP mean (orange)
ctx.beginPath();
ctx.strokeStyle = '#f97316'; ctx.lineWidth = 2;
grid.forEach((x, i) => {
const cx = toCanvasX(x), cy = toCanvasY(means[i], yMin, yMax);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
// Axes
ctx.strokeStyle = '#94a3b8'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + PH);
ctx.lineTo(PAD.left + PW, PAD.top + PH);
ctx.stroke();
// y=0 line
if (yMin < 0 && yMax > 0) {
const y0 = toCanvasY(0, yMin, yMax);
ctx.strokeStyle = '#cbd5e1'; ctx.lineWidth = 0.8;
ctx.setLineDash([3, 3]);
ctx.beginPath(); ctx.moveTo(PAD.left, y0); ctx.lineTo(PAD.left + PW, y0); ctx.stroke();
ctx.setLineDash([]);
}
// Axis labels
ctx.fillStyle = '#64748b'; ctx.font = '11px system-ui';
ctx.textAlign = 'center';
[0, 1, 2, 3, 4, 5, 6].forEach(v => {
if (v <= XMAX) {
const cx = toCanvasX(v);
ctx.fillText(v, cx, PAD.top + PH + 18);
}
});
ctx.textAlign = 'right';
[-3, -2, -1, 0, 1, 2, 3, 4].forEach(v => {
if (v >= yMin - 0.1 && v <= yMax + 0.1) {
ctx.fillText(v, PAD.left - 5, toCanvasY(v, yMin, yMax) + 4);
}
});
// Observed points
obsX.forEach((x, i) => {
const cx = toCanvasX(x), cy = toCanvasY(obsY[i], yMin, yMax);
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557'; ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
});
// EI acquisition: find next point (highlight)
if (obsX.length > 0) {
const fBest = Math.min(...obsY);
let bestEI = -Infinity, bestIdx = 0;
for (let i = 0; i < NX; i++) {
const v = ei(means[i], stds[i], fBest);
if (v > bestEI) { bestEI = v; bestIdx = i; }
}
const nx = grid[bestIdx];
const cx = toCanvasX(nx);
ctx.strokeStyle = '#16a34a'; ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(cx, PAD.top); ctx.lineTo(cx, PAD.top + PH); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#16a34a'; ctx.font = '10px system-ui'; ctx.textAlign = 'left';
ctx.fillText('next →', cx + 3, PAD.top + 12);
}
// Legend
ctx.font = '10px system-ui'; ctx.textAlign = 'left';
const lx = PAD.left + 8, ly = PAD.top + 14;
ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 1.8; ctx.setLineDash([5, 3]);
ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 20, ly); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#2563eb'; ctx.fillText('true f(x)', lx + 24, ly + 4);
ctx.strokeStyle = '#f97316'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(lx, ly + 14); ctx.lineTo(lx + 20, ly + 14); ctx.stroke();
ctx.fillStyle = '#f97316'; ctx.fillText('GP surrogate', lx + 24, ly + 18);
}
function pickNext() {
const preds = gpPredict(obsX, obsY, grid);
const means = preds.map(p => p.mean);
const stds = preds.map(p => p.std);
let xNext;
if (obsX.length === 0) {
// First point: random
xNext = XMIN + Math.random() * (XMAX - XMIN);
} else {
const fBest = Math.min(...obsY);
let bestEI = -Infinity, bestIdx = 0;
for (let i = 0; i < NX; i++) {
const v = ei(means[i], stds[i], fBest);
if (v > bestEI) { bestEI = v; bestIdx = i; }
}
xNext = grid[bestIdx];
}
const yNext = observe(xNext);
obsX.push(xNext);
obsY.push(yNext);
evalCount++;
return { xNext, yNext };
}
function updateInfo() {
const info = document.getElementById('info');
if (obsX.length === 0) {
info.textContent = 'Press "Next evaluation" to start.';
return;
}
const bestIdx = obsY.indexOf(Math.min(...obsY));
const bestX = obsX[bestIdx].toFixed(3);
const bestY = obsY[bestIdx].toFixed(3);
const trueMinStr = trueMin.toFixed(3);
info.textContent = fmt('Eval #%evalCount% — best so far: f(%bestX%) = %bestY% | true min ≈ %trueMinStr% at x ≈ %trueMinX%', { evalCount: evalCount, bestX: bestX, bestY: bestY, trueMinStr: trueMinStr, trueMinX: trueMinX.toFixed(3) });
}
document.getElementById('btn-next').addEventListener('click', () => {
pickNext();
drawAll();
updateInfo();
});
document.getElementById('btn-reset').addEventListener('click', () => {
obsX = []; obsY = []; evalCount = 0;
drawAll();
updateInfo();
});
drawAll();
updateInfo();
// Self-test (runs at load, not shown to user): verify GP and EI are working.
(function selfTest() {
// With a known observation, GP mean at that x should be close to observed y.
const tx = [2.0], ty = [trueF(2.0)];
const pred = gpPredict(tx, ty, [2.0]);
const meanAt2 = pred[0].mean;
const stdAt2 = pred[0].std;
// mean at observed point should be very close to observed value
if (Math.abs(meanAt2 - ty[0]) > 0.5) {
console.error('GP self-test FAILED: mean at obs point =', meanAt2, 'expected ~', ty[0]);
} else {
console.log('GP self-test passed: mean at obs =', meanAt2.toFixed(4), '(expected ~', ty[0].toFixed(4) + ')');
}
// Std at observed point should be low (noise-conditioned)
if (stdAt2 > 0.5) {
console.error('GP self-test FAILED: std at obs point too large =', stdAt2);
} else {
console.log('GP self-test passed: std at obs =', stdAt2.toFixed(4), '(expected low)');
}
// EI at a new point should be > 0 when we can improve
const testEI = ei(-5, 0.5, ty[0]); // mean << fBest => big EI
if (testEI <= 0) {
console.error('EI self-test FAILED: expected positive EI, got', testEI);
} else {
console.log('EI self-test passed: EI =', testEI.toFixed(4));
}
})();
Comments
Loading comments...