<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://majid-mazouchi.github.io/autonomy/feed.xml" rel="self" type="application/atom+xml" /><link href="https://majid-mazouchi.github.io/autonomy/" rel="alternate" type="text/html" /><updated>2026-04-19T17:55:57-04:00</updated><id>https://majid-mazouchi.github.io/autonomy/feed.xml</id><title type="html">Autonomy</title><subtitle>Personal engineering notes on control, motor control, machine learning, neural networks, and reinforcement learning — the stack under autonomous systems.</subtitle><author><name>Majid Mazouchi</name></author><entry><title type="html">Bayesian Optimization — an Interactive Explainer</title><link href="https://majid-mazouchi.github.io/autonomy/posts/bayesian-optimization-interactive-explainer/" rel="alternate" type="text/html" title="Bayesian Optimization — an Interactive Explainer" /><published>2026-04-19T14:00:00-04:00</published><updated>2026-04-19T14:00:00-04:00</updated><id>https://majid-mazouchi.github.io/autonomy/posts/bayesian-optimization-interactive-explainer</id><content type="html" xml:base="https://majid-mazouchi.github.io/autonomy/posts/bayesian-optimization-interactive-explainer/"><![CDATA[<p>Some functions are cheap to evaluate. Those are the easy ones. The interesting problems live on the other side — training a neural network, running a wind-tunnel experiment, tuning a motor controller on a dyno — where a single evaluation takes hours, costs money, and gives you one noisy number back. Classical optimization assumes you can evaluate $f(x)$ millions of times, or that you have gradients. Neither assumption holds here. You need a method that is <em>sample-efficient</em> — one that extracts as much information as possible from every measurement and uses that information to decide where to sample next. That’s what Bayesian optimization does, and the demo below shows it running on a benchmark designed to trip it up.</p>

<h2 id="two-ingredients-a-belief-and-a-strategy">Two ingredients: a belief and a strategy</h2>

<p>Every Bayesian-optimization algorithm is built from two interchangeable pieces: a <strong>surrogate model</strong> that represents our current belief about $f$, and an <strong>acquisition function</strong> that scores each candidate point by how useful evaluating it would be.</p>

<p><strong>01 · Surrogate.</strong> A cheap statistical model fit to whatever observations we have so far. Because data is scarce, the model must express <em>uncertainty</em> — a prediction alone isn’t enough; we also need to know where the model is confident and where it’s guessing. By far the most common choice is a <a href="/autonomy/posts/gaussian-processes-interactive-explainer/">Gaussian Process</a>, which gives a full posterior distribution over functions rather than a point estimate.</p>

<p><strong>02 · Acquisition.</strong> A rule for choosing the next sample. The surrogate tells us, at each point, what we <em>expect</em> to see and how <em>uncertain</em> we are. The acquisition function combines these into a single score we can maximize cheaply. Good acquisition functions balance <strong>exploitation</strong> (sampling where the surrogate predicts good values) and <strong>exploration</strong> (sampling where the surrogate is uncertain).</p>

<blockquote>
  <p>Bayesian optimization treats the choice of <em>where to sample next</em> as itself an optimization problem — one we can solve cheaply, since the acquisition function lives on the surrogate, not on the real objective.</p>
</blockquote>

<h2 id="interactive-demo--see-it-run-step-by-step">Interactive demo — see it run, step by step</h2>

<p>Below is a working Bayesian-optimization loop. The objective is the <strong>Forrester function</strong> $f(x) = (6x-2)^2 \sin(12x - 4)$, a standard BO benchmark with a deceptive local minimum near $x = 0.15$ and a global minimum near $x = 0.76$. Pretend you don’t know its shape.</p>

<p>The <strong>shaded band</strong> is the GP posterior (solid line = mean, band = ±2σ). <strong>Gold dots</strong> are observations. The <strong>orange marker</strong> shows where the acquisition function is maximized — that’s the next candidate. Click <em>Next iteration</em> to evaluate at the suggested point and update the model. Or click anywhere on the upper plot to sample there manually.</p>

<div class="bo-widget">

  <div class="bo-plot">
    <div class="bo-plot-head">
      <div class="bo-plot-label">Objective &amp; GP posterior</div>
      <div class="bo-legend">
        <span class="bo-leg"><span class="sw line-mean"></span>GP mean</span>
        <span class="bo-leg"><span class="sw fill-band"></span>±2σ</span>
        <span class="bo-leg"><span class="sw dot-obs"></span>observed</span>
        <span class="bo-leg"><span class="sw line-true"></span>true f(x)</span>
        <span class="bo-leg"><span class="sw dot-next"></span>next</span>
      </div>
    </div>
    <div class="bo-canvas-outer">
      <canvas id="objCanvas" class="bo-obj"></canvas>
      <div class="bo-tooltip" id="objTooltip"></div>
    </div>
  </div>

  <div class="bo-plot">
    <div class="bo-plot-head">
      <div class="bo-plot-label">Acquisition function &mdash; <span id="acqName">Expected Improvement</span></div>
      <div class="bo-legend">
        <span class="bo-leg"><span class="sw line-acq"></span>α(x)</span>
        <span class="bo-leg"><span class="sw dot-next"></span>argmax</span>
      </div>
    </div>
    <div class="bo-canvas-outer">
      <canvas id="acqCanvas" class="bo-acq"></canvas>
    </div>
  </div>

  <div class="bo-controls">
    <div class="bo-panel">
      <div class="bo-panel-label">Acquisition</div>
      <div class="bo-btn-group" id="acqGroup">
        <button type="button" data-acq="EI" class="active">EI</button>
        <button type="button" data-acq="UCB">UCB</button>
        <button type="button" data-acq="PI">PI</button>
      </div>
      <div class="bo-slider">
        <div class="bo-slider-label"><span>GP lengthscale ℓ</span><span class="val" id="lsVal">0.10</span></div>
        <input type="range" id="lsSlider" min="0.02" max="0.40" step="0.005" value="0.10" />
      </div>
    </div>

    <div class="bo-panel">
      <div class="bo-panel-label">Parameters</div>
      <div class="bo-slider">
        <div class="bo-slider-label"><span>UCB exploration κ</span><span class="val" id="kappaVal">2.0</span></div>
        <input type="range" id="kappaSlider" min="0.1" max="5.0" step="0.1" value="2.0" />
      </div>
      <div class="bo-slider">
        <div class="bo-slider-label"><span>EI / PI margin ξ</span><span class="val" id="xiVal">0.01</span></div>
        <input type="range" id="xiSlider" min="0" max="0.5" step="0.01" value="0.01" />
      </div>
      <div class="bo-toggle-row">
        <span>Show true f(x)</span>
        <div class="bo-toggle on" id="trueToggle" role="button" aria-pressed="true" tabindex="0"></div>
      </div>
    </div>

    <div class="bo-panel">
      <div class="bo-panel-label">Actions</div>
      <div class="bo-action-row">
        <button type="button" class="bo-btn-primary" id="stepBtn">Next iteration →</button>
      </div>
      <div class="bo-action-row">
        <button type="button" class="bo-btn" id="auto10Btn">Run 10 steps</button>
        <button type="button" class="bo-btn" id="resetBtn">Reset</button>
      </div>
    </div>
  </div>

  <div class="bo-status">
    <div class="bo-stat"><span class="k">Iteration</span><span class="v" id="iterStat">0</span></div>
    <div class="bo-stat"><span class="k">Observations</span><span class="v" id="nObsStat">3</span></div>
    <div class="bo-stat"><span class="k">Best f*</span><span class="v best" id="bestStat">—</span></div>
    <div class="bo-stat"><span class="k">at x*</span><span class="v" id="bestXStat">—</span></div>
    <div class="bo-stat"><span class="k">Next candidate</span><span class="v next" id="nextStat">—</span></div>
    <div class="bo-stat"><span class="k">Global optimum</span><span class="v muted">−6.021 @ 0.7572</span></div>
  </div>

</div>

<h2 id="gaussian-processes-briefly">Gaussian processes, briefly</h2>

<p>A Gaussian Process is a distribution over functions — any finite collection of function values is jointly Gaussian, fully specified by a mean function $m(x)$ (usually zero) and a covariance kernel $k(x, x’)$.</p>

<p>The kernel encodes our prior about smoothness. The squared-exponential (RBF) kernel, used in the demo above, says that nearby inputs have highly correlated outputs and that correlation decays with distance on a scale set by the lengthscale $\ell$:</p>

\[k(x, x') = \sigma_f^2 \,\exp\!\left(-\frac{\lVert x - x'\rVert^2}{2\ell^2}\right)\]

<p>Given observations $\mathbf{y} = [y_1, \ldots, y_n]^\top$ at inputs $X = {x_1, \ldots, x_n}$ with noise variance $\sigma_n^2$, the posterior at any test point $x_*$ is Gaussian with mean and variance:</p>

\[\mu(x_*) = \mathbf{k}_*^\top \left(K + \sigma_n^2 I\right)^{-1} \mathbf{y}\]

\[\sigma^2(x_*) = k(x_*, x_*) - \mathbf{k}_*^\top \left(K + \sigma_n^2 I\right)^{-1} \mathbf{k}_*\]

<p>Here $K_{ij} = k(x_i, x_j)$ and $(\mathbf{k}<em>*)_i = k(x_i, x</em>*)$. Two properties make this an ideal BO surrogate: the posterior mean <strong>interpolates</strong> the noise-free observations, and the posterior variance <strong>collapses to zero</strong> at observed points and grows smoothly away from them. That’s exactly the signal an acquisition function needs. In practice you solve the linear system via Cholesky decomposition in $\mathcal{O}(n^3)$ — perfectly fine for BO, where $n$ rarely exceeds a few hundred.</p>

<h2 id="three-ways-to-score-a-candidate">Three ways to score a candidate</h2>

<p>Each acquisition function takes the GP posterior $\mathcal{N}(\mu(x), \sigma^2(x))$ and collapses it into a single scalar. What they differ on is <em>how</em> they weigh the two knobs — expected value and uncertainty.</p>

<h3 id="expected-improvement-ei">Expected Improvement (EI)</h3>

<p>For minimization with current best $f^<em>$, define the improvement at $x$ as $I(x) = \max(0, f^</em> - f(x) - \xi)$, where $\xi \geq 0$ encourages more exploration. EI is its expected value under the GP posterior:</p>

\[\alpha_{\text{EI}}(x) = (f^* - \mu(x) - \xi)\,\Phi(z) + \sigma(x)\,\phi(z), \qquad z = \frac{f^* - \mu(x) - \xi}{\sigma(x)}\]

<p>EI is self-calibrating: when $\sigma \to 0$ it reduces to pure exploitation; where $\sigma$ is large and $\mu$ is not too terrible, it favors exploration. This is why it’s the default choice in most BO packages.</p>

<h3 id="lower-confidence-bound-ucb">Lower Confidence Bound (UCB)</h3>

<p>The simplest possible acquisition. Pick the point with the lowest optimistic estimate of $f$ — a linear combination of the posterior mean and a scaled standard deviation:</p>

\[\alpha_{\text{LCB}}(x) = \mu(x) - \kappa\,\sigma(x)\]

<p>The exploration weight $\kappa$ is the tuning knob. $\kappa = 0$ is pure greedy exploitation; large $\kappa$ becomes uncertainty-sampling. Srinivas et al. give a theoretical schedule for $\kappa$ that yields no-regret bounds.</p>

<h3 id="probability-of-improvement-pi">Probability of Improvement (PI)</h3>

<p>Just the probability that a sample at $x$ beats the current best by at least $\xi$:</p>

\[\alpha_{\text{PI}}(x) = \Phi\!\left(\frac{f^* - \mu(x) - \xi}{\sigma(x)}\right)\]

<p>Historically first, but known to under-explore — it happily picks points with microscopic improvement probability as long as <em>any</em> improvement is likely. The margin $\xi$ partially compensates. In practice EI is almost always preferred.</p>

<h2 id="the-whole-algorithm-in-twelve-lines">The whole algorithm, in twelve lines</h2>

<p>Everything above combines into a single loop. The outer loop queries the expensive objective; the inner optimization of the acquisition function is cheap, because it operates on the surrogate, not on $f$ itself.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Given: objective f, domain 𝒳, budget T, acquisition α

initialize D ← { (xᵢ, f(xᵢ)) } for i = 1..n₀     # e.g. Latin hypercube or random

for t = 1, 2, ..., T:
    # 1. fit / update surrogate
    GP ← fit_gp(D)

    # 2. maximize acquisition — cheap, uses only the surrogate
    x_next ← argmax over x ∈ 𝒳  of  α(x | GP, f*_D)

    # 3. query expensive objective at the chosen point
    y_next ← f(x_next)

    # 4. augment dataset
    D ← D ∪ { (x_next, y_next) }

return argmin over (x, y) ∈ D of y
</code></pre></div></div>

<p>Step 2 is the only subtle one. The acquisition function is cheap to evaluate but can be multi-modal, so practitioners use multi-start L-BFGS, DIRECT, or dense grid search on the surrogate. None of this touches the real objective.</p>

<h2 id="where-it-lives">Where it lives</h2>

<p>The common thread across BO applications is a function you can’t see inside, that returns a noisy scalar, and costs real time or real money to query.</p>

<ul>
  <li><strong>Hyperparameter tuning.</strong> Training a deep network costs hours. BO finds strong configurations in 20–50 trials instead of thousands of random ones.</li>
  <li><strong>Experimental design.</strong> Materials discovery, chemistry, biology — any setting where each data point is a physical experiment.</li>
  <li><strong>Controller calibration.</strong> Tuning PID, MPC, or motor-control parameters against a high-fidelity simulator or dyno where each run is slow.</li>
  <li><strong>Engineering design.</strong> Airfoil shapes, antenna geometries, chip layouts — design variables evaluated by expensive CFD or EM solvers.</li>
  <li><strong>Robotics and policy search.</strong> Tuning gait parameters or policy coefficients on hardware, where every rollout risks wear or damage.</li>
  <li><strong>A/B testing at scale.</strong> Treating each experimental configuration as an expensive sample when user traffic or exposure is the bottleneck.</li>
</ul>

<p>Bayesian optimization is the default tool whenever that query cost dominates.</p>

<hr />

<p>The interactive demo above is implemented with vanilla JavaScript and the Canvas 2D API — the GP fit (RBF kernel, Cholesky solve), the three acquisition functions, and all plotting run in the browser with no external libraries. If you want to see how it’s stitched together, view source on the page.</p>

<!-- =====================================================
     Post-scoped styles for the Bayesian-optimization widget
     Reuses the blog's CSS variables (paper, ink, rule, accent)
     ===================================================== -->
<style>
  .article-body .bo-widget {
    /* Local color variables — JS reads these via getComputedStyle */
    --c-gp-mean:  var(--ink);
    --c-band:     rgba(185, 74, 27, 0.15);
    --c-true:     rgba(74, 70, 64, 0.55);
    --c-obs:      #a86a2a;     /* warm bronze for observation dots */
    --c-next:     var(--accent);
    --c-acq:      #4d6a8f;     /* muted steel blue for acquisition curve */
    --c-acq-fill: rgba(77, 106, 143, 0.14);
    --c-grid:     rgba(74, 70, 64, 0.07);
    --c-axis:     rgba(74, 70, 64, 0.25);
    --c-tick:     var(--ink-mute);

    max-width: var(--read);
    margin: 32px auto 36px;
    background: var(--paper-2);
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 20px;
  }

  @media (prefers-color-scheme: dark) {
    .article-body .bo-widget {
      --c-band:     rgba(224, 122, 74, 0.18);
      --c-true:     rgba(184, 176, 160, 0.45);
      --c-obs:      #d4a048;
      --c-acq:      #7aa6c9;
      --c-acq-fill: rgba(122, 166, 201, 0.16);
      --c-grid:     rgba(184, 176, 160, 0.08);
      --c-axis:     rgba(184, 176, 160, 0.25);
    }
  }

  .article-body .bo-plot { margin: 0 0 14px; }
  .article-body .bo-plot-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    flex-wrap: wrap;
    margin: 0 0 8px;
    padding: 0 2px;
  }
  .article-body .bo-plot-label {
    font-family: var(--f-ui);
    font-size: .74rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-mute);
  }
  .article-body .bo-legend {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    font-family: var(--f-ui);
    font-size: .66rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-mute);
  }
  .article-body .bo-leg { display: inline-flex; align-items: center; gap: 5px; }
  .article-body .bo-legend .sw { display: inline-block; }
  .article-body .bo-legend .sw.line-mean { width: 16px; height: 2px; background: var(--c-gp-mean); }
  .article-body .bo-legend .sw.line-acq  { width: 16px; height: 2px; background: var(--c-acq); }
  .article-body .bo-legend .sw.line-true { width: 16px; height: 0; border-top: 1.5px dashed var(--c-true); }
  .article-body .bo-legend .sw.fill-band { width: 16px; height: 8px; background: var(--c-band); border-radius: 1px; }
  .article-body .bo-legend .sw.dot-obs   { width: 7px; height: 7px; border-radius: 50%; background: var(--c-obs); }
  .article-body .bo-legend .sw.dot-next  { width: 7px; height: 7px; border-radius: 50%; background: var(--c-next); }

  .article-body .bo-canvas-outer {
    position: relative;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: 2px;
  }
  .article-body .bo-obj { display: block; width: 100%; height: 340px; cursor: crosshair; }
  .article-body .bo-acq { display: block; width: 100%; height: 150px; }

  .article-body .bo-tooltip {
    position: absolute;
    pointer-events: none;
    background: var(--ink);
    color: var(--paper);
    font-family: var(--f-ui);
    font-size: .7rem;
    letter-spacing: .04em;
    padding: 5px 9px;
    border-radius: 2px;
    opacity: 0;
    transition: opacity .12s ease;
    white-space: nowrap;
    transform: translate(0, -100%);
    z-index: 2;
  }
  .article-body .bo-tooltip.visible { opacity: 0.95; }

  .article-body .bo-controls {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 14px;
    margin: 18px 0 0;
  }
  .article-body .bo-panel {
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 12px 14px;
  }
  .article-body .bo-panel-label {
    font-family: var(--f-ui);
    font-size: .68rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-mute);
    margin: 0 0 10px;
  }

  .article-body .bo-btn-group {
    display: flex;
    gap: 4px;
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 2px;
    margin: 0 0 12px;
  }
  .article-body .bo-btn-group button {
    flex: 1;
    font-family: var(--f-ui);
    font-size: .74rem;
    font-weight: 400;
    letter-spacing: .08em;
    text-transform: uppercase;
    background: transparent;
    color: var(--ink-soft);
    border: none;
    padding: 7px 4px;
    cursor: pointer;
    border-radius: 1px;
    transition: background .15s ease, color .15s ease;
  }
  .article-body .bo-btn-group button:hover { color: var(--ink); background: var(--paper-2); }
  .article-body .bo-btn-group button.active {
    background: var(--accent);
    color: var(--paper);
  }

  .article-body .bo-slider { margin: 10px 0; }
  .article-body .bo-slider-label {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-family: var(--f-ui);
    font-size: .72rem;
    letter-spacing: .03em;
    color: var(--ink-soft);
    margin: 0 0 6px;
  }
  .article-body .bo-slider-label .val {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
  }
  .article-body .bo-widget input[type="range"] {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 3px;
    background: var(--rule);
    border-radius: 2px;
    outline: none;
    margin: 4px 0;
  }
  .article-body .bo-widget input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 15px; height: 15px;
    background: var(--accent);
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid var(--paper);
    box-shadow: 0 0 0 1px var(--accent);
  }
  .article-body .bo-widget input[type="range"]::-moz-range-thumb {
    width: 13px; height: 13px;
    background: var(--accent);
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid var(--paper);
    box-shadow: 0 0 0 1px var(--accent);
  }

  .article-body .bo-toggle-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-family: var(--f-ui);
    font-size: .74rem;
    color: var(--ink-soft);
    margin: 14px 0 2px;
  }
  .article-body .bo-toggle {
    width: 30px;
    height: 16px;
    background: var(--rule);
    border-radius: 8px;
    position: relative;
    cursor: pointer;
    transition: background .2s ease;
  }
  .article-body .bo-toggle::after {
    content: "";
    position: absolute;
    top: 2px; left: 2px;
    width: 12px; height: 12px;
    background: var(--paper);
    border-radius: 50%;
    transition: transform .2s ease;
    box-shadow: 0 1px 2px rgba(0,0,0,0.2);
  }
  .article-body .bo-toggle.on { background: var(--accent); }
  .article-body .bo-toggle.on::after { transform: translateX(14px); }

  .article-body .bo-action-row {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
  }
  .article-body .bo-action-row:last-child { margin-bottom: 0; }
  .article-body .bo-btn,
  .article-body .bo-btn-primary {
    flex: 1;
    font-family: var(--f-ui);
    font-size: .74rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    border: 1px solid var(--rule);
    padding: 9px 10px;
    cursor: pointer;
    border-radius: 2px;
    transition: background .15s ease, color .15s ease, border-color .15s ease;
  }
  .article-body .bo-btn {
    background: transparent;
    color: var(--ink);
  }
  .article-body .bo-btn:hover {
    border-color: var(--ink-mute);
    background: var(--paper-2);
  }
  .article-body .bo-btn-primary {
    background: var(--accent);
    color: var(--paper);
    border-color: var(--accent);
    font-weight: 500;
  }
  .article-body .bo-btn-primary:hover { filter: brightness(0.93); }

  .article-body .bo-status {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
    gap: 10px;
    margin: 14px 0 0;
    padding: 14px 16px;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: 2px;
  }
  .article-body .bo-stat {
    display: flex;
    flex-direction: column;
    gap: 3px;
  }
  .article-body .bo-stat .k {
    font-family: var(--f-ui);
    font-size: .62rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-mute);
  }
  .article-body .bo-stat .v {
    font-family: var(--f-ui);
    font-size: .95rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
  }
  .article-body .bo-stat .v.best { color: var(--c-gp-mean); }
  .article-body .bo-stat .v.next { color: var(--c-next); }
  .article-body .bo-stat .v.muted { color: var(--ink-mute); font-weight: 400; }
</style>

<script>
(function() {
  var objCanvas = document.getElementById('objCanvas');
  var acqCanvas = document.getElementById('acqCanvas');
  if (!objCanvas || !acqCanvas) return;
  var objCtx = objCanvas.getContext('2d');
  var acqCtx = acqCanvas.getContext('2d');

  // Pull a CSS variable from the widget scope so colors live in the stylesheet
  var widgetEl = objCanvas.closest('.bo-widget');
  function cv(name, fallback) {
    var v = getComputedStyle(widgetEl).getPropertyValue(name).trim();
    return v || fallback;
  }

  // ---------- Objective: Forrester (2008) ----------
  function forrester(x) {
    var a = 6 * x - 2;
    return a * a * Math.sin(12 * x - 4);
  }
  var X_MIN = 0, X_MAX = 1;
  var Y_MIN = -9, Y_MAX = 18;

  // ---------- Linear algebra ----------
  function choleskyDecompose(A) {
    var n = A.length;
    var L = [];
    for (var i = 0; i < n; i++) L.push(new Float64Array(n));
    for (var i2 = 0; i2 < n; i2++) {
      for (var j = 0; j <= i2; j++) {
        var sum = 0;
        for (var k = 0; k < j; k++) sum += L[i2][k] * L[j][k];
        if (i2 === j) L[i2][j] = Math.sqrt(Math.max(A[i2][i2] - sum, 1e-12));
        else L[i2][j] = (A[i2][j] - sum) / L[j][j];
      }
    }
    return L;
  }
  function forwardSub(L, b) {
    var n = L.length;
    var y = new Float64Array(n);
    for (var i = 0; i < n; i++) {
      var s = b[i];
      for (var k = 0; k < i; k++) s -= L[i][k] * y[k];
      y[i] = s / L[i][i];
    }
    return y;
  }
  function backSubLT(L, y) {
    var n = L.length;
    var x = new Float64Array(n);
    for (var i = n - 1; i >= 0; i--) {
      var s = y[i];
      for (var k = i + 1; k < n; k++) s -= L[k][i] * x[k];
      x[i] = s / L[i][i];
    }
    return x;
  }

  function rbfKernel(x1, x2, ls, sf2) {
    var d = x1 - x2;
    return sf2 * Math.exp(-0.5 * d * d / (ls * ls));
  }

  function gpFit(X, y, ls, sf2, sn2) {
    var n = X.length;
    var K = [];
    for (var i = 0; i < n; i++) K.push(new Float64Array(n));
    for (var i2 = 0; i2 < n; i2++) {
      for (var j = 0; j < n; j++) K[i2][j] = rbfKernel(X[i2], X[j], ls, sf2);
      K[i2][i2] += sn2;
    }
    var L = choleskyDecompose(K);
    var alpha = backSubLT(L, forwardSub(L, y));
    return { L: L, alpha: alpha, X: X, ls: ls, sf2: sf2 };
  }

  function gpPredict(model, Xtest) {
    var L = model.L, alpha = model.alpha, X = model.X, ls = model.ls, sf2 = model.sf2;
    var n = X.length;
    var out = [];
    for (var t = 0; t < Xtest.length; t++) {
      var xs = Xtest[t];
      var kstar = new Float64Array(n);
      for (var i = 0; i < n; i++) kstar[i] = rbfKernel(X[i], xs, ls, sf2);
      var mean = 0;
      for (var i2 = 0; i2 < n; i2++) mean += kstar[i2] * alpha[i2];
      var v = forwardSub(L, kstar);
      var vtv = 0;
      for (var i3 = 0; i3 < n; i3++) vtv += v[i3] * v[i3];
      var variance = Math.max(sf2 - vtv, 1e-10);
      out.push({ mean: mean, std: Math.sqrt(variance) });
    }
    return out;
  }

  // Standard normal pdf / cdf (Abramowitz & Stegun 7.1.26)
  function phi(z) { return Math.exp(-0.5 * z * z) / Math.sqrt(2 * Math.PI); }
  function Phi(z) {
    var a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
    var a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
    var sign = z < 0 ? -1 : 1;
    var x = Math.abs(z) / Math.sqrt(2);
    var t = 1.0 / (1.0 + p * x);
    var y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
    return 0.5 * (1.0 + sign * y);
  }

  function acqEI(mean, std, fStar, xi) {
    if (std < 1e-9) return 0;
    var imp = fStar - mean - xi;
    var z = imp / std;
    return imp * Phi(z) + std * phi(z);
  }
  function acqUCB(mean, std, kappa) {
    // For minimization: LCB = μ - κσ; return -LCB so we can argmax
    return kappa * std - mean;
  }
  function acqPI(mean, std, fStar, xi) {
    if (std < 1e-9) return 0;
    return Phi((fStar - mean - xi) / std);
  }

  // ---------- State ----------
  var STATE = {
    obs: [],
    ls: 0.10,
    sf2: 9.0,
    sn2: 1e-4,
    acq: 'EI',
    kappa: 2.0,
    xi: 0.01,
    showTrue: true,
    iter: 0,
    nextX: null,
    grid: null,
    gpPred: null,
    acqVals: null
  };
  var GRID_N = 300;

  function buildGrid() {
    var g = new Float64Array(GRID_N);
    for (var i = 0; i < GRID_N; i++) g[i] = X_MIN + (X_MAX - X_MIN) * i / (GRID_N - 1);
    STATE.grid = g;
  }
  buildGrid();

  function seedInitial() {
    STATE.obs = [];
    var seeds = [0.08, 0.45, 0.92];
    for (var i = 0; i < seeds.length; i++) {
      var xx = seeds[i];
      STATE.obs.push({ x: xx, y: forrester(xx) });
    }
    STATE.iter = 0;
  }

  function currentBestFstar() {
    var fmin = Infinity, xmin = null;
    for (var i = 0; i < STATE.obs.length; i++) {
      var o = STATE.obs[i];
      if (o.y < fmin) { fmin = o.y; xmin = o.x; }
    }
    return { fmin: fmin, xmin: xmin };
  }

  function refit() {
    var X = STATE.obs.map(function(o) { return o.x; });
    var y = STATE.obs.map(function(o) { return o.y; });
    var model = gpFit(X, y, STATE.ls, STATE.sf2, STATE.sn2);
    var preds = gpPredict(model, STATE.grid);
    STATE.gpPred = preds;

    var fmin = currentBestFstar().fmin;
    var acq = new Float64Array(GRID_N);
    for (var i = 0; i < GRID_N; i++) {
      var m = preds[i].mean, s = preds[i].std;
      var a;
      if (STATE.acq === 'EI') a = acqEI(m, s, fmin, STATE.xi);
      else if (STATE.acq === 'UCB') a = acqUCB(m, s, STATE.kappa);
      else a = acqPI(m, s, fmin, STATE.xi);
      acq[i] = a;
    }
    STATE.acqVals = acq;

    var best = -Infinity, bi = 0;
    for (var j = 0; j < GRID_N; j++) {
      if (acq[j] > best) { best = acq[j]; bi = j; }
    }
    STATE.nextX = STATE.grid[bi];
  }

  // ---------- Canvas + transforms ----------
  var PAD_L = 48, PAD_R = 18, PAD_T = 12, PAD_B = 28;
  function resizeCanvases() {
    var dpr = window.devicePixelRatio || 1;
    [objCanvas, acqCanvas].forEach(function(cv) {
      var rect = cv.parentElement.getBoundingClientRect();
      var w = rect.width;
      var h = cv.clientHeight;
      cv.width = Math.floor(w * dpr);
      cv.height = Math.floor(h * dpr);
      cv.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0);
    });
    draw();
  }
  function xToPx(x, w) { return PAD_L + (x - X_MIN) / (X_MAX - X_MIN) * (w - PAD_L - PAD_R); }
  function pxToX(px, w) { return X_MIN + (px - PAD_L) / (w - PAD_L - PAD_R) * (X_MAX - X_MIN); }
  function yToPxObj(y, h) { return PAD_T + (1 - (y - Y_MIN) / (Y_MAX - Y_MIN)) * (h - PAD_T - PAD_B); }

  function drawAxes(ctx, w, h, yMin, yMax) {
    var grid = cv('--c-grid', 'rgba(0,0,0,0.06)');
    var axis = cv('--c-axis', 'rgba(0,0,0,0.2)');
    var tick = cv('--c-tick', '#8a8277');

    ctx.save();
    ctx.strokeStyle = grid;
    ctx.lineWidth = 1;
    for (var t = 0; t <= 5; t++) {
      var xv = X_MIN + t / 5 * (X_MAX - X_MIN);
      var px = xToPx(xv, w);
      ctx.beginPath(); ctx.moveTo(px, PAD_T); ctx.lineTo(px, h - PAD_B); ctx.stroke();
    }
    for (var t2 = 0; t2 <= 5; t2++) {
      var yv = yMin + t2 / 5 * (yMax - yMin);
      var py = PAD_T + (1 - (yv - yMin) / (yMax - yMin)) * (h - PAD_T - PAD_B);
      ctx.beginPath(); ctx.moveTo(PAD_L, py); ctx.lineTo(w - PAD_R, py); ctx.stroke();
    }
    ctx.strokeStyle = axis;
    ctx.beginPath();
    ctx.moveTo(PAD_L, PAD_T);
    ctx.lineTo(PAD_L, h - PAD_B);
    ctx.lineTo(w - PAD_R, h - PAD_B);
    ctx.stroke();

    ctx.fillStyle = tick;
    ctx.font = '10px "DM Mono", ui-monospace, monospace';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';
    for (var t3 = 0; t3 <= 5; t3++) {
      var xv2 = X_MIN + t3 / 5 * (X_MAX - X_MIN);
      ctx.fillText(xv2.toFixed(1), xToPx(xv2, w), h - PAD_B + 6);
    }
    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    for (var t4 = 0; t4 <= 5; t4++) {
      var yv2 = yMin + t4 / 5 * (yMax - yMin);
      var py2 = PAD_T + (1 - (yv2 - yMin) / (yMax - yMin)) * (h - PAD_T - PAD_B);
      ctx.fillText(yv2.toFixed(1), PAD_L - 6, py2);
    }
    ctx.restore();
  }

  function drawObjectivePlot() {
    var w = objCanvas.clientWidth;
    var h = objCanvas.clientHeight;
    objCtx.clearRect(0, 0, w, h);
    drawAxes(objCtx, w, h, Y_MIN, Y_MAX);
    if (!STATE.gpPred) return;

    var g = STATE.grid;
    var p = STATE.gpPred;

    // GP ±2σ band
    objCtx.save();
    objCtx.beginPath();
    for (var i = 0; i < g.length; i++) {
      var px = xToPx(g[i], w);
      var upper = p[i].mean + 2 * p[i].std;
      var py = yToPxObj(Math.min(Math.max(upper, Y_MIN), Y_MAX), h);
      if (i === 0) objCtx.moveTo(px, py); else objCtx.lineTo(px, py);
    }
    for (var i2 = g.length - 1; i2 >= 0; i2--) {
      var px2 = xToPx(g[i2], w);
      var lower = p[i2].mean - 2 * p[i2].std;
      var py2 = yToPxObj(Math.min(Math.max(lower, Y_MIN), Y_MAX), h);
      objCtx.lineTo(px2, py2);
    }
    objCtx.closePath();
    objCtx.fillStyle = cv('--c-band', 'rgba(185,74,27,0.15)');
    objCtx.fill();
    objCtx.restore();

    // True f(x) dashed
    if (STATE.showTrue) {
      objCtx.save();
      objCtx.strokeStyle = cv('--c-true', 'rgba(74,70,64,0.5)');
      objCtx.lineWidth = 1.2;
      objCtx.setLineDash([4, 4]);
      objCtx.beginPath();
      var N = 200;
      for (var k = 0; k <= N; k++) {
        var x = X_MIN + k / N * (X_MAX - X_MIN);
        var yv = forrester(x);
        var px3 = xToPx(x, w);
        var py3 = yToPxObj(Math.min(Math.max(yv, Y_MIN), Y_MAX), h);
        if (k === 0) objCtx.moveTo(px3, py3); else objCtx.lineTo(px3, py3);
      }
      objCtx.stroke();
      objCtx.restore();
    }

    // GP posterior mean
    objCtx.save();
    objCtx.strokeStyle = cv('--c-gp-mean', '#1a1814');
    objCtx.lineWidth = 2;
    objCtx.beginPath();
    for (var i3 = 0; i3 < g.length; i3++) {
      var px4 = xToPx(g[i3], w);
      var py4 = yToPxObj(Math.min(Math.max(p[i3].mean, Y_MIN), Y_MAX), h);
      if (i3 === 0) objCtx.moveTo(px4, py4); else objCtx.lineTo(px4, py4);
    }
    objCtx.stroke();
    objCtx.restore();

    // Next-candidate vertical guide + marker
    if (STATE.nextX !== null) {
      objCtx.save();
      objCtx.strokeStyle = cv('--c-next', '#b94a1b');
      objCtx.globalAlpha = 0.38;
      objCtx.lineWidth = 1;
      objCtx.setLineDash([3, 4]);
      var pxn = xToPx(STATE.nextX, w);
      objCtx.beginPath();
      objCtx.moveTo(pxn, PAD_T);
      objCtx.lineTo(pxn, h - PAD_B);
      objCtx.stroke();
      objCtx.globalAlpha = 1;
      objCtx.setLineDash([]);

      var near = 0, bd = Infinity;
      for (var i4 = 0; i4 < g.length; i4++) {
        var d = Math.abs(g[i4] - STATE.nextX);
        if (d < bd) { bd = d; near = i4; }
      }
      var pyn = yToPxObj(Math.min(Math.max(p[near].mean, Y_MIN), Y_MAX), h);
      objCtx.fillStyle = cv('--c-next', '#b94a1b');
      objCtx.beginPath();
      objCtx.arc(pxn, pyn, 5, 0, 2 * Math.PI);
      objCtx.fill();
      objCtx.strokeStyle = cv('--paper', '#f4efe6');
      objCtx.lineWidth = 2;
      objCtx.stroke();
      objCtx.restore();
    }

    // Observations
    var xmin = currentBestFstar().xmin;
    objCtx.save();
    for (var oi = 0; oi < STATE.obs.length; oi++) {
      var o = STATE.obs[oi];
      var pxo = xToPx(o.x, w);
      var pyo = yToPxObj(Math.min(Math.max(o.y, Y_MIN), Y_MAX), h);
      var isBest = (o.x === xmin);
      objCtx.fillStyle = cv('--c-obs', '#a86a2a');
      objCtx.strokeStyle = cv('--paper', '#f4efe6');
      objCtx.lineWidth = 2;
      objCtx.beginPath();
      objCtx.arc(pxo, pyo, isBest ? 6 : 4.5, 0, 2 * Math.PI);
      objCtx.fill();
      objCtx.stroke();
      if (isBest) {
        objCtx.strokeStyle = cv('--c-obs', '#a86a2a');
        objCtx.globalAlpha = 0.45;
        objCtx.lineWidth = 1;
        objCtx.beginPath();
        objCtx.arc(pxo, pyo, 10, 0, 2 * Math.PI);
        objCtx.stroke();
        objCtx.globalAlpha = 1;
      }
    }
    objCtx.restore();
  }

  function drawAcquisitionPlot() {
    var w = acqCanvas.clientWidth;
    var h = acqCanvas.clientHeight;
    acqCtx.clearRect(0, 0, w, h);
    if (!STATE.acqVals) { drawAxes(acqCtx, w, h, 0, 1); return; }

    var minA = Infinity, maxA = -Infinity;
    for (var v = 0; v < STATE.acqVals.length; v++) {
      var val = STATE.acqVals[v];
      if (val < minA) minA = val;
      if (val > maxA) maxA = val;
    }
    if (maxA - minA < 1e-6) maxA = minA + 1e-6;
    var pad = (maxA - minA) * 0.08;
    var yMin = minA - pad, yMax = maxA + pad;
    drawAxes(acqCtx, w, h, yMin, yMax);

    var g = STATE.grid;
    // Filled area
    acqCtx.save();
    acqCtx.beginPath();
    for (var i = 0; i < g.length; i++) {
      var px = xToPx(g[i], w);
      var val2 = STATE.acqVals[i];
      var py = PAD_T + (1 - (val2 - yMin) / (yMax - yMin)) * (h - PAD_T - PAD_B);
      if (i === 0) acqCtx.moveTo(px, py); else acqCtx.lineTo(px, py);
    }
    acqCtx.lineTo(xToPx(g[g.length - 1], w), h - PAD_B);
    acqCtx.lineTo(xToPx(g[0], w), h - PAD_B);
    acqCtx.closePath();
    acqCtx.fillStyle = cv('--c-acq-fill', 'rgba(77,106,143,0.14)');
    acqCtx.fill();
    acqCtx.restore();

    // Curve
    acqCtx.save();
    acqCtx.strokeStyle = cv('--c-acq', '#4d6a8f');
    acqCtx.lineWidth = 2;
    acqCtx.beginPath();
    for (var i2 = 0; i2 < g.length; i2++) {
      var px2 = xToPx(g[i2], w);
      var val3 = STATE.acqVals[i2];
      var py2 = PAD_T + (1 - (val3 - yMin) / (yMax - yMin)) * (h - PAD_T - PAD_B);
      if (i2 === 0) acqCtx.moveTo(px2, py2); else acqCtx.lineTo(px2, py2);
    }
    acqCtx.stroke();
    acqCtx.restore();

    // argmax marker
    if (STATE.nextX !== null) {
      var bi = 0, bd = Infinity;
      for (var j = 0; j < g.length; j++) {
        var d = Math.abs(g[j] - STATE.nextX);
        if (d < bd) { bd = d; bi = j; }
      }
      var pxm = xToPx(g[bi], w);
      var vv = STATE.acqVals[bi];
      var pym = PAD_T + (1 - (vv - yMin) / (yMax - yMin)) * (h - PAD_T - PAD_B);
      acqCtx.save();
      acqCtx.strokeStyle = cv('--c-next', '#b94a1b');
      acqCtx.globalAlpha = 0.38;
      acqCtx.lineWidth = 1;
      acqCtx.setLineDash([3, 4]);
      acqCtx.beginPath();
      acqCtx.moveTo(pxm, PAD_T);
      acqCtx.lineTo(pxm, h - PAD_B);
      acqCtx.stroke();
      acqCtx.globalAlpha = 1;
      acqCtx.setLineDash([]);
      acqCtx.fillStyle = cv('--c-next', '#b94a1b');
      acqCtx.beginPath();
      acqCtx.arc(pxm, pym, 5, 0, 2 * Math.PI);
      acqCtx.fill();
      acqCtx.strokeStyle = cv('--paper', '#f4efe6');
      acqCtx.lineWidth = 2;
      acqCtx.stroke();
      acqCtx.restore();
    }
  }

  function updateStatus() {
    document.getElementById('iterStat').textContent = STATE.iter;
    document.getElementById('nObsStat').textContent = STATE.obs.length;
    var b = currentBestFstar();
    document.getElementById('bestStat').textContent = isFinite(b.fmin) ? b.fmin.toFixed(3) : '—';
    document.getElementById('bestXStat').textContent = b.xmin !== null ? b.xmin.toFixed(3) : '—';
    document.getElementById('nextStat').textContent = STATE.nextX !== null ? STATE.nextX.toFixed(3) : '—';
    var names = { EI: 'Expected Improvement', UCB: 'Lower Confidence Bound', PI: 'Probability of Improvement' };
    document.getElementById('acqName').textContent = names[STATE.acq];
  }

  function draw() { drawObjectivePlot(); drawAcquisitionPlot(); updateStatus(); }

  function stepOnce() {
    if (STATE.nextX === null) return;
    var x = STATE.nextX;
    var dup = STATE.obs.some(function(o) { return Math.abs(o.x - x) < 1e-4; });
    if (dup) return;
    STATE.obs.push({ x: x, y: forrester(x) });
    STATE.iter += 1;
    refit();
    draw();
  }
  function addManualObservation(x) {
    if (x < X_MIN || x > X_MAX) return;
    var dup = STATE.obs.some(function(o) { return Math.abs(o.x - x) < 1e-3; });
    if (dup) return;
    STATE.obs.push({ x: x, y: forrester(x) });
    STATE.iter += 1;
    refit();
    draw();
  }
  function reset() { seedInitial(); refit(); draw(); }

  document.getElementById('stepBtn').addEventListener('click', stepOnce);
  document.getElementById('resetBtn').addEventListener('click', reset);
  document.getElementById('auto10Btn').addEventListener('click', function() {
    var count = 0;
    (function runner() {
      if (count >= 10) return;
      stepOnce();
      count += 1;
      setTimeout(runner, 220);
    })();
  });

  document.querySelectorAll('#acqGroup button').forEach(function(btn) {
    btn.addEventListener('click', function() {
      document.querySelectorAll('#acqGroup button').forEach(function(b) { b.classList.remove('active'); });
      btn.classList.add('active');
      STATE.acq = btn.dataset.acq;
      refit();
      draw();
    });
  });

  function bindSlider(id, valId, fmt, setter) {
    var s = document.getElementById(id);
    var vEl = document.getElementById(valId);
    s.addEventListener('input', function() {
      var v = parseFloat(s.value);
      setter(v);
      vEl.textContent = fmt(v);
      refit();
      draw();
    });
  }
  bindSlider('lsSlider', 'lsVal', function(v) { return v.toFixed(2); }, function(v) { STATE.ls = v; });
  bindSlider('kappaSlider', 'kappaVal', function(v) { return v.toFixed(1); }, function(v) { STATE.kappa = v; });
  bindSlider('xiSlider', 'xiVal', function(v) { return v.toFixed(2); }, function(v) { STATE.xi = v; });

  var trueToggle = document.getElementById('trueToggle');
  function toggleTrue() {
    STATE.showTrue = !STATE.showTrue;
    trueToggle.classList.toggle('on', STATE.showTrue);
    trueToggle.setAttribute('aria-pressed', String(STATE.showTrue));
    draw();
  }
  trueToggle.addEventListener('click', toggleTrue);
  trueToggle.addEventListener('keydown', function(e) {
    if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggleTrue(); }
  });

  // Click on objective canvas → add observation
  objCanvas.addEventListener('click', function(e) {
    var rect = objCanvas.getBoundingClientRect();
    var w = rect.width;
    var px = e.clientX - rect.left;
    var x = pxToX(px, w);
    if (x >= X_MIN && x <= X_MAX) addManualObservation(x);
  });

  // Hover tooltip
  var tooltip = document.getElementById('objTooltip');
  objCanvas.addEventListener('mousemove', function(e) {
    if (!STATE.gpPred) return;
    var rect = objCanvas.getBoundingClientRect();
    var w = rect.width;
    var px = e.clientX - rect.left;
    var x = pxToX(px, w);
    if (x < X_MIN || x > X_MAX) { tooltip.classList.remove('visible'); return; }
    var bi = 0, bd = Infinity;
    for (var i = 0; i < STATE.grid.length; i++) {
      var d = Math.abs(STATE.grid[i] - x);
      if (d < bd) { bd = d; bi = i; }
    }
    var mp = STATE.gpPred[bi];
    tooltip.textContent = 'x=' + x.toFixed(3) + '  μ=' + mp.mean.toFixed(2) + '  σ=' + mp.std.toFixed(2);
    tooltip.style.left = (px + 12) + 'px';
    tooltip.style.top = (e.clientY - rect.top - 6) + 'px';
    tooltip.classList.add('visible');
  });
  objCanvas.addEventListener('mouseleave', function() { tooltip.classList.remove('visible'); });

  // Init
  window.addEventListener('resize', resizeCanvases);
  seedInitial();
  refit();
  resizeCanvases();
})();
</script>]]></content><author><name>Majid Mazouchi</name></author><category term="Machine Learning" /><summary type="html"><![CDATA[A working Bayesian optimization loop you can run in your browser. Watch Expected Improvement, UCB, and PI compete to find the minimum of a deceptive benchmark function — and read the math that makes it work.]]></summary></entry><entry><title type="html">Gaussian Processes — an Interactive Explainer</title><link href="https://majid-mazouchi.github.io/autonomy/posts/gaussian-processes-interactive-explainer/" rel="alternate" type="text/html" title="Gaussian Processes — an Interactive Explainer" /><published>2026-04-19T09:00:00-04:00</published><updated>2026-04-19T09:00:00-04:00</updated><id>https://majid-mazouchi.github.io/autonomy/posts/gaussian-processes-interactive-explainer</id><content type="html" xml:base="https://majid-mazouchi.github.io/autonomy/posts/gaussian-processes-interactive-explainer/"><![CDATA[<p>A Gaussian Process is one of those ideas that looks abstract on paper and clicks the moment you can actually <em>play</em> with one. The object of this post is that moment. Below is an interactive plot where the prior, the posterior, and all the kernel hyperparameters respond in real time. The prose around it tries to match what you’re seeing on the screen to the math underneath — and, in the last section, to the very un-glamorous reality of the $O(n^3)$ cost that determines when GPs earn their keep.</p>

<h2 id="what-is-a-gp-really">What is a GP, really?</h2>

<div class="callout-soft">

  <p><strong>The plain-English version.</strong> Imagine you’re trying to guess an unknown function from just a few measurements. A Gaussian Process is a principled way of saying: “<em>here are all the functions I think are plausible</em>” — and then updating that belief every time you see a new data point.</p>

  <p>Three ideas to hold onto:</p>

  <ol>
    <li>It’s a <em>distribution over functions</em>, not over numbers. Instead of “$x = 3.2 \pm 0.5$”, a GP gives you “the function could be this shape, or this one, or this one” — an infinite family of curves, each with a probability.</li>
    <li>The <em>kernel</em> encodes your assumption about smoothness: points that are close in input should have similar output values. That one assumption is enough to turn a handful of measurements into a full curve with confidence bounds.</li>
    <li>The magic is <em>calibrated uncertainty</em>. Near your data the GP is confident, far from it the GP is humble and the error bars grow. That honesty is what makes GPs useful for control, optimization, and diagnostics — the model tells you when not to trust it.</li>
  </ol>

</div>

<p>A one-line mental model: a GP is <strong>linear regression with infinitely many features</strong>, where the kernel silently handles the infinite sum for you.</p>

<h2 id="the-one-sentence-definition">The one-sentence definition</h2>

<p>A <strong>Gaussian Process</strong> is a distribution over functions such that any finite collection of function values has a joint Gaussian distribution. It is fully specified by a mean function $m(x)$ and a covariance (kernel) function $k(x, x’)$:</p>

\[f(x) \sim \mathcal{GP}\!\left( m(x),\; k(x, x') \right)\]

<p>In practice we almost always set $m(x) = 0$ (after centering the data) and do all the modeling work through the kernel.</p>

<h2 id="interactive-demo">Interactive demo</h2>

<div class="gp-widget">

  <div class="mode-bar">
    <button id="mode-prior" type="button">Prior (no data)</button>
    <button id="mode-post" class="active" type="button">Posterior (with data)</button>
    <button id="clear" type="button">Clear points</button>
    <button id="resample" type="button">Resample</button>
  </div>

  <div class="hint" id="mode-hint">Posterior: click on the plot to add (or remove) observations. The GP conditions on them — the mean threads through the points, variance collapses nearby, and stays high far away.</div>

  <canvas id="gp-canvas" width="780" height="400"></canvas>

  <div class="legend">
    <div class="item"><span class="swatch mean"></span> Posterior mean</div>
    <div class="item"><span class="swatch band"></span> 95% credible band</div>
    <div class="item"><span class="swatch sample"></span> Sampled functions</div>
    <div class="item"><span class="dot-swatch"></span> Observations</div>
  </div>

  <div class="controls">
    <div class="control">
      <label>Length scale ℓ <span class="val" id="ls-val">1.00</span></label>
      <input type="range" id="ls" min="0.1" max="3.0" step="0.05" value="1.0" />
      <p class="caption">Smoothness of sampled functions</p>
    </div>
    <div class="control">
      <label>Signal variance σ<sub>f</sub>² <span class="val" id="sf-val">1.00</span></label>
      <input type="range" id="sf" min="0.1" max="3.0" step="0.05" value="1.0" />
      <p class="caption">Vertical amplitude of functions</p>
    </div>
    <div class="control">
      <label>Noise σ<sub>n</sub> <span class="val" id="sn-val">0.10</span></label>
      <input type="range" id="sn" min="0.001" max="0.5" step="0.005" value="0.1" />
      <p class="caption">Observation noise std</p>
    </div>
    <div class="control">
      <label>Sample paths <span class="val" id="ns-val">5</span></label>
      <input type="range" id="ns" min="0" max="10" step="1" value="5" />
      <p class="caption">Number of drawn functions</p>
    </div>
  </div>

  <div class="kernel-box">
    <div class="k-label">Kernel: squared exponential (RBF)</div>
    <div class="k-eq">k(x, x') = σ<sub>f</sub>² · exp( −(x − x')² / (2ℓ²) )</div>
  </div>

</div>

<h2 id="how-to-read-the-plot">How to read the plot</h2>

<h3 id="the-prior">The prior</h3>

<p>Click <em>Prior</em> and slide <strong>Sample paths</strong> up. Every colored curve is one function drawn from $\mathcal{GP}(0, k)$. The shaded band is the 95% credible region. Before seeing any data, the GP says “the function could be any of these.”</p>

<p>The length scale $\ell$ controls how wiggly these samples are:</p>

<ul>
  <li><strong>Small $\ell$</strong> — nearby inputs are only weakly correlated, so samples look rough.</li>
  <li><strong>Large $\ell$</strong> — strong correlation, so samples look smooth.</li>
</ul>

<p>The signal variance $\sigma_f^2$ scales the vertical amplitude of the prior.</p>

<h3 id="the-posterior">The posterior</h3>

<p>Click <em>Posterior</em> and then click anywhere on the plot to drop observations. Click an existing point to remove it. Two things happen:</p>

<ul>
  <li>The <strong>mean</strong> curve threads through the observations (or very close, limited by the noise $\sigma_n$).</li>
  <li>The <strong>uncertainty band</strong> collapses near data points and re-opens in regions with no data.</li>
</ul>

<blockquote>
  <p><strong>Key takeaway.</strong> This is the main selling point for GPs: <em>calibrated uncertainty that grows where you haven’t looked</em>. That’s exactly why they shine in sparse-data regimes like active learning, Bayesian optimization, and safe control.</p>
</blockquote>

<h2 id="the-math-in-three-lines">The math in three lines</h2>

<p>Given training data $(X, y)$ with i.i.d. Gaussian noise $\sigma_n^2$, the joint distribution of the observed targets and the function value $f_<em>$ at a test point $x_</em>$ is:</p>

\[\begin{bmatrix} y \\ f_* \end{bmatrix}
\sim \mathcal{N}\!\left(
\begin{bmatrix} 0 \\ 0 \end{bmatrix},\;
\begin{bmatrix}
K(X,X) + \sigma_n^2 I &amp; K(X, x_*) \\
K(x_*, X) &amp; K(x_*, x_*)
\end{bmatrix}
\right)\]

<p>Conditioning on $y$ using the standard Gaussian conditioning identity gives closed-form posterior mean and variance:</p>

\[\mu_* = K(x_*, X) \left[ K(X,X) + \sigma_n^2 I \right]^{-1} y\]

\[\sigma_*^2 = K(x_*, x_*) - K(x_*, X) \left[ K(X,X) + \sigma_n^2 I \right]^{-1} K(X, x_*)\]

<p>The widget implements exactly this. For numerical stability we use a Cholesky decomposition:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>K + σ_n²·I = L · Lᵀ       (Cholesky, O(n³))
α = Lᵀ \ (L \ y)           (triangular solves)
μ*  = k*ᵀ · α              (mean at test point)
v   = L \ k*
σ*² = k(x*, x*) − vᵀv      (variance at test point)
</code></pre></div></div>

<h2 id="common-kernels">Common kernels</h2>

<ul>
  <li><strong>Squared Exponential (RBF).</strong> Infinitely differentiable. Produces very smooth functions. Default choice, used in the demo above.</li>
  <li><strong>Matérn ($\nu = 3/2,\ 5/2$).</strong> Controllable smoothness. Often more realistic than RBF for physical signals that are continuous but not infinitely smooth.</li>
  <li><strong>Periodic.</strong> Encodes periodicity with a fixed period. Good for oscillatory signals like motor ripple or seasonal effects.</li>
  <li><strong>Linear.</strong> Recovers Bayesian linear regression as a special case.</li>
  <li><strong>Sums and products.</strong> Kernels are closed under addition and multiplication, so you can compose them — e.g. <code class="language-plaintext highlighter-rouge">Periodic · RBF</code> for a slowly-decaying oscillation.</li>
</ul>

<h2 id="hyperparameter-learning">Hyperparameter learning</h2>

<p>The kernel hyperparameters $\theta = {\ell, \sigma_f, \sigma_n}$ are typically learned by maximizing the <strong>log marginal likelihood</strong> of the observed data:</p>

\[\log p(y \mid X, \theta) = -\tfrac{1}{2}\, y^{\!\top} \!\left[K + \sigma_n^2 I\right]^{-1} y \;-\; \tfrac{1}{2} \log \!\left| K + \sigma_n^2 I \right| \;-\; \tfrac{n}{2} \log 2\pi\]

<p>The three terms have a clean interpretation: <em>data fit</em>, <em>complexity penalty</em>, and a constant. This is the built-in Occam’s razor that makes GPs elegant — overly wiggly models are penalized by the log-determinant term.</p>

<h2 id="computational-complexity">Computational complexity</h2>

<p>GPs are conceptually clean but computationally heavy. The cost is dominated by a single operation: inverting (or Cholesky-factorizing) the $n \times n$ kernel matrix $K + \sigma_n^2 I$, where $n$ is the number of training points.</p>

<h3 id="exact-gp--the-honest-numbers">Exact GP — the honest numbers</h3>

<table>
  <thead>
    <tr>
      <th>Operation</th>
      <th>Time</th>
      <th>Memory</th>
      <th>What’s happening</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Training (one-time)</td>
      <td><code class="language-plaintext highlighter-rouge">O(n³)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(n²)</code></td>
      <td>Build <code class="language-plaintext highlighter-rouge">K</code>, do Cholesky <code class="language-plaintext highlighter-rouge">K = LLᵀ</code>, solve for <code class="language-plaintext highlighter-rouge">α = K⁻¹y</code></td>
    </tr>
    <tr>
      <td>Predict mean (per test point)</td>
      <td><code class="language-plaintext highlighter-rouge">O(n)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(n)</code></td>
      <td>Inner product <code class="language-plaintext highlighter-rouge">k*ᵀα</code> — cheap once <code class="language-plaintext highlighter-rouge">α</code> is cached</td>
    </tr>
    <tr>
      <td>Predict variance (per test point)</td>
      <td><code class="language-plaintext highlighter-rouge">O(n²)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(n)</code></td>
      <td>Triangular solve <code class="language-plaintext highlighter-rouge">v = L \ k*</code>, then <code class="language-plaintext highlighter-rouge">σ*² = k** − vᵀv</code></td>
    </tr>
    <tr>
      <td>Hyperparameter learning (per iter.)</td>
      <td><code class="language-plaintext highlighter-rouge">O(n³)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(n²)</code></td>
      <td>New <code class="language-plaintext highlighter-rouge">θ</code> → rebuild <code class="language-plaintext highlighter-rouge">K</code> → redo Cholesky → gradient of log-marginal-likelihood</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Rule of thumb.</strong> Exact GP is comfortable up to a few thousand points on a laptop. At $n \approx 10{,}000$ you’re hitting the wall (memory for $K$ alone is roughly 800 MB in float64, and one Cholesky takes minutes). Beyond that, you need approximations.</p>
</blockquote>

<h3 id="why-its-on3">Why it’s $O(n^3)$</h3>

<p>The $O(n^3)$ cost comes from the Cholesky decomposition of the kernel matrix, which is the dominant numerical step. Every time hyperparameters change during training, the matrix changes, and the whole factorization has to be redone. Mean prediction at a new test point is cheap because it’s just a dot product against a precomputed vector. Variance prediction is more expensive because each test point needs its own triangular solve against $L$.</p>

<h3 id="scaling-beyond-exact-gp">Scaling beyond exact GP</h3>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Training</th>
      <th>Prediction</th>
      <th>Idea</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Exact GP</td>
      <td><code class="language-plaintext highlighter-rouge">O(n³)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(n²)</code></td>
      <td>Full Cholesky — baseline</td>
    </tr>
    <tr>
      <td>Sparse GP / FITC</td>
      <td><code class="language-plaintext highlighter-rouge">O(n·m²)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(m²)</code></td>
      <td><code class="language-plaintext highlighter-rouge">m</code> inducing points summarize <code class="language-plaintext highlighter-rouge">n</code> training points</td>
    </tr>
    <tr>
      <td>SVGP (variational)</td>
      <td><code class="language-plaintext highlighter-rouge">O(m³)</code> per batch</td>
      <td><code class="language-plaintext highlighter-rouge">O(m²)</code></td>
      <td>Mini-batch training; scales to millions of points</td>
    </tr>
    <tr>
      <td>KISS-GP / SKI</td>
      <td><code class="language-plaintext highlighter-rouge">O(n + m log m)</code></td>
      <td><code class="language-plaintext highlighter-rouge">O(1)</code> amortized</td>
      <td>Structured kernel interpolation on a grid</td>
    </tr>
    <tr>
      <td>Local GPs</td>
      <td><code class="language-plaintext highlighter-rouge">O(k³)</code> per local model</td>
      <td><code class="language-plaintext highlighter-rouge">O(k²)</code></td>
      <td>Partition input space, fit a small GP per region</td>
    </tr>
  </tbody>
</table>

<p>Here $m$ is the number of inducing (pseudo-)points, usually $m \ll n$, and $k$ is the local neighborhood size. In practice, $m$ in the range 50–500 is common.</p>

<blockquote>
  <p><strong>Embedded context.</strong> For real-time control (GP-augmented MPC, for instance), even the $O(n)$ or $O(m)$ prediction cost matters. Common tricks: freeze hyperparameters offline, precompute $\alpha$, use a small fixed inducing set, or switch to a parametric approximation once the GP has been learned.</p>
</blockquote>

<h2 id="when-to-reach-for-a-gp">When to reach for a GP</h2>

<ul>
  <li><strong>Small-to-medium data</strong> where uncertainty quantification matters more than raw throughput (GP inference is $O(n^3)$, impractical beyond ~10k points without approximations).</li>
  <li><strong>Bayesian optimization</strong> — GP surrogate + acquisition function (EI, UCB) for expensive black-box optimization.</li>
  <li><strong>Active learning</strong> — pick the next query where posterior variance is highest.</li>
  <li><strong>Safe / uncertainty-aware control</strong> — GP-augmented MPC uses the GP posterior mean to correct model mismatch and the variance to gate how aggressively the controller trusts that correction.</li>
  <li><strong>System identification and calibration</strong> — non-parametric regression with confidence bounds that grow in extrapolation regions.</li>
  <li><strong>Diagnostics and prognostics</strong> — detect when the current operating point is out-of-distribution relative to training data.</li>
</ul>

<h2 id="limitations-to-be-honest-about">Limitations to be honest about</h2>

<ul>
  <li><strong>Scaling.</strong> Naïve GP is $O(n^3)$ / $O(n^2)$. Sparse variational GPs, inducing points, and structured kernels (KISS-GP, SKI) push this further.</li>
  <li><strong>High input dimensions.</strong> Stationary kernels suffer in high-$D$ without ARD or dimensionality reduction.</li>
  <li><strong>Kernel choice matters.</strong> A misspecified kernel gives confidently wrong uncertainty — the variance is only calibrated <em>given</em> the model.</li>
  <li><strong>Non-Gaussian likelihoods.</strong> Classification and count data need approximations (Laplace, EP, variational).</li>
</ul>

<hr />

<p>The interactive demo above is implemented with vanilla JavaScript and the Canvas 2D API — Cholesky solve, posterior sampling, and kernel evaluation are all in-browser, no external libraries. View source on the page if you want to read it.</p>

<!-- =====================================================
     Post-scoped styles for the interactive GP widget
     Reuses the blog's CSS variables (paper, ink, rule, accent)
     so it harmonizes with the warm editorial theme.
     ===================================================== -->
<style>
  .article-body .callout-soft {
    background: var(--paper-2);
    border-left: 3px solid var(--accent);
    border-radius: 0 4px 4px 0;
    padding: 18px 24px;
    margin: 28px auto;
    max-width: var(--read);
    font-size: 1rem;
  }
  .article-body .callout-soft p { margin: 0 0 12px; }
  .article-body .callout-soft p:last-child { margin-bottom: 0; }
  .article-body .callout-soft ol { padding-left: 20px; margin: 0 0 0; }
  .article-body .callout-soft li { margin: 8px 0; }

  .article-body .gp-widget {
    max-width: var(--read);
    margin: 28px auto 36px;
    background: var(--paper-2);
    border: 1px solid var(--rule);
    border-radius: 4px;
    padding: 22px;
    font-family: var(--f-body);
  }

  .article-body .gp-widget .mode-bar {
    display: flex;
    gap: 8px;
    margin: 0 0 14px;
    flex-wrap: wrap;
  }
  .article-body .gp-widget button {
    font-family: var(--f-ui);
    font-size: .74rem;
    font-weight: 400;
    letter-spacing: .08em;
    text-transform: uppercase;
    background: transparent;
    color: var(--ink);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 8px 12px;
    cursor: pointer;
    transition: background .15s ease, color .15s ease, border-color .15s ease;
  }
  .article-body .gp-widget button:hover {
    background: var(--paper);
    border-color: var(--ink-mute);
  }
  .article-body .gp-widget button.active {
    background: var(--accent);
    color: var(--paper);
    border-color: var(--accent);
  }

  .article-body .gp-widget .hint {
    font-family: var(--f-body);
    font-size: .92rem;
    font-style: italic;
    color: var(--ink-soft);
    margin: 0 0 14px;
    line-height: 1.5;
  }

  .article-body .gp-widget canvas#gp-canvas {
    width: 100%;
    height: auto;
    display: block;
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: 2px;
    cursor: crosshair;
    margin: 0 0 14px;
  }

  .article-body .gp-widget .legend {
    display: flex;
    gap: 18px;
    flex-wrap: wrap;
    font-family: var(--f-ui);
    font-size: .72rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ink-mute);
    margin: 0 0 18px;
  }
  .article-body .gp-widget .legend .item {
    display: inline-flex; align-items: center; gap: 6px;
  }
  .article-body .gp-widget .swatch { display: inline-block; width: 18px; height: 2px; }
  .article-body .gp-widget .swatch.mean { background: var(--ink); }
  .article-body .gp-widget .swatch.sample { background: var(--accent); }
  .article-body .gp-widget .swatch.band {
    height: 10px;
    background: rgba(185, 74, 27, 0.18);
    border-radius: 1px;
  }
  .article-body .gp-widget .dot-swatch {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--ink);
  }

  .article-body .gp-widget .controls {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 18px;
    margin: 0 0 4px;
  }
  .article-body .gp-widget .control label {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-family: var(--f-ui);
    font-size: .76rem;
    letter-spacing: .04em;
    color: var(--ink-soft);
    margin: 0 0 6px;
  }
  .article-body .gp-widget .control label .val {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
  }
  .article-body .gp-widget .control .caption {
    font-family: var(--f-body);
    font-size: .82rem;
    font-style: italic;
    color: var(--ink-mute);
    margin: 6px 0 0;
  }

  .article-body .gp-widget input[type="range"] {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 3px;
    background: var(--rule);
    border-radius: 2px;
    outline: none;
    margin: 6px 0;
  }
  .article-body .gp-widget input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px; height: 16px;
    background: var(--accent);
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid var(--paper);
    box-shadow: 0 0 0 1px var(--accent);
  }
  .article-body .gp-widget input[type="range"]::-moz-range-thumb {
    width: 14px; height: 14px;
    background: var(--accent);
    border-radius: 50%;
    cursor: pointer;
    border: 2px solid var(--paper);
    box-shadow: 0 0 0 1px var(--accent);
  }

  .article-body .gp-widget .kernel-box {
    background: var(--paper);
    border: 1px solid var(--rule);
    border-radius: 2px;
    padding: 12px 16px;
    margin: 18px 0 0;
    font-size: .9rem;
  }
  .article-body .gp-widget .kernel-box .k-label {
    font-family: var(--f-ui);
    font-size: .72rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-mute);
    margin-bottom: 6px;
  }
  .article-body .gp-widget .kernel-box .k-eq {
    font-family: var(--f-ui);
    color: var(--ink);
    font-size: .88rem;
  }

  /* Tables within the article body */
  .article-body table {
    width: 100%;
    max-width: var(--read);
    margin: 24px auto;
    border-collapse: collapse;
    font-size: .94rem;
  }
  .article-body table th,
  .article-body table td {
    text-align: left;
    padding: 10px 12px;
    border-bottom: 1px solid var(--rule);
    vertical-align: top;
  }
  .article-body table th {
    font-family: var(--f-ui);
    font-weight: 500;
    font-size: .74rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-mute);
    border-bottom: 1px solid var(--ink);
  }
  .article-body table td:first-child { font-weight: 500; }
  .article-body table code {
    background: var(--tag-bg);
    color: var(--ink);
    font-size: .86em;
  }
</style>

<script>
(function() {
  var canvas = document.getElementById('gp-canvas');
  if (!canvas) return;
  var ctx = canvas.getContext('2d');

  function getCSS(name) {
    return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
  }

  // Warm palette that harmonizes with the blog's burnt-orange accent
  var SAMPLE_COLORS = [
    '#b94a1b', // accent — burnt orange
    '#4d6a8f', // muted steel blue
    '#6b8a3f', // olive green
    '#8a4a6b', // dusty burgundy
    '#a86a2a', // bronze
    '#3f7a6b', // teal
    '#7a4a3a', // brick
    '#5a6a4a', // moss
    '#9a6a4a', // sienna
    '#4a4a7a'  // indigo
  ];

  function resizeCanvas() {
    var dpr = window.devicePixelRatio || 1;
    var rect = canvas.getBoundingClientRect();
    canvas.width = rect.width * dpr;
    canvas.height = 400 * dpr;
    canvas.style.height = '400px';
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    draw();
  }

  var W = function() { return canvas.getBoundingClientRect().width; };
  var H = function() { return 400; };
  var PAD_L = 48, PAD_R = 20, PAD_T = 20, PAD_B = 40;
  var plotW = function() { return W() - PAD_L - PAD_R; };
  var plotH = function() { return H() - PAD_T - PAD_B; };

  var X_MIN = -5, X_MAX = 5;
  var Y_MIN = -3, Y_MAX = 3;

  var N_TEST = 120;
  var xTest = [];
  for (var i = 0; i < N_TEST; i++) xTest.push(X_MIN + (X_MAX - X_MIN) * i / (N_TEST - 1));

  var mode = 'post';
  var points = [{x: -3, y: 0.5}, {x: -1, y: -0.8}, {x: 1.5, y: 1.2}, {x: 3, y: -0.3}];
  var ls = 1.0, sf = 1.0, sn = 0.1, nSamples = 5;
  var sampleSeed = 42;

  function xToPx(x) { return PAD_L + (x - X_MIN) / (X_MAX - X_MIN) * plotW(); }
  function yToPx(y) { return PAD_T + (Y_MAX - y) / (Y_MAX - Y_MIN) * plotH(); }
  function pxToX(px) { return X_MIN + (px - PAD_L) / plotW() * (X_MAX - X_MIN); }
  function pxToY(py) { return Y_MAX - (py - PAD_T) / plotH() * (Y_MAX - Y_MIN); }

  function rbf(x1, x2) {
    var d = x1 - x2;
    return sf * sf * Math.exp(-(d * d) / (2 * ls * ls));
  }

  function buildK(xs) {
    var n = xs.length;
    var K = [];
    for (var i = 0; i < n; i++) {
      K.push([]);
      for (var j = 0; j < n; j++) K[i].push(rbf(xs[i], xs[j]));
    }
    return K;
  }

  function cholesky(A) {
    var n = A.length;
    var L = [];
    for (var i = 0; i < n; i++) L.push(new Array(n).fill(0));
    for (var i2 = 0; i2 < n; i2++) {
      for (var j = 0; j <= i2; j++) {
        var s = 0;
        for (var k = 0; k < j; k++) s += L[i2][k] * L[j][k];
        if (i2 === j) {
          var v = A[i2][i2] - s;
          L[i2][j] = Math.sqrt(Math.max(v, 1e-10));
        } else {
          L[i2][j] = (A[i2][j] - s) / L[j][j];
        }
      }
    }
    return L;
  }

  function solveLower(L, b) {
    var n = L.length;
    var y = new Array(n).fill(0);
    for (var i = 0; i < n; i++) {
      var s = 0;
      for (var k = 0; k < i; k++) s += L[i][k] * y[k];
      y[i] = (b[i] - s) / L[i][i];
    }
    return y;
  }

  function solveUpper(LT, b) {
    var n = LT.length;
    var x = new Array(n).fill(0);
    for (var i = n - 1; i >= 0; i--) {
      var s = 0;
      for (var k = i + 1; k < n; k++) s += LT[i][k] * x[k];
      x[i] = (b[i] - s) / LT[i][i];
    }
    return x;
  }

  function transpose(A) {
    var n = A.length, m = A[0].length;
    var T = [];
    for (var i = 0; i < m; i++) {
      T.push([]);
      for (var j = 0; j < n; j++) T[i].push(A[j][i]);
    }
    return T;
  }

  var rngState = sampleSeed;
  function seed(s) { rngState = s; }
  function rand() {
    rngState = (rngState * 1664525 + 1013904223) % 4294967296;
    return rngState / 4294967296;
  }
  function randn() {
    var u = Math.max(rand(), 1e-10);
    var v = rand();
    return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
  }

  function computeGP() {
    var n = points.length;
    var mean = new Array(N_TEST).fill(0);
    var variance = new Array(N_TEST).fill(sf * sf);
    var cholPost = null;

    if (mode === 'post' && n > 0) {
      var xs = points.map(function(p) { return p.x; });
      var ys = points.map(function(p) { return p.y; });

      var Kxx = buildK(xs);
      for (var i = 0; i < n; i++) Kxx[i][i] += sn * sn;

      var L = cholesky(Kxx);
      var alpha = solveUpper(transpose(L), solveLower(L, ys));

      var Kxs = [];
      for (var i2 = 0; i2 < n; i2++) {
        Kxs.push([]);
        for (var j = 0; j < N_TEST; j++) Kxs[i2].push(rbf(xs[i2], xTest[j]));
      }

      for (var j2 = 0; j2 < N_TEST; j2++) {
        var m = 0;
        for (var i3 = 0; i3 < n; i3++) m += Kxs[i3][j2] * alpha[i3];
        mean[j2] = m;
      }

      for (var j3 = 0; j3 < N_TEST; j3++) {
        var kxs = Kxs.map(function(row) { return row[j3]; });
        var v2 = solveLower(L, kxs);
        var vv = 0;
        for (var i4 = 0; i4 < n; i4++) vv += v2[i4] * v2[i4];
        variance[j3] = Math.max(sf * sf - vv, 1e-8);
      }

      var Kss = buildK(xTest);
      var KsxT = [];
      for (var i5 = 0; i5 < N_TEST; i5++) {
        KsxT.push([]);
        for (var k = 0; k < n; k++) KsxT[i5].push(rbf(xTest[i5], xs[k]));
      }
      var Kpost = [];
      for (var i6 = 0; i6 < N_TEST; i6++) {
        Kpost.push([]);
        var vi = solveLower(L, KsxT[i6]);
        for (var j4 = 0; j4 < N_TEST; j4++) {
          var vj = solveLower(L, KsxT[j4]);
          var dot = 0;
          for (var k2 = 0; k2 < n; k2++) dot += vi[k2] * vj[k2];
          Kpost[i6].push(Kss[i6][j4] - dot + (i6 === j4 ? 1e-6 : 0));
        }
      }
      cholPost = cholesky(Kpost);
    } else {
      var Kss2 = buildK(xTest);
      for (var i7 = 0; i7 < N_TEST; i7++) Kss2[i7][i7] += 1e-6;
      cholPost = cholesky(Kss2);
    }

    return { mean: mean, variance: variance, cholPost: cholPost };
  }

  function drawSample(chol, mean) {
    var n = chol.length;
    var z = [];
    for (var i = 0; i < n; i++) z.push(randn());
    var sample = new Array(n).fill(0);
    for (var i2 = 0; i2 < n; i2++) {
      var s = 0;
      for (var k = 0; k <= i2; k++) s += chol[i2][k] * z[k];
      sample[i2] = mean[i2] + s;
    }
    return sample;
  }

  function bandFillColor() {
    // Pull the accent from the document and use it for the credible band
    var acc = getCSS('--accent') || '#b94a1b';
    // Derive an rgba from the hex so it works in both light/dark
    if (acc[0] === '#' && (acc.length === 7 || acc.length === 4)) {
      var r, g, b;
      if (acc.length === 7) {
        r = parseInt(acc.slice(1, 3), 16);
        g = parseInt(acc.slice(3, 5), 16);
        b = parseInt(acc.slice(5, 7), 16);
      } else {
        r = parseInt(acc[1] + acc[1], 16);
        g = parseInt(acc[2] + acc[2], 16);
        b = parseInt(acc[3] + acc[3], 16);
      }
      return 'rgba(' + r + ',' + g + ',' + b + ',0.18)';
    }
    return 'rgba(185,74,27,0.18)';
  }

  function draw() {
    var textCol  = getCSS('--ink')      || '#1a1814';
    var textMute = getCSS('--ink-mute') || '#8a8277';
    var rule     = getCSS('--rule')     || '#c8bfae';
    var paper    = getCSS('--paper')    || '#f4efe6';

    var w = W(), h = H();
    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = paper;
    ctx.fillRect(0, 0, w, h);

    // Grid
    ctx.strokeStyle = rule;
    ctx.lineWidth = 0.5;
    ctx.setLineDash([3, 3]);
    for (var gx = Math.ceil(X_MIN); gx <= X_MAX; gx++) {
      ctx.beginPath();
      ctx.moveTo(xToPx(gx), PAD_T);
      ctx.lineTo(xToPx(gx), PAD_T + plotH());
      ctx.stroke();
    }
    for (var gy = Math.ceil(Y_MIN); gy <= Y_MAX; gy++) {
      ctx.beginPath();
      ctx.moveTo(PAD_L, yToPx(gy));
      ctx.lineTo(PAD_L + plotW(), yToPx(gy));
      ctx.stroke();
    }
    ctx.setLineDash([]);

    // Axes
    ctx.strokeStyle = textMute;
    ctx.lineWidth = 0.5;
    ctx.beginPath();
    ctx.moveTo(PAD_L, PAD_T);
    ctx.lineTo(PAD_L, PAD_T + plotH());
    ctx.lineTo(PAD_L + plotW(), PAD_T + plotH());
    ctx.stroke();

    // Labels
    ctx.fillStyle = textMute;
    ctx.font = '11px ' + getComputedStyle(document.body).fontFamily;
    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    for (var gy2 = Math.ceil(Y_MIN); gy2 <= Y_MAX; gy2++) {
      ctx.fillText(gy2.toFixed(0), PAD_L - 8, yToPx(gy2));
    }
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';
    for (var gx2 = Math.ceil(X_MIN); gx2 <= X_MAX; gx2++) {
      ctx.fillText(gx2.toFixed(0), xToPx(gx2), PAD_T + plotH() + 8);
    }
    ctx.fillText('x', PAD_L + plotW() / 2, PAD_T + plotH() + 22);
    ctx.save();
    ctx.translate(16, PAD_T + plotH() / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('f(x)', 0, 0);
    ctx.restore();

    var r = computeGP();
    var mean = r.mean, variance = r.variance, cholPost = r.cholPost;

    // Credible band
    ctx.fillStyle = bandFillColor();
    ctx.beginPath();
    for (var i = 0; i < N_TEST; i++) {
      var std = Math.sqrt(variance[i]);
      var upper = mean[i] + 2 * std;
      var px = xToPx(xTest[i]);
      var py = yToPx(Math.min(upper, Y_MAX));
      if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
    }
    for (var i2 = N_TEST - 1; i2 >= 0; i2--) {
      var std2 = Math.sqrt(variance[i2]);
      var lower = mean[i2] - 2 * std2;
      ctx.lineTo(xToPx(xTest[i2]), yToPx(Math.max(lower, Y_MIN)));
    }
    ctx.closePath();
    ctx.fill();

    // Sampled functions
    seed(sampleSeed);
    for (var s = 0; s < nSamples; s++) {
      var sample = drawSample(cholPost, mean);
      ctx.strokeStyle = SAMPLE_COLORS[s % SAMPLE_COLORS.length];
      ctx.globalAlpha = 0.72;
      ctx.lineWidth = 1.3;
      ctx.beginPath();
      for (var i3 = 0; i3 < N_TEST; i3++) {
        var px2 = xToPx(xTest[i3]);
        var py2 = yToPx(sample[i3]);
        if (i3 === 0) ctx.moveTo(px2, py2); else ctx.lineTo(px2, py2);
      }
      ctx.stroke();
    }
    ctx.globalAlpha = 1;

    // Posterior mean
    ctx.strokeStyle = textCol;
    ctx.lineWidth = 2;
    ctx.beginPath();
    for (var i4 = 0; i4 < N_TEST; i4++) {
      var px3 = xToPx(xTest[i4]);
      var py3 = yToPx(mean[i4]);
      if (i4 === 0) ctx.moveTo(px3, py3); else ctx.lineTo(px3, py3);
    }
    ctx.stroke();

    // Observations
    if (mode === 'post') {
      for (var pi = 0; pi < points.length; pi++) {
        var p = points[pi];
        ctx.fillStyle = textCol;
        ctx.beginPath();
        ctx.arc(xToPx(p.x), yToPx(p.y), 5, 0, 2 * Math.PI);
        ctx.fill();
        ctx.strokeStyle = paper;
        ctx.lineWidth = 1.5;
        ctx.stroke();
      }
    }

    // Mode label
    ctx.fillStyle = textMute;
    ctx.font = '11px ' + getComputedStyle(document.body).fontFamily;
    ctx.textAlign = 'left';
    ctx.textBaseline = 'top';
    var label = mode === 'prior'
      ? 'Prior GP · no data'
      : 'Posterior GP · ' + points.length + ' observation' + (points.length === 1 ? '' : 's');
    ctx.fillText(label, PAD_L + 8, PAD_T + 6);
  }

  canvas.addEventListener('click', function(e) {
    if (mode !== 'post') return;
    var rect = canvas.getBoundingClientRect();
    var px = e.clientX - rect.left;
    var py = e.clientY - rect.top;
    if (px < PAD_L || px > PAD_L + plotW() || py < PAD_T || py > PAD_T + plotH()) return;
    var x = pxToX(px), y = pxToY(py);

    for (var i = 0; i < points.length; i++) {
      if (Math.abs(points[i].x - x) < 0.25 && Math.abs(points[i].y - y) < 0.25) {
        points.splice(i, 1);
        draw();
        return;
      }
    }
    points.push({x: x, y: y});
    draw();
  });

  function bindSlider(id, out, fmt, setter) {
    var el = document.getElementById(id);
    var o = document.getElementById(out);
    el.addEventListener('input', function() {
      var v = parseFloat(el.value);
      setter(v);
      o.textContent = fmt(v);
      draw();
    });
  }

  bindSlider('ls', 'ls-val', function(v) { return v.toFixed(2); }, function(v) { ls = v; });
  bindSlider('sf', 'sf-val', function(v) { return v.toFixed(2); }, function(v) { sf = v; });
  bindSlider('sn', 'sn-val', function(v) { return v.toFixed(2); }, function(v) { sn = v; });
  bindSlider('ns', 'ns-val', function(v) { return v.toFixed(0); }, function(v) { nSamples = v; });

  var btnPrior = document.getElementById('mode-prior');
  var btnPost  = document.getElementById('mode-post');

  btnPrior.addEventListener('click', function() {
    mode = 'prior';
    btnPrior.classList.add('active');
    btnPost.classList.remove('active');
    document.getElementById('mode-hint').textContent =
      'Prior: no data. Functions are drawn from GP(0, k). Adjust ℓ and σ_f to see how the kernel shapes the prior distribution.';
    draw();
  });

  btnPost.addEventListener('click', function() {
    mode = 'post';
    btnPost.classList.add('active');
    btnPrior.classList.remove('active');
    document.getElementById('mode-hint').textContent =
      'Posterior: click on the plot to add (or remove) observations. The GP conditions on them — the mean threads through the points, variance collapses nearby, and stays high far away.';
    draw();
  });

  document.getElementById('clear').addEventListener('click', function() {
    points = [];
    draw();
  });

  document.getElementById('resample').addEventListener('click', function() {
    sampleSeed = Math.floor(Math.random() * 1000000);
    draw();
  });

  window.addEventListener('resize', resizeCanvas);
  resizeCanvas();
})();
</script>]]></content><author><name>Majid Mazouchi</name></author><category term="Machine Learning" /><summary type="html"><![CDATA[Click, slide, and watch the posterior update. A working intuition for Gaussian Processes — from the one-sentence definition through the Cholesky math and the honest O(n³) scaling story.]]></summary></entry></feed>