<?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="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-10-11T13:26:16+00:00</updated><id>/feed.xml</id><title type="html">Ludwig Winkler</title><subtitle>Jekyll version of the Massively theme by HTML5UP</subtitle><entry><title type="html">Basics with Matrices</title><link href="/blog/BasicsWithMatrics/" rel="alternate" type="text/html" title="Basics with Matrices" /><published>2026-09-03T00:00:00+00:00</published><updated>2026-09-03T00:00:00+00:00</updated><id>/blog/BasicsWithMatrics</id><content type="html" xml:base="/blog/BasicsWithMatrics/"><![CDATA[<script>
MathJax = {
  tex: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\\[','\\]']],
    processEscapes: true,
    tags: 'all'
  },
  options: {
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
  }
};
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async=""></script>

<h3 id="enter-the-matrix">Enter the Matrix</h3>

<p>A matrix is little else than plain linear scaling but written in the most space and time saving way.</p>

<p>Take the equation \(y = a \cdot x\) which scales a number $x$ by a factor of $a$.
What happens if we have multiple numbers and want to scale them all at once?
For that we introduce little subscripts to indicate the first $x$ as $x_1$ and the second $x$ as $x_2$. Similarly, we denote the corresponding outputs as $y_1$ and $y_2$ and the scaling turning $x_1$ into $y_1$ as $a_1$ and the scaling turning $x_2$ into $y_2$ as $a_2$.
So for example we would have $y_1 = a_1 \cdot x_1$ and $y_2 = a_2 \cdot x_2$ and so on.
Stacking the $y_1$ and $y_2$ equations, we can it out as</p>

\[\begin{align*}
y_1 = a_1 \cdot x_1 \\
y_2 = a_2 \cdot x_2
\end{align*}\]

<p>In the equations above we’re clearly using $x_i$ as a single input to obtain a corresponding output $y_i$.
Since Indian mathematicians came up with the concept of $0$ a long time ago, we can also theoretically add all the $x_i$’s to both equations by properly multiplying them by zero:</p>

\[\begin{align*}
y_1 = a_1 \cdot x_1 + 0 \cdot x_2 \\
y_2 = 0 \cdot x_1 + a_2 \cdot x_2
\end{align*}\]

<p>But now we for each $y_i$ have an expression that involves all the $x_i$’s, not just a single one.
If we now want to distinguish that a particular $a_i$ is the scaling we use to scale a particular $x_j$, we need a way to indicate both what input $x_i$ is scaled how for each output $y_i$.
So we have to denote each $a$ by it’s input and output indices $a_{\text{output} \leftarrow {\text{input}}}$.
So we get</p>

\[\begin{align*}
y_1 = a_{1 \leftarrow 1} \cdot x_1 + \overbrace{a_{1 \leftarrow 2}}^{=0} \cdot x_2 \\
y_2 = \underbrace{a_{2 \leftarrow 1}}_{=0} \cdot x_1 + a_{2 \leftarrow 2} \cdot x_2
\end{align*}\]

<p>Writing arrows is cumbersome, especially for larger matrices, which is why mathematicians prefer the compact notation and we simply write $a_{ij}$ instead of $a_{i \leftarrow j}$ how input $x_j$ affects output $y_i$.
Since this is just a sum, we can also write it as such:</p>

\[\begin{align}
y_1 &amp;= \sum_{j} a_{1j} \cdot x_j \\
y_2 &amp;= \sum_{j} a_{2j} \cdot x_j
\end{align}\]

<p>Mathematicians are a lazy bunch, sorry, efficient bunch and they quickly become bored at writing out all these little subscripts $_1$ and $_2$ all the time, so they thought hard about how to write the two equations in a more compact form.
The $y$’s, $a$’s and $x$’s only differ in their subscripts so we should be able to collect all the $y$’s, $a$’s and $x$’s into some sort of bundle up object that represents all of them at once, which leads us to the concept of vectors and matrices.</p>

<p>Let’s first collect the $y$’s and $x$’s and introduce introduce a collective object $\mathbf{x} = ( x_1 , x_2)$ and $\mathbf{y} = ( y_1 , y_2)$.
This is straight forward as both $\mathbf{x}$ and $\mathbf{y}$ only have one dimension which is denoted by its single subscript.
The $a$’s form a two-dimensional array since they have two subscripts which we call a matrix, and we denote it by</p>

<p>\(\mathbf{A} = \begin{bmatrix} a_{11} &amp; a_{12} \\ a_{21} &amp; a_{22} \end{bmatrix}\).</p>

<p>This brings us to the workhorse of linear algebra, the matrix-vector multiplication:</p>

\[\begin{align*}
\mathbf{y} &amp;= \mathbf{A} \mathbf{x} \\
\begin{bmatrix} y_1 \\ y_2 \end{bmatrix} &amp;= \begin{bmatrix} a_{11} &amp; a_{12} \\ a_{21} &amp; a_{22} \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} \\
 &amp;= \begin{bmatrix} a_{11} \cdot x_1 &amp; + &amp; a_{12} \cdot x_2 \\  a_{21} \cdot x_1 &amp; + &amp; a_{22} \cdot x_2 \end{bmatrix} \\
\end{align*}\]

<p>As a simple starting point, consider the diagonal matrix</p>

\[\mathbf{A} = \begin{bmatrix} a_{11} &amp; 0 \\ 0 &amp; a_{22} \end{bmatrix}.\]

<p>But wait a second, if we squint really closely, we can rediscover the stacking we did with the vector $\mathbf{x}$ and $\mathbf{y}$ in the matrix $\mathbf{A}$ by interpreting the columns of the matrix as vectors as well:</p>

\[\mathbf{A}
= \begin{bmatrix} a_{11} &amp; 0 \\ 0 &amp; a_{22} \end{bmatrix}
= \begin{bmatrix}
\begin{bmatrix} a_{11} \\ 0 \end{bmatrix} &amp;
\begin{bmatrix} 0 \\ a_{22} \end{bmatrix}
\end{bmatrix}.\]

<p>so our matrix $\mathbf{A}$ can be seen as a collection of vectors as well!</p>

<p>With this representation of a matrix, we can now reinterpret matrix-vector multiplication as a combination of the matrix’s column vectors:</p>

\[\begin{align*}
\mathbf{y} &amp;= \mathbf{A} \mathbf{x} \\
\begin{bmatrix} y_1 \\ y_2 \end{bmatrix} &amp;= \begin{bmatrix}
\begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix} &amp;
\begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix}
\end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} \\
&amp;= \begin{bmatrix} \begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix} x_1 + \begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix} x_2
\end{bmatrix} \\
&amp;= \begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix} x_1 + \begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix} x_2
\end{align*}\]

<p>This now opens up a new interpretation as we’ve initially considered $\mathbf{y} = \mathbf{Ax}$ as the scaling of the input $\mathbf{x}$ by their respective $a$ coefficients. Now, we can also see it as forming the output $\mathbf{y}$ by combining the column vectors of $\mathbf{A}$, each scaled by the corresponding component of $\mathbf{x}$.
Initially we thought about scaling $x$ with $a$, but now we’re flipping the script and scaling the column vectors in the matrix $\mathbf{A}$ by the components of $\mathbf{x}$.</p>

<p>The animation below highlights both of these approaches: We either interpret it as a scaling of the input $x$ by the values in $A$ through its column space. Alternatively, we interpret the linear transformation as a combination of the column space of $A$ through $x$.</p>

<style>
  /* Shared figure theme, emitted once with the first widget. */
  :is(#matrix-scaling-widget,#matrix-rank-widget,#rank-projection-widget,#eigenvector-widget,#determinant-diagonal-widget,#determinant-area-widget).matrix-figure{
    --line:#e2e8ec;
    --ink:#243340;
    --dim:#6b7c89;
    --blue:#2f6fad;
    --d1-gold:#7153b8;
    --w2-gold:#7153b8;
    --serif-body:"Helvetica Neue",Helvetica,Arial,sans-serif;
    --mono:ui-monospace,SFMono-Regular,Consolas,monospace;
    margin:32px 0;
    color:var(--ink);
    background:#fff;
    border:1px solid var(--line);
    border-radius:12px;
    box-shadow:none;
    font:400 14px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif;
    text-transform:none;
    & :is(.scaling-head,.rank-head,.projection-head,.eigen-head,.widget-titlewrap){
      padding:20px 24px;
      background:#fff;
      border-bottom:1px solid #e2e8ec;
    }
    & :is(h4,.widget-title){
      color:#243340;
      font:600 17px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
      letter-spacing:-0.02em;
    }
    & h5{color:#405361; font-size:14px; font-weight:500;}
    & :is(.eigen-kicker,.d1-kicker,.w2-kicker,.wt-sub){
      padding:0;
      color:#6b7c89;
      background:none;
      border:0;
      font:400 11px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
      letter-spacing:0.02em;
      text-transform:none;
    }
    & :is(.scaling-panel,.rank-panel,.projection-body,.eigen-body,.widget-body){padding:24px;}
    & :is(.rank-badge,.rank-dimension){color:#6b7c89; font-size:11px;}
    & :is(.projection-note,.rank-hint){color:#6b7c89; font-size:11px;}
    & :is(.eigen-plot-shell,.d1-plot-shell,.w2-plot-shell){
      position:relative;
      background:#f9fbfc;
      border:1px solid #e8eef1;
      border-radius:8px;
    }
    & :is(.eigen-plot,.projection-panel svg,canvas){background:#f9fbfc;}
    & .projection-panel svg{border-color:#e8eef1; border-radius:8px;}
    & :is(.eigen-legend,.d1-plot-key,.w2-plot-key){
      padding:10px 14px;
      color:#6b7c89;
      background:#f9fbfc;
      border-bottom:0;
      font:400 11px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
    }
    & :is(.eigen-metric,.d1-metric,.projection-readout){
      padding:12px 14px;
      background:#f4f7f9;
      border:0;
      border-radius:6px;
      font-size:13px;
    }
    & .eigen-metric[data-active="true"]{background:#edf7f4;}
    & :is(.eigen-metric-label,.d1-metric-label,.w2-summary-label,.w2-stage-label,.eq-label){
      color:#6b7c89;
      font:400 11px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
      letter-spacing:0;
      text-transform:none;
    }
    & :is(.eigen-metric strong,.d1-metric strong){font-size:16px; font-weight:500;}
    & :is(.eigen-actions,.d1-actions,.w2-actions){
      padding:14px 0 0;
      background:none;
      border:0;
      border-top:1px solid #e2e8ec;
      border-radius:0;
    }
    & :is(.eigen-feedback,.caption,.projection-message){
      color:#546977;
      font:400 12px/1.55 "Helvetica Neue",Helvetica,Arial,sans-serif;
      text-align:left;
    }
    & .eigen-feedback[data-found="true"]{color:#147f7a;}
    & :is(button,button.step-btn){
      padding:8px 12px;
      color:#405361 !important;
      background:#fff;
      border:1px solid #d4dfe5;
      border-radius:6px;
      box-shadow:none;
      font:500 11px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
      letter-spacing:0;
      text-transform:none;
      transition:background 160ms ease,border-color 160ms ease;
    }
    & :is(button,button.step-btn):hover{
      color:#147f7a !important;
      background:#edf7f4;
      border-color:#9bc6be;
    }
    & .w2-summary{
      padding:14px 0;
      background:none;
      border:0;
      border-bottom:1px solid #e2e8ec;
      border-radius:0;
    }
    & .w2-equation{padding:18px; background:#f9fbfc; border-color:#e8eef1;}
    & .eq-step{transition:color 180ms ease;}
    & .eq-step.is-current{color:#147f7a;}
    & .eq-step.is-current .eq-note{color:#147f7a;}
    & .eq-step.is-revealed{animation:matrix-equation-reveal 240ms ease-out;}
    & .move-hint{
      opacity:1;
      pointer-events:none;
      transition:opacity 180ms ease;
    }
    & .move-hint.is-dismissed{opacity:0;}
    & .figure-canvas-hint{
      position:absolute;
      z-index:1;
      padding:4px 7px;
      color:#147f7a;
      background:rgba(255,255,255,0.96);
      border:1px solid #d6e5e4;
      border-radius:3px;
      white-space:nowrap;
      font:400 10px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
    }
    & .figure-canvas-hint.second{color:#7153b8; border-color:#ded8ec;}
    & [data-w2-piece].is-emphasized{background:#edf3f6; border-radius:3px;}
    & .eigen-guides{transition:opacity 180ms ease;}
    & .eigen-angle-arc{transition:opacity 180ms ease;}
    & .scaling-panel[data-active-component="0"] .second,
    & .scaling-panel[data-active-component="1"] .first{opacity:0.4;}
    & .scaling-panel :is(.first,.second){transition:opacity 120ms ease;}
    & .rank-controls,
    & .scaling-comparison{background:#f9fbfc;}
    @media (max-width:520px){
      & :is(.scaling-head,.rank-head,.projection-head,.eigen-head,.widget-titlewrap,.scaling-panel,.rank-panel,.projection-body,.eigen-body,.widget-body){padding:16px;}
      & :is(h4,.widget-title){font-size:15px;}
      & .eigen-plot text{font-size:16px; stroke-width:3px;}
      & .eigen-plot .move-hint text{font-size:15px; stroke:none;}
      & #w2canvas{height:clamp(200px,61.75vw,420px);}
    }
    @media (prefers-reduced-motion:reduce){
      & *,
      & *::before,
      & *::after{animation:none !important; transition:none !important;}
    }
  }
  @keyframes matrix-equation-reveal{
    from{opacity:0; transform:translateY(4px);}
    to{opacity:1; transform:translateY(0);}
  }
  #matrix-scaling-widget{
    --line:#e2e6e9;
    --ink:#212931;
    --dim:#626d78;
    --teal:#147f7a;
    --amber:#a96100;
    --purple:#7153b8;
    margin:26px 0;
    color:var(--ink);
    background:#fff;
    border:1px solid var(--line);
    border-radius:8px;
    font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
    font-size:14px;
    font-weight:400;
    letter-spacing:0;
    line-height:1.5;
    text-align:left;
    text-transform:none;
  }
  #matrix-scaling-widget,
  #matrix-scaling-widget *{box-sizing:border-box;}
  #matrix-scaling-widget h4,
  #matrix-scaling-widget h5{
    margin:0;
    color:var(--ink);
    font:600 17px/1.35 "Helvetica Neue",Helvetica,Arial,sans-serif;
    letter-spacing:0;
    text-transform:none;
  }
  #matrix-scaling-widget h5{font-size:16px;}
  #matrix-scaling-widget p{margin:5px 0 0; font-size:13px; line-height:1.5; text-align:left;}
  #matrix-scaling-widget .scaling-head{padding:18px 20px; border-bottom:1px solid var(--line);}
  #matrix-scaling-widget .scaling-panels{display:grid; grid-template-columns:repeat(2,minmax(0,1fr));}
  #matrix-scaling-widget .scaling-panel{min-width:0; padding:20px;}
  #matrix-scaling-widget .scaling-panel + .scaling-panel{border-left:1px solid var(--line);}
  #matrix-scaling-widget .first{color:var(--teal);}
  #matrix-scaling-widget .second{color:var(--purple);}
  #matrix-scaling-widget .scaling-calculation{
    display:grid;
    grid-template-columns:max-content max-content;
    align-items:center;
    justify-content:center;
    gap:8px 6px;
    margin:14px 0 12px;
    font:13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;
  }
  #matrix-scaling-widget .scaling-equation-label{grid-column:1 / -1; justify-self:center;}
  #matrix-scaling-widget .scaling-equals{justify-self:end;}
  #matrix-scaling-widget .scaling-operands,
  #matrix-scaling-widget .scaling-output,
  #matrix-scaling-widget .scaling-expansion{
    display:flex;
    flex:0 0 auto;
    align-items:center;
    justify-content:center;
    gap:6px;
    min-height:57px;
    font:13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;
    font-variant-numeric:tabular-nums;
  }
  #matrix-scaling-widget .scaling-expansion{justify-content:flex-start; gap:4px;}
  #matrix-scaling-widget .scaling-scaled-column{display:flex; align-items:center; gap:3px;}
  #matrix-scaling-widget .scaling-operand{text-align:center;}
  #matrix-scaling-widget .scaling-bracket{
    position:relative;
    display:grid;
    gap:2px 12px;
    padding:4px 7px;
    font:13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;
  }
  #matrix-scaling-widget .scaling-matrix{grid-template-columns:repeat(2,1fr);}
  #matrix-scaling-widget .scaling-bracket::before,
  #matrix-scaling-widget .scaling-bracket::after{
    content:"";
    position:absolute;
    top:0;
    bottom:0;
    width:5px;
    border-top:1px solid #89939e;
    border-bottom:1px solid #89939e;
  }
  #matrix-scaling-widget .scaling-bracket::before{left:0; border-left:1px solid #89939e;}
  #matrix-scaling-widget .scaling-bracket::after{right:0; border-right:1px solid #89939e;}
  #matrix-scaling-widget .scaling-zero{color:#89939e;}
  #matrix-scaling-widget .scaling-times{color:var(--dim);}
  #matrix-scaling-widget .scaling-operand > .scaling-bracket{min-height:57px; align-content:center;}
  #matrix-scaling-widget .scaling-column-collection{grid-template-columns:repeat(2,1fr); gap:6px; padding:4px 6px;}
  #matrix-scaling-widget .scaling-column-collection > .scaling-bracket::before,
  #matrix-scaling-widget .scaling-column-collection > .scaling-bracket::after,
  #matrix-scaling-widget .scaling-scaled-column > .scaling-bracket::before,
  #matrix-scaling-widget .scaling-scaled-column > .scaling-bracket::after{border-color:currentColor;}
  #matrix-scaling-widget .scaling-answer{color:var(--amber);}
  #matrix-scaling-widget .scaling-answer::before,
  #matrix-scaling-widget .scaling-answer::after{border-color:var(--amber);}
  #matrix-scaling-widget figure{margin:0;}
  #matrix-scaling-widget svg{display:block; width:100%; max-width:340px; height:auto; margin:0 auto; background:#fff;}
  #matrix-scaling-widget svg text{font-family:inherit; font-size:11px;}
  #matrix-scaling-widget .scaling-vector-label{paint-order:stroke; stroke:#fff; stroke-width:3px; stroke-linejoin:round;}
  #matrix-scaling-widget .scaling-legend{
    display:flex;
    flex-wrap:wrap;
    justify-content:space-between;
    gap:4px 12px;
    color:var(--dim);
    font-size:11px;
  }
  #matrix-scaling-widget .scaling-legend .legend-fixed::before{
    content:"";
    display:inline-block;
    width:18px;
    margin-right:5px;
    vertical-align:middle;
    border-top:2px dashed #89939e;
  }
  #matrix-scaling-widget .legend-result{color:var(--amber);}
  #matrix-scaling-widget .scaling-controls{display:grid; gap:8px; margin-top:12px;}
  #matrix-scaling-widget .scaling-control{display:grid; grid-template-columns:2.2em minmax(0,1fr) 3.6em; align-items:center; gap:8px; min-width:0; color:var(--teal);}
  #matrix-scaling-widget .scaling-control + .scaling-control{color:var(--purple);}
  #matrix-scaling-widget .control-heading{display:contents;}
  #matrix-scaling-widget label{grid-column:1; grid-row:1; margin:0; color:inherit; font-family:inherit; font-size:14px; font-weight:600; letter-spacing:0; text-transform:none;}
  #matrix-scaling-widget output{grid-column:3; grid-row:1; text-align:right; font:600 13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;}
  #matrix-scaling-widget input[type="range"]{
    display:block;
    grid-column:2;
    grid-row:1;
    width:100%;
    min-width:0;
    height:28px;
    margin:0;
    padding:0;
    accent-color:var(--teal);
    cursor:pointer;
  }
  #matrix-scaling-widget .scaling-control + .scaling-control input{accent-color:var(--purple);}
  #matrix-scaling-widget input:focus-visible{outline:2px solid currentColor; outline-offset:2px; border-radius:3px;}
  #matrix-scaling-widget .scaling-comparison{padding:15px 20px; border-top:1px solid var(--line);}
  #matrix-scaling-widget .scaling-equivalence{font-size:12px;}
  #matrix-scaling-widget .scaling-equivalence[data-matched="true"]{color:var(--teal);}
  @media (max-width:760px){
    #matrix-scaling-widget .scaling-panels{grid-template-columns:1fr;}
    #matrix-scaling-widget .scaling-panel + .scaling-panel{border-left:0; border-top:1px solid var(--line);}
  }
  @media (max-width:400px){
    #matrix-scaling-widget .scaling-head,
    #matrix-scaling-widget .scaling-panel,
    #matrix-scaling-widget .scaling-comparison{padding:16px 12px;}
    #matrix-scaling-widget svg text{font-size:13px;}
  }
</style>

<div id="matrix-scaling-widget" class="matrix-figure" role="group" aria-labelledby="scaling-title">
  <div class="scaling-head">
    <h4 id="scaling-title">Scaling a matrix-vector product</h4>
  </div>
  <div class="scaling-panels">
    <section class="scaling-panel" data-scaling-panel="left" aria-labelledby="scaling-left-title">
      <h5 id="scaling-left-title">Vary A · fixed x</h5>
      <div class="scaling-calculation" data-left-operands="" role="img" aria-label="y = Ax, expanded as the two diagonal entries scaling the fixed all-ones vector">
        <div class="scaling-equation-label" aria-hidden="true"><b>y</b> = <b>A</b><b>x</b></div>
        <div class="scaling-output" aria-hidden="true">
          <span class="scaling-operand">
            <span class="scaling-bracket scaling-answer" data-left-readout="">
              <span>1.00</span><span>1.00</span>
            </span>
          </span>
          <span>=</span>
        </div>
        <div class="scaling-operands" aria-hidden="true">
          <div class="scaling-operand">
            <span class="scaling-bracket scaling-matrix">
              <span class="first" data-left-cell="a1">1.00</span><span class="scaling-zero">0</span>
              <span class="scaling-zero">0</span><span class="second" data-left-cell="a2">1.00</span>
            </span>
          </div>
          <span class="scaling-times">×</span>
          <div class="scaling-operand">
            <span class="scaling-bracket"><span class="first">1</span><span class="second">1</span></span>
          </div>
        </div>
        <span class="scaling-equals" aria-hidden="true">=</span>
        <div class="scaling-expansion" data-row-expansion="" aria-hidden="true">
          <span class="scaling-bracket">
            <span class="first" data-left-expansion="0">1.00 × 1</span>
            <span class="second" data-left-expansion="1">1.00 × 1</span>
          </span>
        </div>
      </div>
      <figure>
        <svg viewBox="0 0 320 300" role="img" aria-label="The separately scaled coordinates of fixed input x locate output y = Ax" data-left-plot=""></svg>
        <figcaption class="scaling-legend"><span class="legend-fixed">fixed x</span><span class="legend-result">↗ Ax</span></figcaption>
      </figure>
      <div class="scaling-controls">
        <div class="scaling-control">
          <div class="control-heading">
            <label for="scaling-a11">a₁₁</label>
            <output for="scaling-a11" data-left-output="a1">1.00</output>
          </div>
          <input id="scaling-a11" data-left-control="a1" type="range" min="-2" max="2" step="0.05" value="1" aria-label="a₁₁: scale input component x₁" aria-describedby="scaling-left-product1" />
          <div id="scaling-left-product1" hidden="">y₁ = <span data-left-product="0">1.00 × 1 = 1.00</span></div>
        </div>
        <div class="scaling-control">
          <div class="control-heading">
            <label for="scaling-a22">a₂₂</label>
            <output for="scaling-a22" data-left-output="a2">1.00</output>
          </div>
          <input id="scaling-a22" data-left-control="a2" type="range" min="-2" max="2" step="0.05" value="1" aria-label="a₂₂: scale input component x₂" aria-describedby="scaling-left-product2" />
          <div id="scaling-left-product2" hidden="">y₂ = <span data-left-product="1">1.00 × 1 = 1.00</span></div>
        </div>
      </div>
    </section>
    <section class="scaling-panel" data-scaling-panel="right" aria-labelledby="scaling-right-title">
      <h5 id="scaling-right-title">Fixed A · vary x</h5>
      <div class="scaling-calculation" data-right-operands="" role="img" aria-label="y = Ax, expanded as column a₁ times x₁ plus column a₂ times x₂, with A fixed at the identity">
        <div class="scaling-equation-label" aria-hidden="true"><b>y</b> = <b>A</b><b>x</b></div>
        <div class="scaling-output" aria-hidden="true">
          <span class="scaling-operand">
            <span class="scaling-bracket scaling-answer" data-right-readout="">
              <span>1.00</span><span>1.00</span>
            </span>
          </span>
          <span>=</span>
        </div>
        <div class="scaling-operands" aria-hidden="true">
          <div class="scaling-operand">
            <span class="scaling-bracket scaling-column-collection" data-fixed-column-matrix="">
              <span class="scaling-bracket first" data-column-coordinates="a1"><span>1</span><span>0</span></span>
              <span class="scaling-bracket second" data-column-coordinates="a2"><span>0</span><span>1</span></span>
            </span>
          </div>
          <span class="scaling-times">×</span>
          <div class="scaling-operand">
            <span class="scaling-bracket" data-variable-input="">
              <span class="first" data-right-cell="x1">1.00</span>
              <span class="second" data-right-cell="x2">1.00</span>
            </span>
          </div>
        </div>
        <span class="scaling-equals" aria-hidden="true">=</span>
        <div class="scaling-expansion" data-column-expansion="" aria-hidden="true">
          <span class="scaling-scaled-column first">
            <span class="scaling-bracket" data-expansion-column="a1"><span>1</span><span>0</span></span>
            <span>×</span><span data-right-expansion="0">1.00</span>
          </span>
          <span>+</span>
          <span class="scaling-scaled-column second">
            <span class="scaling-bracket" data-expansion-column="a2"><span>0</span><span>1</span></span>
            <span>×</span><span data-right-expansion="1">1.00</span>
          </span>
        </div>
      </div>
      <figure>
        <svg viewBox="0 0 320 300" role="img" aria-label="Scaled columns a₁x₁ and a₂x₂ adding to Ax" data-right-plot=""></svg>
        <figcaption class="scaling-legend"><span class="legend-fixed">fixed columns</span><span class="legend-result">Ax = a₁x₁ + a₂x₂</span></figcaption>
      </figure>
      <div class="scaling-controls">
        <div class="scaling-control">
          <div class="control-heading">
            <label for="scaling-x1">x₁</label>
            <output for="scaling-x1" data-right-output="x1">1.00</output>
          </div>
          <input id="scaling-x1" data-right-control="x1" type="range" min="-2" max="2" step="0.05" value="1" aria-label="x₁: weight of the first column" aria-describedby="scaling-right-product1" />
          <div id="scaling-right-product1" hidden="">a₁x₁ = <span data-right-product="0">(1.00, 0.00)</span></div>
        </div>
        <div class="scaling-control">
          <div class="control-heading">
            <label for="scaling-x2">x₂</label>
            <output for="scaling-x2" data-right-output="x2">1.00</output>
          </div>
          <input id="scaling-x2" data-right-control="x2" type="range" min="-2" max="2" step="0.05" value="1" aria-label="x₂: weight of the second column" aria-describedby="scaling-right-product2" />
          <div id="scaling-right-product2" hidden="">a₂x₂ = <span data-right-product="1">(0.00, 1.00)</span></div>
        </div>
      </div>
    </section>
  </div>
  <div class="scaling-comparison">
    <div class="scaling-equivalence" data-scaling-equivalence="" role="status" aria-live="polite" aria-atomic="true"></div>
  </div>
  <noscript><p>Enable JavaScript to explore the four sliders.</p></noscript>
</div>
<script>
(function(){
  const root = document.getElementById('matrix-scaling-widget');
  const fixed = [1, 1], origin = [160, 150], plotScale = 40;
  const colors = {first:'#147f7a', second:'#7153b8', result:'#a96100', fixed:'#89939e'};
  const select = selector => root.querySelector(selector);
  const equivalence = root.querySelector('[data-scaling-equivalence]');
  const fmt = value => (Math.abs(value) < 1e-9 ? 0 : value).toFixed(2);
  const formatVector = vector => '('+vector.map(fmt).join(', ')+')';
  const point = vector => [origin[0]+vector[0]*plotScale, origin[1]-vector[1]*plotScale];

  function element(tag, attributes, parent, text){
    const node = document.createElementNS('http://www.w3.org/2000/svg',tag);
    Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));
    if(text !== undefined) node.textContent = text;
    parent.appendChild(node);
    return node;
  }

  function segment(line, from, to){
    const start = point(from), end = point(to);
    ['x1','y1','x2','y2'].forEach((key,index)=>line.setAttribute(key,[...start,...end][index]));
    // A zero-length SVG line can still paint its marker; hide it when the component vanishes.
    line.setAttribute('visibility',from.every((value,index)=>Math.abs(value-to[index])<1e-9) ? 'hidden' : 'visible');
  }

  function makePanel(side, keys){
    const svg = select('[data-'+side+'-plot]');
    const defs = element('defs',{},svg);
    Object.entries(colors).forEach(([name,color])=>{
      const marker = element('marker',{id:'scaling-'+side+'-'+name,viewBox:'0 0 10 10',refX:9,refY:5,markerWidth:7,markerHeight:7,markerUnits:'userSpaceOnUse',orient:'auto'},defs);
      element('path',{d:'M0 0L10 5L0 10z',fill:color},marker);
    });
    const grid = element('g',{stroke:'#edf0f2','stroke-width':1},svg);
    for(let tick=-3;tick<=3;tick++){
      const [x,y] = point([tick,tick]);
      element('path',{d:'M'+x+' 30V270 M40 '+y+'H280'},grid);
      if(tick && Math.abs(tick)<3){
        element('text',{x,y:origin[1]+17,fill:'#89939e','text-anchor':'middle',stroke:'none'},grid,tick);
        element('text',{x:origin[0]-9,y:y+4,fill:'#89939e','text-anchor':'end',stroke:'none'},grid,tick);
      }
    }
    element('path',{d:'M30 150H290 M160 20V280',stroke:'#c4cbd1','stroke-width':1},svg);
    element('text',{x:151,y:167,fill:'#89939e','text-anchor':'end'},svg,'0');
    element('text',{x:289,y:166,fill:'#89939e','text-anchor':'end'},svg,'y₁');
    element('text',{x:170,y:23,fill:'#89939e'},svg,'y₂');

    const guides = element('path',{fill:'none',stroke:'#d9dee5','stroke-dasharray':'3 4'},svg);
    const arrow = (name, attributes) => element('line',{
      stroke:colors[name],'stroke-width':2.5,'marker-end':'url(#scaling-'+side+'-'+name+')',...attributes
    },svg);
    const first = arrow('first',{['data-'+side+'-segment']:'x1','stroke-width':4,'stroke-opacity':0.5});
    const second = arrow('second',{['data-'+side+'-segment']:'x2','stroke-width':4,'stroke-opacity':0.5});
    if(side === 'left'){
      const [x,y] = point(fixed);
      element('path',{d:'M'+x+' 150V'+y+'H160',fill:'none',stroke:'#c4cbd1','stroke-dasharray':'3 4','data-fixed-input-guides':''},svg);
      const input = arrow('fixed',{'data-left-vector':'x','stroke-dasharray':'4 4','stroke-width':1.8});
      segment(input,[0,0],fixed);
      element('circle',{cx:x,cy:y,r:3,fill:'#fff',stroke:colors.fixed},svg);
      element('text',{x:x-5,y:y-10,fill:'#626d78','text-anchor':'end'},svg,'fixed x');
    }else{
      // Hollow tips keep the fixed columns identifiable even when a scaled arrow overlaps them.
      [[fixed[0],0],[0,fixed[1]]].forEach((vector,index)=>{
        const name = index ? 'second' : 'first';
        const column = arrow(name,{'data-fixed-column':index,'stroke-dasharray':'3 4','stroke-width':1.5});
        segment(column,[0,0],vector);
        const [x,y] = point(vector);
        element('circle',{cx:x,cy:y,r:3.5,fill:'#fff',stroke:colors[name],'stroke-width':1.5},svg);
        element('text',{x:x+(index ? -10 : 3),y:y+(index ? -8 : 32),fill:colors[name],'text-anchor':index ? 'end' : 'start'},svg,index ? 'a₂' : 'a₁');
      });
    }
    const result = arrow('result',{['data-'+side+'-vector']:'y','stroke-width':2.5});
    const tip = element('circle',{r:3.5,fill:colors.result,['data-'+side+'-tip']:''},svg);
    const firstLabel = element('text',{fill:colors.first,'text-anchor':'middle',class:'scaling-vector-label'},svg,side === 'left' ? 'a₁₁x₁' : 'a₁x₁');
    const secondLabel = element('text',{fill:colors.second,class:'scaling-vector-label'},svg,side === 'left' ? 'a₂₂x₂' : 'a₂x₂');
    return {
      side, svg, guides, first, second, result, tip, firstLabel, secondLabel,
      root:root.querySelector('[data-scaling-panel="'+side+'"]'),
      controls:keys.map(key=>select('[data-'+side+'-control="'+key+'"]')),
      outputs:keys.map(key=>select('[data-'+side+'-output="'+key+'"]')),
      cells:keys.map(key=>select('[data-'+side+'-cell="'+key+'"]')),
      expansions:keys.map((key,index)=>select('[data-'+side+'-expansion="'+index+'"]')),
      products:keys.map((key,index)=>select('[data-'+side+'-product="'+index+'"]')),
      operands:select('[data-'+side+'-operands]'),
      readout:select('[data-'+side+'-readout]')
    };
  }
  const left = makePanel('left',['a1','a2']), right = makePanel('right',['x1','x2']);
  [left,right].forEach(panel=>panel.controls.forEach((control,index)=>{
    control.addEventListener('focus',()=>{
      panel.root.dataset.activeComponent=String(index);
      [panel.first,panel.second].forEach((line,component)=>{
        line.style.opacity=component===index?'1':'0.25';
        panel.readout.children[component].style.opacity=component===index?'1':'0.4';
      });
    });
    control.addEventListener('blur',()=>{
      delete panel.root.dataset.activeComponent;
      [panel.first,panel.second].forEach((line,component)=>{
        line.style.opacity='';
        panel.readout.children[component].style.opacity='';
      });
    });
  }));

  function drawPanel(panel){
    const values = panel.controls.map(control=>Number(control.value));
    const result = values.map((value,index)=>value*fixed[index]);
    const elbow = [result[0],0], [x,y] = point(result);
    const componentView = panel.side === 'left';
    segment(panel.first,[0,0],elbow);
    segment(panel.second,componentView ? [0,0] : elbow,componentView ? [0,result[1]] : result);
    segment(panel.result,[0,0],result);
    panel.guides.setAttribute('d',componentView ? 'M'+x+' 150V'+y+'H160' : 'M160 '+y+'H'+x);
    panel.tip.setAttribute('cx',x); panel.tip.setAttribute('cy',y);
    panel.firstLabel.setAttribute('x',origin[0]+Math.sign(result[0])*24);
    panel.firstLabel.setAttribute('y',origin[1]+(result[1]>=0 ? 32 : -10));
    panel.firstLabel.setAttribute('visibility',Math.abs(result[0])>=1 ? 'visible' : 'hidden');
    panel.secondLabel.setAttribute('x',componentView ? origin[0]+(result[0]>=0 ? -26 : 26) : x+(result[0]>=0 ? 10 : -10));
    panel.secondLabel.setAttribute('y',(origin[1]+y)/2+4);
    panel.secondLabel.setAttribute('text-anchor',(componentView ? result[0]>=0 : result[0]<0) ? 'end' : 'start');
    panel.secondLabel.setAttribute('visibility',Math.abs(result[1])>=0.5 ? 'visible' : 'hidden');
    values.forEach((value,index)=>{
      const contribution = index === 0 ? [result[0],0] : [0,result[1]];
      const rowProduct = fmt(value)+' × '+fixed[index];
      panel.outputs[index].value = fmt(value);
      panel.cells[index].textContent = fmt(value);
      panel.readout.children[index].textContent = fmt(result[index]);
      panel.expansions[index].textContent = componentView ? rowProduct : fmt(value);
      panel.products[index].textContent = componentView
        ? rowProduct+' = '+fmt(result[index])
        : formatVector(contribution);
      panel.controls[index].setAttribute('aria-valuetext',fmt(value)+(componentView
        ? '; output component '+fmt(result[index])
        : '; scaled column '+formatVector(contribution)));
    });
    const description = panel.side === 'left'
      ? 'Variable A = diag'+formatVector(values)+' times fixed x = '+formatVector(fixed)+', the all-ones column vector'
      : 'Fixed A = diag'+formatVector(fixed)+', the identity I, times variable x = '+formatVector(values);
    const interpretation = componentView
      ? ' Row-wise dot products: '+panel.expansions.map(node=>node.textContent).join('; ')+'.'
      : ' Column combination: (1, 0) times '+fmt(values[0])+' plus (0, 1) times '+fmt(values[1])+'.';
    panel.operands.setAttribute('aria-label',description+'. Output Ax = '+formatVector(result)+'.'+interpretation);
    panel.svg.setAttribute('aria-label',description+'. Output y = Ax = '+formatVector(result)+(componentView
      ? '. Teal and purple arrows independently scale the input coordinates; projection guides locate the output.'
      : '. Teal a₁x₁ followed head to tail by purple a₂x₂; the amber arrow is their vector sum.'));
    return values;
  }

  function draw(){
    const diagonal = drawPanel(left), input = drawPanel(right);
    const matches = diagonal.every((value,index)=>Math.abs(value-input[index])<1e-9);
    equivalence.dataset.matched = String(matches);
    equivalence.textContent = matches
      ? 'Same output: a₁₁ = x₁ and a₂₂ = x₂.'
      : 'Different outputs. Match a₁₁ = x₁ and a₂₂ = x₂.';
  }
  [...left.controls,...right.controls].forEach(control=>control.addEventListener('input',draw));
  draw();
})();
</script>

<p>The column space view shifts the analysis from the input $x$ onto the matrix $A$ itself, opening up some interesting options which we will see below. We thus turn the matrix $\mathbf A$ from a simple object of writing scaling $x$ compactly into a first class citizen that warrants its own analysis tools and distinct properties. One of those analysis tools is the rank of a matrix.</p>

<h3 id="rank-of-a-matrix">Rank of a Matrix</h3>

<p>When inspecting the column space of a matrix</p>

\[\begin{align*}
\mathbf{y} &amp;= \mathbf{A} \mathbf{x} \\
\begin{bmatrix} y_1 \\ y_2 \end{bmatrix} &amp;= \begin{bmatrix}
\begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix} &amp;
\begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix}
\end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} \\
&amp;= \begin{bmatrix} \begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix} x_1 + \begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix} x_2
\end{bmatrix} \\
&amp;= \begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix} x_1 + \begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix} x_2
\end{align*}\]

<p>we can see that the resulting vector $\mathbf{y}$ is a linear combination of the columns of $\mathbf{A}$.
Why? Because we’ve shifted from interpreting $a_{ij}$ as the linear coefficients to interpreting $x_i$ as the coefficient which scales the vectors in the column space of the matrix.</p>

<p>Consequently, we can ask what kind of vectors $\mathbf{y}$ we can generate from $\mathbf{A}$ for a given $x$. The answer to that is closely related to the rank of a matrix Let’s consider the matrix</p>

\[A = \begin{bmatrix} \begin{bmatrix} 2.5 \\ 0 \end{bmatrix} &amp; \begin{bmatrix} 0 \\ 0.5 \end{bmatrix} \end{bmatrix}\]

<p>which coincidentally can be interpreted as the basis vectors of a two dimensional coordinate system. A vector $\mathbf{x} = (x_1, x_2)$ scales the first column by $x_1$ and the second column by $x_2$</p>

\[\begin{bmatrix} y_1 \\ y_2 \end{bmatrix} = \begin{bmatrix} 2.5 \\ 0 \end{bmatrix} x_1 +  \begin{bmatrix} 0 \\ 0.5 \end{bmatrix} x_2\]

<p>The column space of $\mathbf{A}$ consists of two different vectors as they both point into different directions. Essentially both vectors stay out of each other’s lanes and don’t interfere across dimension with each other. But what happens if we flip the entries in the second column of $\mathbf{A}$?</p>

\[\begin{bmatrix} y_1 \\ y_2 \end{bmatrix} = \begin{bmatrix} 2.5 \\ 0 \end{bmatrix} x_1 +  \begin{bmatrix} 0.5 \\ 0 \end{bmatrix} x_2\]

<p>In this case regardless of what we use as input $\mathbf{x}$, the second dimension of our output vector $y_2$ will always be zero. This collapses the second dimension and in effect we can only generate a variable output in the first dimension. The important test is whether we have independent columns in the matrix $\mathbf{A}$ or whether the column space consists of collinear vectors.
Collinear vectors simply mean that they all point into the same direction but can have different lengths.</p>

<p>To make it more interesting (as in not squashing things to zero) we can purposefully construct a column space where $\mathbf{a}_2$ is a multiple of $\mathbf{a}_1$, namely $\mathbf{a}_2=1/5 \cdot  \mathbf{a}_1$.</p>

\[\begin{align*}
\begin{bmatrix} y_1 \\ y_2 \end{bmatrix}
=
\underbrace{\begin{bmatrix} 2.5 \\ 1 \end{bmatrix}}_{\mathbf a_1}x_1
+
\underbrace{\begin{bmatrix} 0.5 \\ 0.2 \end{bmatrix}}_{\mathbf a_2} x_2.
\end{align*}\]

<p>Both columns have two nonzero components, but the second remains a multiple of the first.
From the column space view of a matrix, we’re essentially creating an output vector $\mathbf{y}$ as a linear combination of vectors that all point into the same direction.
They therefore provide only one independent direction, and every output remains on the line to which the columns point, just with possibly different lengths.
We say that the matrix $\mathbf{A}$ has rank 1 even if it has more than 1 vectors in its column space, as it provides only one independent direction in its column space.</p>

<p>In the widget below, you can see that we can move the output vector $\mathbf{y}$ by adjusting the components of the input vector $\mathbf{x}$.
In the $\text{rank}[A]=1$ case on the right, the output vector $\mathbf{y}$ is constrained to move along a single line, reflecting the fact that the column space of $\mathbf{A}$ has only one independent direction. If all vectors which we scale with $x_i$ point in the same direction, we’re strictly constrained to the subspace that are spanned by those collinear vectors as our basis system doesn’t allow for anything else.</p>

<style>
  #matrix-rank-widget{
    --line:#e2e6e9;
    --ink:#212931;
    --dim:#626d78;
    --teal:#147f7a;
    --purple:#7153b8;
    --blue:#2f6fad;
    --amber:#a96100;
    margin:26px 0;
    border:1px solid var(--line);
    border-radius:8px;
    background:#fff;
    color:var(--ink);
    font:400 14px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif;
    letter-spacing:0;
    text-align:left;
    text-transform:none;
  }
  #matrix-rank-widget,
  #matrix-rank-widget *{box-sizing:border-box;}
  #matrix-rank-widget h4,
  #matrix-rank-widget h5{margin:0; color:var(--ink); font:600 17px/1.35 "Helvetica Neue",Helvetica,Arial,sans-serif; letter-spacing:0; text-transform:none;}
  #matrix-rank-widget h5{font-size:15px;}
  #matrix-rank-widget .rank-head{padding:18px 20px; border-bottom:1px solid var(--line);}
  #matrix-rank-widget .rank-panels{display:grid; grid-template-columns:repeat(2,minmax(0,1fr));}
  #matrix-rank-widget .rank-panel{min-width:0; padding:18px;}
  #matrix-rank-widget .rank-panel + .rank-panel{border-left:1px solid var(--line);}
  #matrix-rank-widget .rank-panel-head{display:flex; flex-wrap:wrap; align-items:baseline; justify-content:space-between; gap:4px 8px;}
  #matrix-rank-widget .rank-badge{color:var(--amber); font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;}
  #matrix-rank-widget .rank-first{color:var(--teal);}
  #matrix-rank-widget .rank-second{color:var(--purple);}
  #matrix-rank-widget .rank-equation,
  #matrix-rank-widget .rank-term{display:flex; align-items:center; justify-content:center; gap:4px;}
  #matrix-rank-widget .rank-equation{margin:18px 0 8px; font:13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace; font-variant-numeric:tabular-nums;}
  #matrix-rank-widget .rank-bracket{position:relative; display:grid; gap:2px; padding:4px 7px; text-align:center;}
  #matrix-rank-widget .rank-bracket::before,
  #matrix-rank-widget .rank-bracket::after{content:""; position:absolute; top:0; bottom:0; width:5px; border-top:1px solid currentColor; border-bottom:1px solid currentColor;}
  #matrix-rank-widget .rank-bracket::before{left:0; border-left:1px solid currentColor;}
  #matrix-rank-widget .rank-bracket::after{right:0; border-right:1px solid currentColor;}
  #matrix-rank-widget .rank-result{color:var(--amber);}
  #matrix-rank-widget figure{margin:0;}
  #matrix-rank-widget svg{display:block; width:100%; height:auto; max-width:410px; margin:0 auto;}
  #matrix-rank-widget svg text{font-family:inherit; font-size:11px;}
  #matrix-rank-widget .rank-legend{display:flex; flex-wrap:wrap; justify-content:center; gap:4px 12px; color:var(--dim); font-size:11px;}
  #matrix-rank-widget .rank-key::before{content:""; display:inline-block; width:14px; margin-right:4px; vertical-align:middle; border-top:3px solid currentColor;}
  #matrix-rank-widget .rank-key-fixed::before{border-top:2px dashed currentColor;}
  #matrix-rank-widget .rank-dimension{margin:12px 0 0; color:var(--amber); font-size:12px; line-height:1.5; text-align:center;}
  #matrix-rank-widget .rank-controls{padding:16px 20px; border-top:1px solid var(--line);}
  #matrix-rank-widget .rank-input-head{display:flex; flex-wrap:wrap; align-items:baseline; justify-content:space-between; gap:4px 12px; margin-bottom:10px;}
  #matrix-rank-widget .rank-input-vector{font:13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;}
  #matrix-rank-widget .rank-sliders{display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px 24px;}
  #matrix-rank-widget .rank-control{display:grid; grid-template-columns:1.8em minmax(0,1fr) 3.5em; align-items:center; gap:8px; min-width:0;}
  #matrix-rank-widget label{margin:0; color:inherit; font:600 14px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif; letter-spacing:0; text-transform:none;}
  #matrix-rank-widget input[type="range"]{display:block; width:100%; min-width:0; height:28px; margin:0; padding:0; accent-color:var(--teal); cursor:pointer;}
  #matrix-rank-widget .rank-second input{accent-color:var(--purple);}
  #matrix-rank-widget input:focus-visible{outline:2px solid currentColor; outline-offset:2px; border-radius:3px;}
  #matrix-rank-widget output{font:600 13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace; text-align:right;}
  #matrix-rank-widget .rank-hint{margin:10px 0 0; color:var(--dim); font-size:11px; line-height:1.5; text-align:left;}
  #matrix-rank-widget .rank-sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;}
  @media (max-width:760px){
    #matrix-rank-widget .rank-panels,
    #matrix-rank-widget .rank-sliders{grid-template-columns:1fr;}
    #matrix-rank-widget .rank-panel + .rank-panel{border-left:0; border-top:1px solid var(--line);}
  }
  @media (max-width:400px){
    #matrix-rank-widget .rank-head,
    #matrix-rank-widget .rank-panel,
    #matrix-rank-widget .rank-controls{padding:16px 12px;}
    #matrix-rank-widget .rank-equation{font-size:12px;}
    #matrix-rank-widget .rank-bracket{padding:4px 6px;}
    #matrix-rank-widget svg text{font-size:13px;}
  }
</style>

<div id="matrix-rank-widget" class="matrix-figure" role="group" aria-labelledby="rank-title">
  <div class="rank-head">
    <h4 id="rank-title">Two independent directions, or one?</h4>
  </div>
  <div class="rank-panels">
    <section class="rank-panel" data-rank-panel="full" aria-labelledby="rank-full-title">
      <div class="rank-panel-head">
        <h5 id="rank-full-title">Independent columns</h5>
        <span class="rank-badge">rank(A) = 2</span>
      </div>
      <div class="rank-equation" data-rank-equation="" role="img" aria-label="y = x₁(2.5, 0) + x₂(0, 0.5)">
        <span class="rank-term rank-first">
          <span data-rank-weight="0">1.00</span>
          <span class="rank-bracket" data-rank-column="0"><span>2.5</span><span>0</span></span>
        </span>
        <span>+</span>
        <span class="rank-term rank-second">
          <span data-rank-weight="1">1.00</span>
          <span class="rank-bracket" data-rank-column="1"><span>0</span><span>0.5</span></span>
        </span>
        <span>=</span>
        <span class="rank-bracket rank-result" data-rank-result=""><span>2.50</span><span>0.50</span></span>
      </div>
      <figure>
        <svg viewBox="0 0 360 224" data-rank-plot="" role="img" aria-label="Independent columns produce a two-dimensional region of outputs"></svg>
        <figcaption class="rank-legend">
          <span class="rank-key rank-key-fixed">fixed a₁, a₂</span>
          <span class="rank-key rank-first">x₁a₁</span>
          <span class="rank-key rank-second">x₂a₂</span>
          <span class="rank-key rank-result">y</span>
        </figcaption>
      </figure>
      <p class="rank-dimension" data-rank-dimension="2">Outputs fill an area.</p>
    </section>
    <section class="rank-panel" data-rank-panel="one" aria-labelledby="rank-one-title">
      <div class="rank-panel-head">
        <h5 id="rank-one-title">Collinear columns</h5>
        <span class="rank-badge">rank(A) = 1</span>
      </div>
      <div class="rank-equation" data-rank-equation="" role="img" aria-label="y = x₁(2.5, 1) + x₂(0.5, 0.2)">
        <span class="rank-term rank-first">
          <span data-rank-weight="0">1.00</span>
          <span class="rank-bracket" data-rank-column="0"><span>2.5</span><span>1</span></span>
        </span>
        <span>+</span>
        <span class="rank-term rank-second">
          <span data-rank-weight="1">1.00</span>
          <span class="rank-bracket" data-rank-column="1"><span>0.5</span><span>0.2</span></span>
        </span>
        <span>=</span>
        <span class="rank-bracket rank-result" data-rank-result=""><span>3.00</span><span>1.20</span></span>
      </div>
      <figure>
        <svg viewBox="0 0 360 224" data-rank-plot="" role="img" aria-label="Collinear columns produce outputs on a single line"></svg>
        <figcaption class="rank-legend">
          <span class="rank-key rank-key-fixed">fixed a₁, a₂</span>
          <span class="rank-key rank-first">x₁a₁</span>
          <span class="rank-key rank-second">x₂a₂</span>
          <span class="rank-key rank-result">y</span>
        </figcaption>
      </figure>
      <p class="rank-dimension" data-rank-dimension="1">Outputs stay on a line.</p>
    </section>
  </div>
  <div class="rank-controls">
    <div class="rank-input-head">
      <span>Shared input x</span>
      <span class="rank-input-vector" data-rank-input="">(1.00, 1.00)</span>
    </div>
    <div class="rank-sliders">
      <div class="rank-control rank-first">
        <label for="rank-x1">x₁</label>
        <input id="rank-x1" type="range" min="-2" max="2" step="0.05" value="1" aria-label="Shared x₁: weight of the first column in both panels" aria-describedby="rank-input-description" />
        <output for="rank-x1" data-rank-control-output="0" aria-live="off">1.00</output>
      </div>
      <div class="rank-control rank-second">
        <label for="rank-x2">x₂</label>
        <input id="rank-x2" type="range" min="-2" max="2" step="0.05" value="1" aria-label="Shared x₂: weight of the second column in both panels" aria-describedby="rank-input-description" />
        <output for="rank-x2" data-rank-control-output="1" aria-live="off">1.00</output>
      </div>
    </div>
    <p class="rank-hint" id="rank-input-description">Both panels use this x. Shading shows outputs for −2 ≤ x₁, x₂ ≤ 2.</p>
  </div>
  <div class="rank-sr-only" data-rank-status="" role="status" aria-live="polite" aria-atomic="true"></div>
  <noscript><p>Enable JavaScript to explore the shared input.</p></noscript>
</div>
<script>
(function(){
  const root = document.getElementById('matrix-rank-widget');
  const controls = [root.querySelector('#rank-x1'),root.querySelector('#rank-x2')];
  const controlOutputs = [...root.querySelectorAll('[data-rank-control-output]')];
  const inputReadout = root.querySelector('[data-rank-input]');
  const status = root.querySelector('[data-rank-status]');
  const origin = [180,116], scale = 24;
  const colors = ['#147f7a','#7153b8','#a96100'];
  const fmt = value => (Math.abs(value)<1e-9 ? 0 : value).toFixed(2);
  const vector = values => '('+values.map(fmt).join(', ')+')';
  const point = values => [origin[0]+scale*values[0],origin[1]-scale*values[1]];

  function element(tag,attributes,parent,text){
    const node = document.createElementNS('http://www.w3.org/2000/svg',tag);
    Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));
    if(text !== undefined) node.textContent = text;
    parent.appendChild(node);
    return node;
  }

  function setLine(line,from,to){
    const coordinates = [...point(from),...point(to)];
    ['x1','y1','x2','y2'].forEach((key,index)=>line.setAttribute(key,coordinates[index]));
    line.setAttribute('visibility',from.every((value,index)=>Math.abs(value-to[index])<1e-9) ? 'hidden' : 'visible');
  }

  function makePanel(key,columns,rank){
    const panel = root.querySelector('[data-rank-panel="'+key+'"]');
    const svg = panel.querySelector('[data-rank-plot]');
    const defs = element('defs',{},svg);
    colors.forEach((color,index)=>{
      const marker = element('marker',{id:'rank-'+key+'-'+index,viewBox:'0 0 10 10',refX:9,refY:5,markerWidth:index===2 ? 7 : 10,markerHeight:index===2 ? 7 : 10,markerUnits:'userSpaceOnUse',orient:'auto'},defs);
      element('path',{d:'M0 0L10 5L0 10z',fill:color},marker);
    });
    if(rank === 2){
      const [x,y] = point([-5,1]);
      element('rect',{x,y,width:10*scale,height:2*scale,fill:'#fff8ed',stroke:'#ead7b7','stroke-dasharray':'3 4','data-rank-reachable':'area'},svg);
    }else{
      const start = point([-6,-2.4]), end = point([6,2.4]);
      element('line',{x1:start[0],y1:start[1],x2:end[0],y2:end[1],stroke:'#f0dbb8','stroke-width':8,'data-rank-reachable':'line'},svg);
    }
    const grid = element('g',{stroke:'#edf0f2','stroke-width':1},svg);
    for(let tick=-6;tick<=6;tick++){
      const [x] = point([tick,0]);
      element('path',{d:'M'+x+' 44V188'},grid);
      if(tick && tick%2===0) element('text',{x,y:133,fill:'#89939e',stroke:'none','text-anchor':'middle'},grid,tick);
    }
    for(let tick=-3;tick<=3;tick++){
      const y = point([0,tick])[1];
      element('path',{d:'M24 '+y+'H336'},grid);
      if(Math.abs(tick)===2) element('text',{x:170,y:y+4,fill:'#89939e',stroke:'none','text-anchor':'end'},grid,tick);
    }
    element('path',{d:'M24 116H336 M180 32V200',stroke:'#bdc6ce','stroke-width':1},svg);
    element('text',{x:172,y:133,fill:'#89939e','text-anchor':'end'},svg,'0');
    element('text',{x:346,y:133,fill:'#89939e','text-anchor':'end'},svg,'y₁');
    element('text',{x:189,y:34,fill:'#89939e'},svg,'y₂');
    const first = element('line',{stroke:colors[0],'stroke-width':5,'stroke-opacity':0.45,'marker-end':'url(#rank-'+key+'-0)','data-rank-vector':'first'},svg);
    const second = element('line',{stroke:colors[1],'stroke-width':3.5,'stroke-opacity':0.7,'marker-end':'url(#rank-'+key+'-1)','data-rank-vector':'second'},svg);
    const result = element('line',{stroke:colors[2],'stroke-width':2.2,'marker-end':'url(#rank-'+key+'-2)','data-rank-vector':'result'},svg);
    const elbow = element('circle',{r:2.5,fill:colors[0],'data-rank-elbow':''},svg);
    // Different stroke widths keep collinear contributions visible beneath the resultant.
    columns.forEach((column,index)=>{
      const line = element('line',{stroke:colors[index],'stroke-width':1.4,'stroke-dasharray':'3 3','data-rank-fixed':index},svg);
      setLine(line,[0,0],column);
      const [x,y] = point(column), vertical = column[1]!==0;
      element('circle',{cx:x,cy:y,r:3.5,fill:'#fff',stroke:colors[index],'stroke-width':1.4},svg);
      element('text',{x:x+(vertical ? -12 : 5),y:y+(vertical ? -8 : 31),fill:colors[index],'text-anchor':vertical ? 'end' : 'start'},svg,index ? 'a₂' : 'a₁');
    });
    const tip = element('circle',{r:3.5,fill:colors[2],'data-rank-tip':''},svg);
    return {
      columns, rank, svg, first, second, result, elbow, tip,
      equation:panel.querySelector('[data-rank-equation]'),
      weights:[...panel.querySelectorAll('[data-rank-weight]')],
      resultCells:[...panel.querySelector('[data-rank-result]').children]
    };
  }

  const panels = [
    makePanel('full',[[2.5,0],[0,0.5]],2),
    makePanel('one',[[2.5,1],[0.5,0.2]],1)
  ];
  controls.forEach((control,index)=>{
    control.addEventListener('focus',()=>{
      panels.forEach(panel=>{
        [panel.first,panel.second].forEach((line,component)=>{
          line.style.opacity=component===index?'1':'0.25';
          panel.weights[component].style.opacity=component===index?'1':'0.4';
        });
      });
    });
    control.addEventListener('blur',()=>{
      panels.forEach(panel=>{
        [panel.first,panel.second].forEach((line,component)=>{
          line.style.opacity='';
          panel.weights[component].style.opacity='';
        });
      });
    });
  });

  function draw(){
    const input = controls.map(control=>Number(control.value));
    inputReadout.textContent = vector(input);
    input.forEach((value,index)=>{
      controlOutputs[index].value = fmt(value);
      controls[index].setAttribute('aria-valuetext',fmt(value));
    });
    const outputs = panels.map(panel=>{
      const first = panel.columns[0].map(value=>input[0]*value);
      const second = panel.columns[1].map(value=>input[1]*value);
      const result = first.map((value,index)=>value+second[index]);
      setLine(panel.first,[0,0],first);
      setLine(panel.second,first,result);
      setLine(panel.result,[0,0],result);
      const join = point(first), end = point(result);
      panel.elbow.setAttribute('cx',join[0]); panel.elbow.setAttribute('cy',join[1]);
      panel.elbow.setAttribute('visibility',Math.abs(input[0])<1e-9 ? 'hidden' : 'visible');
      panel.tip.setAttribute('cx',end[0]); panel.tip.setAttribute('cy',end[1]);
      panel.weights.forEach((node,index)=>node.textContent=fmt(input[index]));
      panel.resultCells.forEach((node,index)=>node.textContent=fmt(result[index]));
      const equation = 'x₁a₁ + x₂a₂ = '+fmt(input[0])+' times '+vector(panel.columns[0])+' plus '+fmt(input[1])+' times '+vector(panel.columns[1])+'. Output y = '+vector(result)+'.';
      panel.equation.setAttribute('aria-label',equation);
      panel.svg.setAttribute('aria-label',equation+(panel.rank===2
        ? ' Independent columns: rank 2. The reachable outputs form an area.'
        : ' Collinear columns: rank 1. All outputs remain on the line y₂ = 0.4 y₁.'));
      return result;
    });
    status.textContent = 'Shared x = '+vector(input)+'. Rank 2 output '+vector(outputs[0])+'. Rank 1 output '+vector(outputs[1])+'.';
  }
  controls.forEach(control=>control.addEventListener('input',draw));
  draw();
})();
</script>

<h3 id="matrix-inverse">Matrix Inverse</h3>

<p>The rank discussion gives us a natural way to understand why some matrices have an inverse and others do not.
The rank-one transformation can be viewed as a many-to-one map. So far we’ve covered how we can compute $\mathbf y = \mathbf A \mathbf x$ given some input $\mathbf x$. But now we might be interested to obtain the $\mathbf x$ that created $\mathbf y$ in the first place. So the natural thing to ask is “What is the inverse matrix $\mathbf A^{-1}$ with which we can compute $\mathbf x = \mathbf A^{-1} \mathbf y$?”. Is there anything we should pay attention to in the properties of the matrix $\mathbf A$ before we tackle this idea?</p>

<p>Recall our rank-one matrix</p>

\[\mathbf{A}
=
\begin{bmatrix}
2.5 &amp; 0.5 \\
1 &amp; 0.2
\end{bmatrix},\]

<p>whose second column is a multiple of its first. Its matrix-vector multiplication can be written as</p>

\[\begin{align*}
\mathbf{A}\mathbf{x}
&amp;=
\begin{bmatrix} 2.5 \\ 1 \end{bmatrix}x_1
+
\begin{bmatrix} 0.5 \\ 0.2 \end{bmatrix}x_2 \\
&amp;=
\begin{bmatrix} 2.5 \\ 1 \end{bmatrix}x_1
+
\frac{1}{5}\begin{bmatrix} 2.5 \\ 1 \end{bmatrix}x_2 \\
&amp;=
\begin{bmatrix} 2.5 \\ 1 \end{bmatrix}
\underbrace{\left(x_1+\frac{1}{5}x_2\right)}_{\in \mathbb{R}}.
\end{align*}\]

<p>Although we multiplied a two-dimensional input, $x_1$ and $x_2$, into the matrix, the output is determined by the direction of the column space of the matrix scaled by a linear combination of the input components.
Whatever two values $x_1$ and $x_2$ we choose, the direction of the resulting output vector $\mathbf{y}$ remains the same and the input only appears for determining the length of the vector.
The entire two-dimensional input plane is squashed onto the one-dimensional line spanned by the vector $\begin{bmatrix}2.5 &amp; 1\end{bmatrix}^{\mathsf T}$.
Said differently, the $x_1 + \frac{1}{5}x_2$ is a scaling factor but which does not affect the direction of $\begin{bmatrix}2.5 &amp; 1\end{bmatrix}^{\mathsf T}$ but only its length.
The problem is many different inputs can lead to the same output, making it impossible to uniquely determine the original input from the output alone.
In other words, the matrix $\mathbf{A}$ has lost information about the input $\mathbf{x}$ that cannot be recovered from the output.</p>

<p>This creates a kind of mathematical trapdoor: many different inputs fall onto the same output.
For example, consider the two inputs</p>

\[\mathbf{x}^{(1)}
=
\begin{bmatrix} 1 \\ 0 \end{bmatrix},
\qquad
\mathbf{x}^{(2)}
=
\begin{bmatrix} 0 \\ 5 \end{bmatrix}.\]

<p>They are clearly different, but both produce exactly the same output:</p>

\[\mathbf{A}\mathbf{x}^{(1)}
=
\begin{bmatrix} 2.5 \\ 1 \end{bmatrix} (1 + \frac{1}{5} \cdot 0)
=
\begin{bmatrix} 2.5 \\ 1 \end{bmatrix}
=
\begin{bmatrix} 2.5 \\ 1 \end{bmatrix} (0 + \frac{1}{5} \cdot 5)
=
\mathbf{A}\mathbf{x}^{(2)}\]

<p>Now imagine that we are only given the output $\mathbf{y}=(2.5, 1)$ and are asked to recover the input.
Was the input $\mathbf{x}^{(1)}$, $\mathbf{x}^{(2)}$, or one of infinitely many other possibilities?
The matrix has discarded the information required to choose a <strong>unique</strong> answer for $\mathbf x$.</p>

<p>We can see the lost information by subtracting the two inputs to obtain the null space of the matrix $\mathbf{A}$. The null space are all the vectors which get mapped to the zero vector by the matrix.</p>

\[\mathbf{A}
\left(
\mathbf{x}^{(2)}-\mathbf{x}^{(1)}
\right)
=
\begin{bmatrix}
2.5 &amp; 0.5 \\
1 &amp; 0.2
\end{bmatrix}
\begin{bmatrix} -1 \\ 5 \end{bmatrix}
=
\begin{bmatrix} 0 \\ 0 \end{bmatrix}.\]

<p>The direction $(-1, 5)$ belongs to the null space of $\mathbf{A}$.
Moving the input in this direction does not move the output at all:</p>

\[\mathbf{A}
\left(
\mathbf{x}
+t\begin{bmatrix}-1 \\ 5\end{bmatrix}
\right)
=
\mathbf{A} x + t \underbrace{\mathbf{A} \begin{bmatrix}-1 \\ 5\end{bmatrix}}_{=\mathbf{0}}
=
\mathbf{A}\mathbf{x}\]

<p>for any value of $t$.
All inputs along this line are indistinguishable after applying $\mathbf{A}$.
So any vector $\mathbf{x}$ that is a scaled version of $(-1, 5)$ will be mapped to the zero vector by $\mathbf{A}$.</p>

<p>An inverse matrix is supposed to undo a matrix transformation.
If</p>

\[\mathbf{y}=\mathbf{A}\mathbf{x},\]

<p>then we would like to apply another matrix $\mathbf{A}^{-1}$ that recovers the original input. This is nothing else than the scalar case of $y =ax$ and getting $x = y / a$:</p>

\[\mathbf{x}=\mathbf{A}^{-1}\mathbf{y}.\]

<p>Doing something and then undoing it should be equivalent to doing nothing, so an inverse must satisfy</p>

\[\mathbf{A}^{-1}\mathbf{A}
=
\mathbf{A}\mathbf{A}^{-1}
=
\mathbf{I}.\]

<p>But our rank-one matrix cannot have such an inverse. If it did, applying $\mathbf{A}^{-1}$ to the collision above would give</p>

\[\mathbf{x}^{(1)}
=
\mathbf{A}^{-1} y =
\mathbf{A}^{-1}\mathbf{A} \underbrace{\mathbf{x}^{(1)}}_{=y}
=
\mathbf{A}^{-1} \underbrace{\mathbf{A}\mathbf{x}^{(2)}}_{=y}
=
\mathbf{x}^{(2)},\]

<p>even though the two inputs are different.
This is again because multiple inputs $\mathbf{x}^{(1)}$ or $\mathbf{x}^{(2)}$ can be mapped to the same output $\mathbf{y}$ by a rank-deficient matrix.</p>

<p>Below is a little animation in which you can drag and move a vector $\mathbf{x}$ and see how it is mapped by the matrix $\mathbf{A}$ to it’s output $\mathbf{y}$.
In the left animation, there is a line of inputs that all get mapped to the same output.
Since all points on that line get mapped to the same point, we can not uniquely recreate the input vice-versa.</p>

<style>
  #rank-projection-widget{
    --line:#e2e6e9;
    --ink:#212931;
    --dim:#626d78;
    --teal:#147f7a;
    --purple:#7153b8;
    --amber:#a96100;
    margin:26px 0;
    border:1px solid var(--line);
    border-radius:8px;
    background:#fff;
    color:var(--ink);
    font:400 14px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif;
    text-align:left;
  }
  #rank-projection-widget,
  #rank-projection-widget *{box-sizing:border-box;}
  #rank-projection-widget .projection-head{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:6px 12px;
    padding:18px 20px;
    border-bottom:1px solid var(--line);
  }
  #rank-projection-widget h4,
  #rank-projection-widget h5{
    margin:0;
    color:var(--ink);
    font:600 17px/1.35 "Helvetica Neue",Helvetica,Arial,sans-serif;
    letter-spacing:0;
    text-transform:none;
  }
  #rank-projection-widget h5{font-size:15px;}
  #rank-projection-widget .projection-matrix{
    display:flex;
    align-items:center;
    gap:10px;
    color:var(--dim);
    font-size:14px;
    font-variant-numeric:tabular-nums;
  }
  #rank-projection-widget .projection-matrix-entries{
    position:relative;
    display:grid;
    grid-template-columns:repeat(2,auto);
    gap:1px 12px;
    padding:2px 8px;
    text-align:center;
  }
  #rank-projection-widget .projection-matrix-entries::before,
  #rank-projection-widget .projection-matrix-entries::after{
    content:"";
    position:absolute;
    top:0;
    bottom:0;
    width:5px;
    border:1px solid currentColor;
  }
  #rank-projection-widget .projection-matrix-entries::before{left:0; border-right:0;}
  #rank-projection-widget .projection-matrix-entries::after{right:0; border-left:0;}
  #rank-projection-widget sup,
  #rank-projection-widget sub{font-size:0.75em;}
  #rank-projection-widget .projection-coordinates{
    font-variant-numeric:tabular-nums;
    white-space:nowrap;
  }
  #rank-projection-widget .projection-body{
    display:grid;
    grid-template-columns:minmax(0,1fr) 34px minmax(0,1fr);
    align-items:start;
    gap:8px;
    padding:18px;
  }
  #rank-projection-widget .projection-panel{min-width:0;}
  #rank-projection-widget .projection-panel-head{
    min-height:46px;
    margin-bottom:8px;
  }
  #rank-projection-widget .projection-note{
    display:block;
    margin-top:3px;
    color:var(--dim);
    font-size:12px;
  }
  #rank-projection-widget svg{
    display:block;
    width:100%;
    height:auto;
    border:1px solid var(--line);
    border-radius:4px;
    background:#fff;
  }
  #rank-projection-widget svg text{
    font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
    font-size:13px;
    paint-order:stroke;
    stroke:#fff;
    stroke-width:3px;
    stroke-linejoin:round;
  }
  #rank-projection-widget .projection-move-hint text{
    font-size:9px;
    stroke:none;
  }
  #rank-projection-widget [data-projection-handle]{cursor:grab; touch-action:none; outline:none;}
  #rank-projection-widget [data-projection-handle]:active{cursor:grabbing;}
  #rank-projection-widget [data-projection-handle]:focus-visible .projection-handle-shape{stroke-width:5;}
  #rank-projection-widget .projection-legend{
    display:flex;
    align-items:baseline;
    gap:8px;
    min-height:42px;
    margin-top:10px;
    color:var(--dim);
    font-size:12px;
  }
  #rank-projection-widget .projection-line-key{
    flex:0 0 20px;
    border-top:2px dashed #7e8791;
    transform:translateY(-3px);
  }
  #rank-projection-widget .projection-column-key{border-top:2px solid var(--amber);}
  #rank-projection-widget .projection-map{
    align-self:center;
    color:var(--amber);
    font:600 20px/1 ui-monospace,SFMono-Regular,Consolas,monospace;
    text-align:center;
  }
  #rank-projection-widget .projection-map span{
    display:block;
    margin-top:5px;
    color:var(--dim);
    font-size:11px;
  }
  #rank-projection-widget .projection-readout{
    min-width:0;
    padding:10px 12px;
    border:1px solid var(--line);
    border-radius:4px;
    font-size:14px;
    line-height:1.8;
  }
  #rank-projection-widget .projection-input-readout{
    display:flex;
    flex-wrap:wrap;
    align-items:baseline;
    gap:0 8px;
  }
  #rank-projection-widget .projection-first{color:var(--teal);}
  #rank-projection-widget .projection-second{color:var(--purple);}
  #rank-projection-widget .projection-third{color:var(--blue);}
  #rank-projection-widget .projection-message{
    margin:0;
    font-size:13px;
  }
  #rank-projection-widget .projection-actions{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:12px;
    border-top:1px solid var(--line);
    padding:14px 18px;
  }
  #rank-projection-widget button{
    height:auto;
    padding:7px 12px;
    color:var(--teal) !important;
    background:#fff;
    border:1px solid #8ebfbc;
    border-radius:4px;
    box-shadow:none;
    font:400 12px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;
    letter-spacing:0;
    text-transform:none;
  }
  #rank-projection-widget button:hover{
    color:#fff !important;
    background:var(--teal);
    box-shadow:none;
  }
  #rank-projection-widget .projection-sr-only{
    position:absolute;
    width:1px;
    height:1px;
    padding:0;
    margin:-1px;
    overflow:hidden;
    clip-path:inset(50%);
    white-space:nowrap;
    border:0;
  }
  @media (max-width:760px){
    #rank-projection-widget .projection-body{grid-template-columns:1fr;}
    #rank-projection-widget .projection-map{font-size:0; padding:2px 0;}
    #rank-projection-widget .projection-map::before{content:"↓"; font-size:22px;}
    #rank-projection-widget .projection-map span{display:none;}
    #rank-projection-widget .projection-panel-head{min-height:0;}
    #rank-projection-widget .projection-legend{min-height:0; margin-bottom:10px;}
  }
  @media (max-width:480px){
    #rank-projection-widget .projection-head,
    #rank-projection-widget .projection-body{padding:14px 12px;}
    #rank-projection-widget .projection-actions{padding:12px;}
  }
</style>

<div id="rank-projection-widget" class="matrix-figure" role="group" aria-labelledby="rank-projection-title">
  <div class="projection-head">
    <h4 id="rank-projection-title">A rank-one matrix forgets one direction</h4>
    <span class="projection-matrix" role="img" aria-label="A equals the matrix with rows 2.5, 0.5 and 1, 0.2">
      <span aria-hidden="true"><i>A</i> =</span>
      <span class="projection-matrix-entries" aria-hidden="true"><span>2.5</span><span>0.5</span><span>1</span><span>0.2</span></span>
    </span>
  </div>
  <div class="projection-body">
    <section class="projection-panel" aria-labelledby="rank-input-plane-title">
      <div class="projection-panel-head">
        <h5 id="rank-input-plane-title">Input space</h5>
        <span class="projection-note" id="rank-projection-help">Drag the first two points along the dashed line. Move the third freely.</span>
      </div>
      <svg viewBox="0 0 320 320" data-projection-input="" role="group" aria-label="Two inputs constrained to a same-output line and one freely draggable input" aria-describedby="rank-projection-help rank-projection-keyboard"></svg>
      <div class="projection-legend">
        <span class="projection-line-key" aria-hidden="true"></span>
        <span><strong>Same-output line (fiber):</strong> every point on the dashed line maps to the same <i>y</i>.<br />It is a shifted null space: <i>A</i><sup>−1</sup>(<i>y</i>) = <i>x</i><sup>(1)</sup> + ker(<i>A</i>).</span>
      </div>
      <div class="projection-readout">
        <div class="projection-input-readout projection-first"><span><i>x</i><sup>(1)</sup> =</span> <span class="projection-coordinates" data-projection-readout="0"></span></div>
        <div class="projection-input-readout projection-second"><span><i>x</i><sup>(2)</sup> =</span> <span class="projection-coordinates" data-projection-readout="1"></span></div>
        <div class="projection-input-readout projection-third"><span><i>x</i><sup>(3)</sup> =</span> <span class="projection-coordinates" data-projection-readout="2"></span></div>
      </div>
    </section>
    <div class="projection-map" aria-hidden="true">→<span><i>A</i></span></div>
    <section class="projection-panel" aria-labelledby="rank-output-line-title">
      <div class="projection-panel-head">
        <h5 id="rank-output-line-title">Output space</h5>
        <span class="projection-note">The first two outputs coincide; the free point still lands on the same line.</span>
      </div>
      <svg viewBox="0 0 320 320" data-projection-output="" role="img" aria-label="Three transformed outputs constrained to the column-space line, with the first two coinciding"></svg>
      <div class="projection-legend">
        <span class="projection-line-key projection-column-key" aria-hidden="true"></span>
        <span>Column space: the line through <i>a</i><sub>1</sub>.<br /><i>a</i><sub>2</sub> = 0.2<i>a</i><sub>1</sub> · rank(<i>A</i>) = 1</span>
      </div>
      <div class="projection-readout">
        <div class="projection-input-readout projection-first"><span><i>y</i><sup>(1)</sup> =</span> <span class="projection-coordinates" data-projection-output-readout="0"></span></div>
        <div class="projection-input-readout projection-second"><span><i>y</i><sup>(2)</sup> =</span> <span class="projection-coordinates" data-projection-output-readout="1"></span></div>
        <div class="projection-input-readout projection-third"><span><i>y</i><sup>(3)</sup> =</span> <span class="projection-coordinates" data-projection-output-readout="2"></span></div>
        <div class="projection-note" data-projection-collision-note=""></div>
      </div>
    </section>
  </div>
  <div class="projection-actions">
    <p class="projection-message">Only the combination <i>x</i><sub>1</sub> + 0.2<i>x</i><sub>2</sub> survives the transformation.<br /><strong>The output can move only along one line, so the map cannot be uniquely inverted.</strong></p>
    <button type="button" data-projection-reset="">Reset inputs</button>
  </div>
  <div class="projection-sr-only" id="rank-projection-keyboard">Focus an input and use the arrow keys to move it. The first two stay on the dashed line; the third moves freely. Hold Shift for larger steps.</div>
  <div class="projection-sr-only" data-projection-status="" role="status" aria-live="polite" aria-atomic="true"></div>
</div>
<script>
(function(){
  const root = document.getElementById('rank-projection-widget');
  const inputSvg = root.querySelector('[data-projection-input]');
  const outputSvg = root.querySelector('[data-projection-output]');
  const inputReadouts = [...root.querySelectorAll('[data-projection-readout]')];
  const outputReadouts = [...root.querySelectorAll('[data-projection-output-readout]')];
  const collisionNote = root.querySelector('[data-projection-collision-note]');
  const status = root.querySelector('[data-projection-status]');
  const reset = root.querySelector('[data-projection-reset]');
  const colors = ['#147f7a','#7153b8','#2f6fad'];
  const initial = [[1,0],[0,5],[2,2]];
  const points = initial.map(point=>point.slice());
  let dragging = null;

  const frame = {left:38,right:302,top:26,bottom:290};
  const inputPlot = {min:[-1,-1],max:[6,6],scale:(302-38)/7};
  const outputPlot = {min:[-5,-5],max:[20,20],scale:(302-38)/25};
  const fmt = value => (Math.abs(value)<1e-9 ? 0 : value).toFixed(2);
  const vector = values => '('+values.map(fmt).join(', ')+')';
  const transform = point => {
    const coordinate = point[0]+0.2*point[1];
    return [2.5*coordinate,coordinate];
  };
  const linePoint = parameter => [1-0.2*parameter,parameter];
  const screen = (plot,point) => [
    frame.left+(point[0]-plot.min[0])*plot.scale,
    frame.bottom-(point[1]-plot.min[1])*plot.scale
  ];
  const inputScreen = point => screen(inputPlot,point);
  const outputScreen = point => screen(outputPlot,point);

  function element(tag,attributes,parent,text){
    const node = document.createElementNS('http://www.w3.org/2000/svg',tag);
    Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));
    if(text!==undefined) node.textContent=text;
    parent.appendChild(node);
    return node;
  }

  function indexedLabel(parent,symbol,index,attributes){
    const label=element('text',attributes,parent);
    element('tspan',{},label,symbol);
    element('tspan',{'baseline-shift':'super','font-size':9},label,'('+(index+1)+')');
    return label;
  }

  function moveHint(parent,index){
    const positions=[
      {x:16,y:-31},
      {x:16,y:14},
      {x:16,y:-31}
    ];
    const position=positions[index];
    const hint=element('g',{
      transform:'translate('+position.x+' '+position.y+')',
      class:'projection-move-hint move-hint',
      'data-projection-move-hint':index,
      'aria-hidden':'true'
    },parent);
    element('rect',{
      x:0,y:0,width:96,height:20,rx:3,
      fill:'rgba(255,255,255,0.96)',stroke:colors[index],'stroke-opacity':0.25,'stroke-width':1
    },hint);
    element('text',{
      x:6,y:13.5,fill:colors[index],'font-size':8.5,'font-weight':600,
      stroke:'none'
    },hint,'Drag & Move Me!');
    return hint;
  }

  function drawAxes(svg,plot,toScreen,xLabel,yLabel,step){
    const grid = element('g',{'data-projection-grid':'',stroke:'#eef0f2','stroke-width':1},svg);
    const firstX=Math.ceil(plot.min[0]/step)*step;
    const firstY=Math.ceil(plot.min[1]/step)*step;
    for(let tick=firstX;tick<=plot.max[0];tick+=step){
      const x=toScreen([tick,0])[0];
      if(tick!==0) element('line',{x1:x,y1:frame.top,x2:x,y2:frame.bottom},grid);
      element('text',{x,y:309,fill:'#76818c','text-anchor':'middle'},svg,tick);
    }
    for(let tick=firstY;tick<=plot.max[1];tick+=step){
      const y=toScreen([0,tick])[1];
      if(tick!==0) element('line',{x1:frame.left,y1:y,x2:frame.right,y2:y},grid);
      element('text',{x:29,y:y+4,fill:'#76818c','text-anchor':'end'},svg,tick);
    }
    const origin=toScreen([0,0]);
    element('line',{x1:frame.left,y1:origin[1],x2:frame.right,y2:origin[1],stroke:'#aeb7c0','data-projection-axis':'x'},svg);
    element('line',{x1:origin[0],y1:frame.top,x2:origin[0],y2:frame.bottom,stroke:'#aeb7c0','data-projection-axis':'y'},svg);
    element('text',{x:302,y:309,fill:'#626d78','text-anchor':'end'},svg,xLabel);
    element('text',{x:29,y:16,fill:'#626d78','text-anchor':'end'},svg,yLabel);
  }

  drawAxes(inputSvg,inputPlot,inputScreen,'x₁','x₂',1);
  drawAxes(outputSvg,outputPlot,outputScreen,'y₁','y₂',5);

  const sameOutputStart=inputScreen(linePoint(inputPlot.min[1]));
  const sameOutputEnd=inputScreen(linePoint(inputPlot.max[1]));
  element('line',{
    x1:sameOutputStart[0],y1:sameOutputStart[1],x2:sameOutputEnd[0],y2:sameOutputEnd[1],
    stroke:'#7e8791','stroke-width':1.5,'stroke-dasharray':'5 5','data-projection-same-output-line':''
  },inputSvg);

  const trailLayer=element('g',{'aria-hidden':'true','pointer-events':'none'},inputSvg);
  const trails=[[],[]];
  const reducedMotion=window.matchMedia('(prefers-reduced-motion: reduce)');

  function recordTrail(index){
    if(index>=2||reducedMotion.matches) return;
    const previous=trails[index][trails[index].length-1];
    if(previous&&Math.hypot(points[index][0]-previous[0],points[index][1]-previous[1])<0.12) return;
    trails[index].push(points[index].slice());
    if(trails[index].length>12) trails[index].shift();
    trailLayer.replaceChildren();
    trails.forEach((trail,component)=>trail.forEach((point,position)=>{
      const [x,y]=inputScreen(point);
      element('circle',{
        cx:x,cy:y,r:2.5,fill:colors[component],
        opacity:0.06+0.2*(position+1)/trail.length
      },trailLayer);
    }));
  }

  const inputMoveHints=[];
  const inputHandles = points.map((point,index)=>{
    const group = element('g',{
      'data-projection-handle':index,role:'button',tabindex:0,
      'aria-label':'Move input x superscript '+(index+1)+' in two dimensions',
      'aria-describedby':'rank-projection-keyboard'
    },inputSvg);
    element('circle',{r:18,fill:'transparent'},group);
    if(index===0){
      element('circle',{r:9,fill:'#fff',stroke:colors[index],'stroke-width':3,class:'projection-handle-shape'},group);
    }else if(index===1){
      element('path',{d:'M 0 -8 L 8 0 L 0 8 L -8 0 Z',fill:'#fff',stroke:colors[index],'stroke-width':3,class:'projection-handle-shape'},group);
    }else{
      element('path',{d:'M 0 -9 L 9 7 L -9 7 Z',fill:'#fff',stroke:colors[index],'stroke-width':3,class:'projection-handle-shape'},group);
    }
    indexedLabel(group,'x',index,{
      x:index===1?-16:16,y:index===2?20:-12,fill:colors[index],'font-weight':600,
      'text-anchor':index===1?'end':'start'
    });
    inputMoveHints[index]=moveHint(group,index);
    return group;
  });

  const lineStart = outputScreen([outputPlot.min[0],0.4*outputPlot.min[0]]);
  const lineEnd = outputScreen([outputPlot.max[0],0.4*outputPlot.max[0]]);
  element('line',{
    x1:lineStart[0],y1:lineStart[1],x2:lineEnd[0],y2:lineEnd[1],
    stroke:'#a96100','stroke-width':1.8,'data-projection-column-space':''
  },outputSvg);
  const outputHandles = points.map((point,index)=>{
    const group=element('g',{'data-projection-output-point':index},outputSvg);
    if(index===0){
      element('circle',{r:10,fill:'#fff',stroke:colors[index],'stroke-width':3},group);
    }else if(index===1){
      element('path',{d:'M 0 -7 L 7 0 L 0 7 L -7 0 Z',fill:'#fff',stroke:colors[index],'stroke-width':3},group);
    }else{
      element('path',{d:'M 0 -9 L 9 7 L -9 7 Z',fill:'#fff',stroke:colors[index],'stroke-width':3},group);
    }
    indexedLabel(group,'y',index,{
      x:index===1?-14:14,y:index===0?-12:index===1?18:-12,fill:colors[index],'font-weight':600,
      'text-anchor':index===1?'end':'start'
    });
    return group;
  });

  function draw(){
    const outputs=points.map(transform);
    points.forEach((point,index)=>{
      const [x,y] = inputScreen(point);
      inputHandles[index].setAttribute('transform','translate('+x+' '+y+')');
      inputHandles[index].setAttribute('data-x1',point[0]);
      inputHandles[index].setAttribute('data-x2',point[1]);
      inputHandles[index].setAttribute('aria-label','Input x superscript '+(index+1)+' at '+vector(point));
      inputReadouts[index].textContent=vector(point);

      const output=outputs[index];
      const [outputX,outputY]=outputScreen(output);
      outputHandles[index].setAttribute('transform','translate('+outputX+' '+outputY+')');
      outputHandles[index].setAttribute('data-y1',output[0]);
      outputHandles[index].setAttribute('data-y2',output[1]);
      outputReadouts[index].textContent=vector(output);
    });
    const thirdSeparation=Math.hypot(outputs[0][0]-outputs[2][0],outputs[0][1]-outputs[2][1]);
    collisionNote.textContent=thirdSeparation<1e-8
      ? 'All three outputs coincide at this position.'
      : 'The first two outputs coincide; the free point produces another point on the same line.';
  }

  function announce(){
    status.textContent='Input 1: '+inputReadouts[0].textContent+', output 1: '+outputReadouts[0].textContent+
      '. Input 2: '+inputReadouts[1].textContent+', output 2: '+outputReadouts[1].textContent+
      '. Input 3: '+inputReadouts[2].textContent+', output 3: '+outputReadouts[2].textContent+'.';
  }

  function moveFree(index,point){
    const next=[
      Math.max(inputPlot.min[0],Math.min(inputPlot.max[0],point[0])),
      Math.max(inputPlot.min[1],Math.min(inputPlot.max[1],point[1]))
    ];
    if(Math.hypot(next[0]-points[index][0],next[1]-points[index][1])>1e-9){
      inputMoveHints[index].classList.add('is-dismissed');
    }
    points[index][0]=next[0];
    points[index][1]=next[1];
    draw();
  }

  function moveOnLine(index,parameter){
    const clamped=Math.max(inputPlot.min[1],Math.min(inputPlot.max[1],parameter));
    if(Math.abs(clamped-points[index][1])>1e-9){
      recordTrail(index);
      inputMoveHints[index].classList.add('is-dismissed');
    }
    points[index]=linePoint(clamped);
    draw();
  }

  function plotPoint(cursor){
    return [
      inputPlot.min[0]+(cursor.x-frame.left)/inputPlot.scale,
      inputPlot.min[1]+(frame.bottom-cursor.y)/inputPlot.scale
    ];
  }

  function parameterAt(point){
    return (-0.2*(point[0]-1)+point[1])/1.04;
  }

  function localPoint(event){
    const point = inputSvg.createSVGPoint();
    point.x=event.clientX;
    point.y=event.clientY;
    return point.matrixTransform(inputSvg.getScreenCTM().inverse());
  }

  inputHandles.forEach((handle,index)=>{
    handle.addEventListener('pointerdown',event=>{
      if(dragging||event.button!==0) return;
      const cursor=plotPoint(localPoint(event));
      dragging=index<2
        ? {index,pointerId:event.pointerId,mode:'line',offset:points[index][1]-parameterAt(cursor)}
        : {index,pointerId:event.pointerId,mode:'free',offset:[points[index][0]-cursor[0],points[index][1]-cursor[1]]};
      handle.setPointerCapture(event.pointerId);
      handle.focus({preventScroll:true});
      event.preventDefault();
    });
    handle.addEventListener('keydown',event=>{
      const step=event.shiftKey?0.5:0.1;
      if(index<2){
        if(event.key==='ArrowUp'||event.key==='ArrowLeft') moveOnLine(index,points[index][1]+step);
        else if(event.key==='ArrowDown'||event.key==='ArrowRight') moveOnLine(index,points[index][1]-step);
        else return;
      }else{
        const next=points[index].slice();
        if(event.key==='ArrowUp') next[1]+=step;
        else if(event.key==='ArrowDown') next[1]-=step;
        else if(event.key==='ArrowLeft') next[0]-=step;
        else if(event.key==='ArrowRight') next[0]+=step;
        else return;
        moveFree(index,next);
      }
      announce();
      event.preventDefault();
    });
  });

  inputSvg.addEventListener('pointermove',event=>{
    if(!dragging||dragging.pointerId!==event.pointerId) return;
    const cursor=plotPoint(localPoint(event));
    if(dragging.mode==='line'){
      moveOnLine(dragging.index,parameterAt(cursor)+dragging.offset);
    }else{
      moveFree(dragging.index,[cursor[0]+dragging.offset[0],cursor[1]+dragging.offset[1]]);
    }
    event.preventDefault();
  });

  function stopDragging(event){
    if(!dragging||dragging.pointerId!==event.pointerId) return;
    const handle=inputHandles[dragging.index];
    if(handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
    dragging=null;
    announce();
  }

  inputSvg.addEventListener('pointerup',stopDragging);
  inputSvg.addEventListener('pointercancel',stopDragging);
  window.addEventListener('pointerup',stopDragging);
  window.addEventListener('pointercancel',stopDragging);

  reset.addEventListener('click',()=>{
    initial.forEach((point,index)=>{
      points[index][0]=point[0];
      points[index][1]=point[1];
      inputMoveHints[index].classList.remove('is-dismissed');
    });
    trails.forEach(trail=>trail.length=0);
    trailLayer.replaceChildren();
    draw();
    announce();
  });
  draw();
})();
</script>

<p>This gives us the essential condition for invertibility:</p>

<blockquote>
  <p><strong>A square matrix is invertible exactly when it has full rank.</strong></p>
</blockquote>

<h3 id="determinants-of-matrices">Determinants of Matrices</h3>

<p>The previous sections asked whether a matrix preserves all directions or squashes some of them together.
The determinant packages the same geometric story into a single number: it tells us how a matrix scales area.</p>

<p>We begin with the simplest case, a diagonal matrix:</p>

\[\begin{align*}
\mathbf{A}
&amp;=
\begin{bmatrix}
a_{11} &amp; 0 \\
0 &amp; a_{22}
\end{bmatrix}
=
\underset{
\begin{array}{cc}
\underbrace{\hphantom{\begin{bmatrix} a_{11} \\ 0 \end{bmatrix}}}_{\mathbf{a}_1}
&amp;
\underbrace{\hphantom{\begin{bmatrix} 0 \\ a_{22} \end{bmatrix}}}_{\mathbf{a}_2}
\end{array}
}{
\begin{bmatrix}
\begin{bmatrix} a_{11} \\ 0 \end{bmatrix}
&amp;
\begin{bmatrix} 0 \\ a_{22} \end{bmatrix}
\end{bmatrix}
}.
\end{align*}\]

<p>The first column stays on the horizontal axis and the second stays on the vertical axis.
Consequently, the unit square becomes a rectangle with side lengths $\lvert a_{11}\rvert$ and $\lvert a_{22}\rvert$, and therefore with area $\lvert a_{11}\rvert\lvert a_{22}\rvert=\lvert a_{11}a_{22}\rvert$.
The corresponding signed area is</p>

\[\det(\mathbf{A})=\sqrt{a_{11}^2 + 0^2} \sqrt{0^2 + a_{22}^2} = \sqrt{a_{11}^2} \sqrt{a_{22}^2} = |a_{11}| |a_{22}|.\]

<p>Drag the two column vectors below.
They are constrained to their respective axes, making it visible that each diagonal entry scales one side independently and that their product scales the area.</p>

<style>
  .determinant-widget{
    --bg: #ffffff;
    --bg2: #ffffff;
    --panel: #ffffff;
    --panel-line: #d9dee5;
    --ink: #212931;
    --ink-dim: #626d78;
    --amber: #b66a00;
    --teal: #147f7a;
    --teal-dim: #8ebfbc;
    --rose: #c43b54;
    --paper: #212931;
    --serif-body:"Helvetica Neue",Helvetica,Arial,sans-serif;
    --mono:ui-monospace,SFMono-Regular,Consolas,monospace;
    margin:26px 0;
    color:var(--ink);
    font-family:var(--serif-body);
    font-size:18px;
    font-weight:400;
    line-height:1.65;
    text-align:left;
    -webkit-font-smoothing:antialiased;
    background:var(--panel);
    border:1px solid var(--panel-line);
    border-radius:6px;
    overflow:hidden;
  }
  .determinant-widget,
  .determinant-widget *{box-sizing:border-box;}
  .determinant-widget .widget-titlewrap{
    display:flex;
    flex-wrap:wrap;
    justify-content:space-between;
    align-items:center;
    gap:8px;
    padding:14px 18px;
    border-bottom:1px solid var(--panel-line);
    background:var(--bg2);
  }
  .determinant-widget .widget-title{font-family:var(--mono); font-size:13.5px; color:var(--paper); font-weight:500;}
  .determinant-widget .wt-sub{font-family:var(--mono); font-size:11px; color:var(--ink-dim); text-transform:uppercase; letter-spacing:0.06em;}
  .determinant-widget .widget-body{padding:18px;}
  .determinant-widget canvas{
    display:block;
    width:100%;
    height:auto;
    aspect-ratio:680 / 420;
    background:var(--bg2);
    border-radius:3px;
    border:1px solid var(--panel-line);
    cursor:grab;
    touch-action:none;
  }
  .determinant-widget canvas.is-dragging{cursor:grabbing;}
  .determinant-widget .btnrow{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;}
  .determinant-widget button.step-btn{
    font-family:var(--mono);
    font-size:12.5px;
    font-weight:400;
    height:auto;
    line-height:normal;
    letter-spacing:0.03em;
    text-transform:none;
    white-space:normal;
    box-shadow:none;
    background:transparent;
    border:1px solid var(--teal-dim);
    /* The site's button colors also use !important. */
    color:var(--teal) !important;
    padding:8px 14px;
    border-radius:4px;
    cursor:pointer;
    transition:background .15s, color .15s;
  }
  .determinant-widget button.step-btn:hover{background:var(--teal); color:var(--bg2) !important;}
  .determinant-widget button.step-btn:disabled{opacity:0.35; cursor:default;}
  .determinant-widget .caption{
    font-family:var(--mono);
    font-size:12.5px;
    color:var(--amber);
    margin-top:12px;
    min-height:18px;
  }
  .determinant-widget .readout{
    font-family:var(--mono);
    font-size:14px;
    color:var(--paper);
    margin-top:10px;
    background:var(--bg2);
    border:1px solid var(--panel-line);
    border-radius:4px;
    padding:10px 14px;
  }
  .determinant-widget .readout .neg{color:var(--rose);}
  .determinant-widget .readout .pos{color:var(--teal);}
  .determinant-widget .w2-equation{
    margin-top:14px;
    padding:20px 18px;
    background:var(--bg2);
    border:1px solid var(--panel-line);
    border-radius:4px;
  }
  .determinant-widget .w2-equation p{margin:0 0 16px; text-align:left;}
  .determinant-widget .w2-equation .eq-label{
    margin-bottom:16px;
    font-family:var(--mono);
    font-size:11px;
    letter-spacing:0.1em;
    text-transform:uppercase;
    color:var(--teal);
  }
  .determinant-widget .w2-equation .eq-step{
    display:none;
    margin:0 0 16px;
    color:var(--ink);
  }
  .determinant-widget .w2-equation .eq-step.is-revealed{display:block;}
  .determinant-widget .w2-equation .eq-note{
    color:var(--ink-dim);
    font-size:0.92em;
    line-height:1.45;
  }
  .determinant-widget .w2-equation .eq-step > p:not(.eq-note){
    max-width:100%;
    overflow-x:auto;
    overflow-y:hidden;
  }
  .determinant-widget ::selection{background:var(--amber); color:var(--bg2);}
  .determinant-widget mjx-container{max-width:100%;}
  .determinant-widget mjx-container[display="true"]{
    overflow-x:auto;
    overflow-y:hidden;
    padding:2px 0 6px;
  }
</style>

<style>
  #determinant-diagonal-widget{
    --d1-teal:#0f766e;
    --d1-teal-soft:#e8f5f2;
    --d1-gold:#a76108;
    --d1-gold-soft:#fff5df;
    --d1-rose:#b8324b;
    --d1-rose-soft:#fff0f2;
    border-color:#dce5e3;
    border-radius:12px;
    box-shadow:0 14px 34px rgba(33,41,49,0.07);
  }
  #determinant-diagonal-widget .widget-titlewrap{
    display:grid;
    grid-template-columns:minmax(0,1fr) auto;
    align-items:start;
    padding:18px 20px 17px;
    background:linear-gradient(135deg,#ffffff 0%,#f6faf9 100%);
    border-bottom-color:#e3ebe9;
  }
  #determinant-diagonal-widget .d1-heading{display:grid; min-width:0; gap:4px;}
  #determinant-diagonal-widget .d1-kicker{
    color:var(--d1-teal);
    font:600 10px/1.2 var(--mono);
    letter-spacing:0.11em;
    text-transform:uppercase;
  }
  #determinant-diagonal-widget .widget-title{
    display:block;
    color:#172129;
    font-size:15px;
    font-weight:600;
    line-height:1.45;
    overflow-wrap:anywhere;
  }
  #determinant-diagonal-widget .wt-sub{
    margin-top:1px;
    padding:5px 9px;
    color:#52616a;
    background:rgba(255,255,255,0.82);
    border:1px solid #dbe5e3;
    border-radius:999px;
    letter-spacing:0.04em;
    text-transform:none;
  }
  #determinant-diagonal-widget .widget-body{padding:20px;}
  #determinant-diagonal-widget .d1-plot-shell{
    overflow:hidden;
    background:#fbfdfc;
    border:1px solid #dce5e3;
    border-radius:9px;
  }
  #determinant-diagonal-widget .d1-plot-key{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    gap:7px 14px;
    min-height:40px;
    padding:9px 12px;
    color:#5a6871;
    background:#f8fbfa;
    border-bottom:1px solid #e2eae8;
    font:500 10.5px/1.35 var(--mono);
  }
  #determinant-diagonal-widget .d1-key-item{
    display:inline-flex;
    align-items:center;
    gap:6px;
    white-space:nowrap;
  }
  #determinant-diagonal-widget .d1-key-item:last-child{margin-left:auto;}
  #determinant-diagonal-widget .d1-swatch{
    width:16px;
    height:9px;
    border-radius:3px;
    background:var(--d1-teal-soft);
    border:1.5px solid var(--d1-teal);
  }
  #determinant-diagonal-widget .d1-swatch.unit{
    background:#fff;
    border-color:#839099;
    border-style:dashed;
  }
  #determinant-diagonal-widget .d1-handle-icon{
    width:11px;
    height:11px;
    border:3px solid #fff;
    border-radius:50%;
    background:var(--d1-gold);
    box-shadow:0 0 0 1px var(--d1-gold);
  }
  #determinant-diagonal-widget canvas{
    height:clamp(230px,61.75vw,420px);
    aspect-ratio:auto;
    background:#fbfdfc;
    border:0;
    border-radius:0;
  }
  #determinant-diagonal-widget canvas:focus-visible{
    outline:3px solid rgba(15,118,110,0.28);
    outline-offset:-3px;
  }
  #determinant-diagonal-widget canvas.is-hovering{cursor:pointer;}
  #determinant-diagonal-widget .readout{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:10px;
    margin-top:12px;
    padding:0;
    background:transparent;
    border:0;
  }
  #determinant-diagonal-widget .d1-metric{
    min-width:0;
    padding:13px 15px 12px;
    background:#fbfdfc;
    border:1px solid #dce5e3;
    border-radius:8px;
  }
  #determinant-diagonal-widget .d1-metric.det{
    background:#f6faf9;
    border-color:#cddfdb;
  }
  #determinant-diagonal-widget .d1-metric-label{
    display:block;
    margin-bottom:4px;
    color:#69757d;
    font:600 9.5px/1.3 var(--mono);
    letter-spacing:0.09em;
    text-transform:uppercase;
  }
  #determinant-diagonal-widget .d1-metric strong{
    display:block;
    overflow-wrap:anywhere;
    color:#172129;
    font:600 20px/1.25 var(--mono);
    font-variant-numeric:tabular-nums;
  }
  #determinant-diagonal-widget .d1-metric.det strong{color:var(--d1-teal);}
  #determinant-diagonal-widget .d1-metric.det strong.neg{color:var(--d1-rose);}
  #determinant-diagonal-widget .d1-metric-detail{
    display:block;
    margin-top:4px;
    color:#69757d;
    font:400 10.5px/1.4 var(--mono);
  }
  #determinant-diagonal-widget .d1-actions{
    display:flex;
    align-items:center;
    gap:14px;
    margin-top:12px;
    padding:11px 12px;
    color:#42515a;
    background:#fffaf0;
    border:1px solid #f0dfbd;
    border-radius:8px;
  }
  #determinant-diagonal-widget .caption{
    flex:1 1 auto;
    margin:0;
    min-height:0;
    color:#85520b;
    font-size:11.5px;
    line-height:1.5;
  }
  #determinant-diagonal-widget .btnrow{flex:0 0 auto; margin:0;}
  #determinant-diagonal-widget button.step-btn{
    padding:7px 11px;
    background:#fff;
    border-color:#adcac5;
    border-radius:6px;
    font-size:11px;
    font-weight:500;
  }
  #determinant-diagonal-widget button.step-btn:hover{
    background:var(--d1-teal);
    color:#fff !important;
  }
  #determinant-diagonal-widget button.step-btn:focus-visible{
    outline:3px solid rgba(15,118,110,0.22);
    outline-offset:2px;
  }
  @media (max-width:520px){
    #determinant-diagonal-widget .widget-titlewrap,
    #determinant-diagonal-widget .widget-body{padding:14px;}
    #determinant-diagonal-widget .widget-titlewrap{gap:10px;}
    #determinant-diagonal-widget .widget-titlewrap{grid-template-columns:1fr;}
    #determinant-diagonal-widget .widget-title{font-size:13px;}
    #determinant-diagonal-widget .wt-sub{justify-self:start; font-size:9.5px;}
    #determinant-diagonal-widget .d1-plot-key{gap:6px 10px; padding:8px 9px;}
    #determinant-diagonal-widget .d1-key-item:last-child{width:100%; margin-left:0;}
    #determinant-diagonal-widget .readout{grid-template-columns:1fr;}
    #determinant-diagonal-widget .d1-actions{align-items:flex-start; flex-direction:column;}
    #determinant-diagonal-widget .btnrow,
    #determinant-diagonal-widget button.step-btn{width:100%;}
  }
</style>

<div id="determinant-diagonal-widget" class="widget determinant-widget matrix-figure">
  <div class="widget-titlewrap">
    <div class="d1-heading">
      <span class="d1-kicker">From square to rectangle</span>
      <span class="widget-title">Diagonal scaling: rectangle area \(=|a_{11}|\,|a_{22}|\)</span>
    </div>
    <span class="wt-sub">drag along the axes</span>
  </div>
  <div class="widget-body">
    <div class="d1-plot-shell">
      <div class="d1-plot-key" aria-hidden="true">
        <span class="d1-key-item"><i class="d1-swatch unit"></i>unit square</span>
        <span class="d1-key-item"><i class="d1-swatch"></i>scaled rectangle</span>
        <span class="d1-key-item"><i class="d1-handle-icon"></i>drag either handle</span>
      </div>
      <canvas id="d1canvas" width="680" height="420" tabindex="0" role="application" aria-describedby="d1help d1caption" aria-label="Interactive diagonal matrix. Use left and right arrows to change a 11, and up and down arrows to change a 22."></canvas>
      <span class="figure-canvas-hint move-hint" data-d1-hint="a" aria-hidden="true">Drag &amp; Move Me!</span>
      <span class="figure-canvas-hint move-hint second" data-d1-hint="d" aria-hidden="true">Drag &amp; Move Me!</span>
    </div>
    <div class="readout" aria-live="polite">
      <div class="d1-metric">
        <span class="d1-metric-label">Geometric area · always non-negative</span>
        <strong id="d1area">—</strong>
        <span class="d1-metric-detail"><span id="d1width">—</span> × <span id="d1height">—</span> = \(|a_{11}|\,|a_{22}|=|a_{11}a_{22}|\)</span>
      </div>
      <div class="d1-metric det">
        <span class="d1-metric-label">Signed area · determinant</span>
        <strong id="d1det">—</strong>
        <span class="d1-metric-detail">\(\det(\mathbf A)=a_{11}a_{22}\) · <span id="d1orientation">—</span></span>
      </div>
    </div>
    <div class="d1-actions">
      <div class="caption" id="d1caption">The columns stay on the coordinate axes, so the unit square can only stretch into a rectangle.</div>
      <div class="btnrow">
        <button class="step-btn" id="d1reset">Reset rectangle</button>
      </div>
    </div>
    <p id="d1help" hidden="">Drag the teal horizontal handle and gold vertical handle. With the plot focused, the left and right arrow keys change a 11, and the up and down arrow keys change a 22. Hold Shift for larger steps.</p>
  </div>
</div>
<script>
(function(){
  const root = document.getElementById('determinant-diagonal-widget');
  const canvas = root.querySelector('#d1canvas');
  const ctx = canvas.getContext('2d');
  const areaEl = root.querySelector('#d1area');
  const detEl = root.querySelector('#d1det');
  const widthEl = root.querySelector('#d1width');
  const heightEl = root.querySelector('#d1height');
  const orientationEl = root.querySelector('#d1orientation');
  const captionEl = root.querySelector('#d1caption');
  const resetBtn = root.querySelector('#d1reset');
  const moveHints={a:root.querySelector('[data-d1-hint="a"]'),d:root.querySelector('[data-d1-hint="d"]')};
  const limits = {a:[-4.5,4.5], d:[-2.8,2.8]};
  const initial = {a:2.8,d:2};
  let W = Number(canvas.getAttribute('width'));
  let H = Number(canvas.getAttribute('height'));
  let ox = W/2;
  let oy = H/2;
  let scale = 58;
  let a = initial.a;
  let d = initial.d;
  let dragging = null;
  let dragPointerId = null;

  function fmt(value){
    return (Math.abs(value)<1e-9?0:value).toFixed(2);
  }

  function toScreen(point){
    return [ox+point.x*scale,oy-point.y*scale];
  }

  function resizeCanvas(){
    const pixelRatio = Math.max(1,window.devicePixelRatio||1);
    const cssWidth = Math.max(1,Math.round(canvas.clientWidth));
    const cssHeight = Math.max(1,Math.round(canvas.clientHeight));
    const width = Math.round(cssWidth*pixelRatio);
    const height = Math.round(cssHeight*pixelRatio);
    if(canvas.width===width && canvas.height===height) return false;
    canvas.width = width;
    canvas.height = height;
    W=cssWidth;
    H=cssHeight;
    ox=W/2;
    oy=H/2;
    scale=Math.min(
      (W-56)/(limits.a[1]-limits.a[0]),
      (H-48)/(limits.d[1]-limits.d[0])
    );
    ctx.setTransform(pixelRatio,0,0,pixelRatio,0,0);
    return true;
  }

  function polygon(points,fill,stroke,dash,lineWidth){
    ctx.save();
    ctx.setLineDash(dash||[]);
    ctx.beginPath();
    points.forEach((point,index)=>{
      const [x,y] = toScreen(point);
      if(index===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
    });
    ctx.closePath();
    if(fill){ctx.fillStyle=fill;ctx.fill();}
    if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=lineWidth||1.7;ctx.stroke();}
    ctx.restore();
  }

  function roundedRect(x,y,width,height,radius){
    const r=Math.min(radius,width/2,height/2);
    ctx.beginPath();
    ctx.moveTo(x+r,y);
    ctx.arcTo(x+width,y,x+width,y+height,r);
    ctx.arcTo(x+width,y+height,x,y+height,r);
    ctx.arcTo(x,y+height,x,y,r);
    ctx.arcTo(x,y,x+width,y,r);
    ctx.closePath();
  }

  function pill(text,x,y,color){
    ctx.save();
    ctx.font='500 11px "Helvetica Neue", Helvetica, Arial, sans-serif';
    const width=ctx.measureText(text).width+14;
    const height=23;
    const left=Math.max(5,Math.min(W-width-5,x-width/2));
    const top=Math.max(5,Math.min(H-height-5,y-height/2));
    ctx.fillStyle='rgba(255,255,255,0.96)';
    roundedRect(left,top,width,height,6);
    ctx.fill();
    ctx.shadowColor='transparent';
    ctx.strokeStyle='#e2e8ec';
    ctx.lineWidth=1;
    ctx.stroke();
    ctx.fillStyle=color;
    ctx.textBaseline='middle';
    ctx.fillText(text,left+7,top+height/2+0.5);
    ctx.restore();
  }

  function arrow(point,color,label){
    const [x,y] = toScreen(point);
    const angle = Math.atan2(y-oy,x-ox);
    ctx.save();
    ctx.lineCap='round';
    ctx.strokeStyle=color;
    ctx.fillStyle=color;
    ctx.lineWidth=3;
    ctx.beginPath();
    ctx.moveTo(ox,oy);
    ctx.lineTo(x,y);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x,y);
    ctx.lineTo(x-10*Math.cos(angle-0.3),y-10*Math.sin(angle-0.3));
    ctx.lineTo(x-10*Math.cos(angle+0.3),y-10*Math.sin(angle+0.3));
    ctx.closePath();
    ctx.fill();
    ctx.globalAlpha=0.13;
    ctx.beginPath();
    ctx.arc(x,y,13,0,Math.PI*2);
    ctx.fill();
    ctx.globalAlpha=1;
    ctx.fillStyle='#ffffff';
    ctx.beginPath();
    ctx.arc(x,y,9,0,Math.PI*2);
    ctx.fill();
    ctx.strokeStyle='#ffffff';
    ctx.lineWidth=4;
    ctx.stroke();
    ctx.fillStyle=color;
    ctx.beginPath();
    ctx.arc(x,y,5.5,0,Math.PI*2);
    ctx.fill();
    ctx.restore();
    const labelX=x+(point.x<0?-54:54);
    const labelY=y+(point.y<0?20:-20);
    pill(label,labelX,labelY,color);
  }

  function draw(){
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle='#fbfdfc';
    ctx.fillRect(0,0,W,H);
    ctx.strokeStyle='rgba(52,72,78,0.075)';
    ctx.lineWidth=1;
    for(let x=Math.ceil(limits.a[0]);x<=Math.floor(limits.a[1]);x++){
      const [sx] = toScreen({x:x,y:0});
      ctx.beginPath();
      ctx.moveTo(sx,12);
      ctx.lineTo(sx,H-12);
      ctx.stroke();
    }
    for(let y=Math.ceil(limits.d[0]);y<=Math.floor(limits.d[1]);y++){
      const [,sy] = toScreen({x:0,y:y});
      ctx.beginPath();
      ctx.moveTo(12,sy);
      ctx.lineTo(W-12,sy);
      ctx.stroke();
    }

    ctx.strokeStyle='rgba(33,41,49,0.42)';
    ctx.lineWidth=1.35;
    ctx.beginPath();
    ctx.moveTo(12,oy);
    ctx.lineTo(W-12,oy);
    ctx.moveTo(ox,12);
    ctx.lineTo(ox,H-12);
    ctx.stroke();

    ctx.fillStyle='#69757d';
    ctx.font='400 10px "Helvetica Neue", Helvetica, Arial, sans-serif';
    ctx.fillText('x',W-20,oy-8);
    ctx.fillText('y',ox+8,19);

    const det = a*d;
    const positive = det>=0;
    const rectangleFill=positive?'rgba(15,118,110,0.16)':'rgba(184,50,75,0.14)';
    const rectangleStroke=positive?'#0f766e':'#b8324b';
    polygon(
      [{x:0,y:0},{x:a,y:0},{x:a,y:d},{x:0,y:d}],
      rectangleFill,
      rectangleStroke,
      [],
      2.2
    );

    polygon(
      [{x:0,y:0},{x:1,y:0},{x:1,y:1},{x:0,y:1}],
      'rgba(255,255,255,0.12)',
      'rgba(80,95,103,0.72)',
      [5,4],
      1.35
    );
    const [unitX,unitY] = toScreen({x:1,y:1});
    pill('1 × 1',unitX+21,unitY-14,'#687780');

    ctx.fillStyle='#26343c';
    ctx.beginPath();
    ctx.arc(ox,oy,3,0,Math.PI*2);
    ctx.fill();

    arrow({x:a,y:0},'#0f766e','a₁');
    arrow({x:0,y:d},'#7153b8','a₂');
    [[a,0,'a'],[0,d,'d']].forEach(([x,y,key])=>{
      const tip=toScreen({x,y});
      const hint=moveHints[key];
      const hintX=key==='a'?tip[0]+14:tip[0]-hint.offsetWidth-14;
      const hintY=key==='a'?tip[1]-34:tip[1]+14;
      hint.style.left=Math.max(6,Math.min(W-hint.offsetWidth-6,hintX))+'px';
      hint.style.top=canvas.offsetTop+Math.max(6,Math.min(H-hint.offsetHeight-6,hintY))+'px';
    });

    const [edgeX,edgeY]=toScreen({x:a,y:d});
    const widthY=oy+(d>=0?22:-22);
    const heightX=ox+(a>=0?-22:22);
    ctx.save();
    ctx.lineWidth=1;
    ctx.strokeStyle='#93aaa9';
    ctx.beginPath();
    ctx.moveTo(ox,widthY); ctx.lineTo(edgeX,widthY);
    [ox,edgeX].forEach(x=>{ctx.moveTo(x,widthY-3);ctx.lineTo(x,widthY+3);});
    ctx.moveTo(heightX,oy); ctx.lineTo(heightX,edgeY);
    [oy,edgeY].forEach(y=>{ctx.moveTo(heightX-3,y);ctx.lineTo(heightX+3,y);});
    ctx.stroke();
    ctx.font='400 11px "Helvetica Neue", Helvetica, Arial, sans-serif';
    ctx.fillStyle='#147f7a';
    ctx.textAlign='center';
    ctx.fillText('|a₁₁|',(ox+edgeX)/2,widthY+(d>=0?15:-7));
    ctx.translate(heightX+(a>=0?-9:15),(oy+edgeY)/2);
    ctx.rotate(-Math.PI/2);
    ctx.fillStyle='#7153b8';
    ctx.fillText('|a₂₂|',0,0);
    ctx.restore();

    const area = Math.abs(det);
    areaEl.textContent=fmt(area);
    widthEl.textContent='|a₁₁| = '+fmt(Math.abs(a));
    heightEl.textContent='|a₂₂| = '+fmt(Math.abs(d));
    detEl.className=positive?'pos':'neg';
    detEl.textContent=fmt(det);
    orientationEl.textContent=Math.abs(det)<0.02
      ? 'collapsed'
      : (det<0?'orientation flipped':'orientation preserved');
    canvas.setAttribute(
      'aria-label',
      'Interactive diagonal matrix. a 11 is '+fmt(a)+', a 22 is '+fmt(d)+
      ', determinant is '+fmt(det)+'. Use left and right arrows to change a 11, and up and down arrows to change a 22.'
    );
    captionEl.textContent=Math.abs(det)<0.02
      ? 'One side has collapsed to zero, so the rectangle has no area and the matrix loses a direction.'
      : (det<0
        ? 'The rectangle still has area |a₁₁a₂₂|, while the negative sign records the flipped orientation.'
        : 'The axis-aligned sides make the rule visible immediately: width |a₁₁| × height |a₂₂| = area |a₁₁a₂₂|.');
  }

  function position(event){
    const rect = canvas.getBoundingClientRect();
    return {
      x:((event.clientX-rect.left)*(W/rect.width)-ox)/scale,
      y:(oy-(event.clientY-rect.top)*(H/rect.height))/scale
    };
  }

  canvas.addEventListener('pointerdown',(event)=>{
    if(dragging) return;
    const point = position(event);
    const distanceA = Math.hypot(point.x-a,point.y);
    const distanceD = Math.hypot(point.x,point.y-d);
    if(distanceA<0.45 && distanceA<distanceD) dragging='a';
    else if(distanceD<0.45) dragging='d';
    if(!dragging) return;
    dragPointerId=event.pointerId;
    canvas.setPointerCapture(event.pointerId);
    canvas.classList.add('is-dragging');
    event.preventDefault();
  });
  canvas.addEventListener('pointermove',(event)=>{
    const point = position(event);
    if(!dragging){
      const nearA=Math.hypot(point.x-a,point.y)<0.45;
      const nearD=Math.hypot(point.x,point.y-d)<0.45;
      canvas.classList.toggle('is-hovering',nearA||nearD);
      return;
    }
    if(event.pointerId!==dragPointerId) return;
    if(dragging==='a'){
      const next=Math.max(limits.a[0],Math.min(limits.a[1],point.x));
      if(Math.abs(next-a)>1e-9) moveHints.a.classList.add('is-dismissed');
      a=next;
    }else{
      const next=Math.max(limits.d[0],Math.min(limits.d[1],point.y));
      if(Math.abs(next-d)>1e-9) moveHints.d.classList.add('is-dismissed');
      d=next;
    }
    draw();
    event.preventDefault();
  });
  function stopDragging(event){
    if(event.pointerId!==dragPointerId) return;
    dragging=null;
    dragPointerId=null;
    canvas.classList.remove('is-dragging');
  }
  canvas.addEventListener('pointerup',stopDragging);
  canvas.addEventListener('pointercancel',stopDragging);
  canvas.addEventListener('lostpointercapture',stopDragging);
  canvas.addEventListener('pointerleave',()=>canvas.classList.remove('is-hovering'));
  canvas.addEventListener('keydown',(event)=>{
    const step=event.shiftKey?0.5:0.1;
    const previous={a,d};
    if(event.key==='ArrowLeft') a=Math.max(limits.a[0],a-step);
    else if(event.key==='ArrowRight') a=Math.min(limits.a[1],a+step);
    else if(event.key==='ArrowDown') d=Math.max(limits.d[0],d-step);
    else if(event.key==='ArrowUp') d=Math.min(limits.d[1],d+step);
    else if(event.key==='Home'){
      a=initial.a;
      d=initial.d;
    }else return;
    if(Math.abs(a-previous.a)>1e-9) moveHints.a.classList.add('is-dismissed');
    if(Math.abs(d-previous.d)>1e-9) moveHints.d.classList.add('is-dismissed');
    if(event.key==='Home') Object.values(moveHints).forEach(hint=>hint.classList.remove('is-dismissed'));
    draw();
    event.preventDefault();
  });
  resetBtn.addEventListener('click',()=>{
    a=initial.a;
    d=initial.d;
    Object.values(moveHints).forEach(hint=>hint.classList.remove('is-dismissed'));
    draw();
  });

  resizeCanvas();
  draw();
  window.addEventListener('resize',()=>{if(resizeCanvas()) draw();});
  if(window.ResizeObserver){
    new ResizeObserver(()=>{if(resizeCanvas()) draw();}).observe(canvas);
  }
  if(document.fonts) document.fonts.ready.then(()=>{resizeCanvas();draw();});
})();
</script>

<p>Now we let the columns point in arbitrary directions:</p>

\[\begin{align*}
\mathbf{A}
&amp;=
\begin{bmatrix}
a_{11} &amp; a_{12} \\
a_{21} &amp; a_{22}
\end{bmatrix}
=
\begin{bmatrix}
\begin{bmatrix} a_{11} \\ a_{21} \end{bmatrix}
&amp;
\begin{bmatrix} a_{12} \\ a_{22} \end{bmatrix}
\end{bmatrix}.
\end{align*}\]

<p>The transformed unit square is still spanned by the two columns, but its right angles can now lean into a general parallelogram.
Its signed area is</p>

\[\det(\mathbf{A})=a_{11}a_{22}-a_{12}a_{21}.\]

<p>This is a deceptively simple equation which you will read in most text books. In order to understand how this simple subtractions comes to be, play around with the widget below which shows how we subtract the triangles from the parallelogram to obtain the term above. The determinant can be calculated by computing the outer bounding box and subtracting all triangles between the outer most bounding box and the actual parallelogram.</p>

<style>
  .determinant-widget{
    --bg: #ffffff;
    --bg2: #ffffff;
    --panel: #ffffff;
    --panel-line: #d9dee5;
    --ink: #212931;
    --ink-dim: #626d78;
    --amber: #b66a00;
    --teal: #147f7a;
    --teal-dim: #8ebfbc;
    --rose: #c43b54;
    --paper: #212931;
    --serif-body:"Helvetica Neue",Helvetica,Arial,sans-serif;
    --mono:ui-monospace,SFMono-Regular,Consolas,monospace;
    margin:26px 0;
    color:var(--ink);
    font-family:var(--serif-body);
    font-size:18px;
    font-weight:400;
    line-height:1.65;
    text-align:left;
    -webkit-font-smoothing:antialiased;
    background:var(--panel);
    border:1px solid var(--panel-line);
    border-radius:6px;
    overflow:hidden;
  }
  .determinant-widget,
  .determinant-widget *{box-sizing:border-box;}
  .determinant-widget .widget-titlewrap{
    display:flex;
    flex-wrap:wrap;
    justify-content:space-between;
    align-items:center;
    gap:8px;
    padding:14px 18px;
    border-bottom:1px solid var(--panel-line);
    background:var(--bg2);
  }
  .determinant-widget .widget-title{font-family:var(--mono); font-size:13.5px; color:var(--paper); font-weight:500;}
  .determinant-widget .wt-sub{font-family:var(--mono); font-size:11px; color:var(--ink-dim); text-transform:uppercase; letter-spacing:0.06em;}
  .determinant-widget .widget-body{padding:18px;}
  .determinant-widget canvas{
    display:block;
    width:100%;
    height:auto;
    aspect-ratio:680 / 420;
    background:var(--bg2);
    border-radius:3px;
    border:1px solid var(--panel-line);
    cursor:grab;
    touch-action:none;
  }
  .determinant-widget canvas.is-dragging{cursor:grabbing;}
  .determinant-widget .btnrow{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;}
  .determinant-widget button.step-btn{
    font-family:var(--mono);
    font-size:12.5px;
    font-weight:400;
    height:auto;
    line-height:normal;
    letter-spacing:0.03em;
    text-transform:none;
    white-space:normal;
    box-shadow:none;
    background:transparent;
    border:1px solid var(--teal-dim);
    /* The site's button colors also use !important. */
    color:var(--teal) !important;
    padding:8px 14px;
    border-radius:4px;
    cursor:pointer;
    transition:background .15s, color .15s;
  }
  .determinant-widget button.step-btn:hover{background:var(--teal); color:var(--bg2) !important;}
  .determinant-widget button.step-btn:disabled{opacity:0.35; cursor:default;}
  .determinant-widget .caption{
    font-family:var(--mono);
    font-size:12.5px;
    color:var(--amber);
    margin-top:12px;
    min-height:18px;
  }
  .determinant-widget .readout{
    font-family:var(--mono);
    font-size:14px;
    color:var(--paper);
    margin-top:10px;
    background:var(--bg2);
    border:1px solid var(--panel-line);
    border-radius:4px;
    padding:10px 14px;
  }
  .determinant-widget .readout .neg{color:var(--rose);}
  .determinant-widget .readout .pos{color:var(--teal);}
  .determinant-widget .w2-equation{
    margin-top:14px;
    padding:20px 18px;
    background:var(--bg2);
    border:1px solid var(--panel-line);
    border-radius:4px;
  }
  .determinant-widget .w2-equation p{margin:0 0 16px; text-align:left;}
  .determinant-widget .w2-equation .eq-label{
    margin-bottom:16px;
    font-family:var(--mono);
    font-size:11px;
    letter-spacing:0.1em;
    text-transform:uppercase;
    color:var(--teal);
  }
  .determinant-widget .w2-equation .eq-step{
    display:none;
    margin:0 0 16px;
    color:var(--ink);
  }
  .determinant-widget .w2-equation .eq-step.is-revealed{display:block;}
  .determinant-widget .w2-equation .eq-note{
    color:var(--ink-dim);
    font-size:0.92em;
    line-height:1.45;
  }
  .determinant-widget .w2-equation .eq-step > p:not(.eq-note){
    max-width:100%;
    overflow-x:auto;
    overflow-y:hidden;
  }
  .determinant-widget ::selection{background:var(--amber); color:var(--bg2);}
  .determinant-widget mjx-container{max-width:100%;}
  .determinant-widget mjx-container[display="true"]{
    overflow-x:auto;
    overflow-y:hidden;
    padding:2px 0 6px;
  }
</style>

<style>
  #determinant-area-widget{
    --w2-teal:#0f766e;
    --w2-teal-soft:#e8f5f2;
    --w2-gold:#a76108;
    --w2-gold-soft:#fff5df;
    --w2-rose:#b8324b;
    border-color:#dce5e3;
    border-radius:12px;
    box-shadow:0 14px 34px rgba(33,41,49,0.07);
  }
  #determinant-area-widget .widget-titlewrap{
    display:grid;
    grid-template-columns:minmax(0,1fr) auto;
    align-items:start;
    padding:18px 20px 17px;
    background:linear-gradient(135deg,#ffffff 0%,#f6faf9 100%);
    border-bottom-color:#e3ebe9;
  }
  #determinant-area-widget .w2-heading{display:grid; min-width:0; gap:4px;}
  #determinant-area-widget .w2-kicker{
    color:var(--w2-teal);
    font:600 10px/1.2 var(--mono);
    letter-spacing:0.11em;
    text-transform:uppercase;
  }
  #determinant-area-widget .widget-title{
    display:block;
    color:#172129;
    font-size:15px;
    font-weight:600;
    line-height:1.45;
    overflow-wrap:anywhere;
  }
  #determinant-area-widget .wt-sub{
    margin-top:1px;
    padding:5px 9px;
    color:#52616a;
    background:rgba(255,255,255,0.82);
    border:1px solid #dbe5e3;
    border-radius:999px;
    letter-spacing:0.04em;
    text-transform:none;
  }
  #determinant-area-widget .widget-body{padding:20px;}
  #determinant-area-widget .w2-plot-shell{
    overflow:hidden;
    background:#fbfdfc;
    border:1px solid #dce5e3;
    border-radius:9px;
  }
  #determinant-area-widget .w2-plot-key{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    gap:7px 14px;
    min-height:40px;
    padding:9px 12px;
    color:#5a6871;
    background:#f8fbfa;
    border-bottom:1px solid #e2eae8;
    font:500 10.5px/1.35 var(--mono);
  }
  #determinant-area-widget .w2-key-item{
    display:inline-flex;
    align-items:center;
    gap:6px;
    white-space:nowrap;
  }
  #determinant-area-widget .w2-key-item:last-child{margin-left:auto;}
  #determinant-area-widget .w2-swatch{
    width:16px;
    height:3px;
    border-radius:2px;
    background:var(--w2-teal);
  }
  #determinant-area-widget .w2-swatch.gold{background:var(--w2-gold);}
  #determinant-area-widget .w2-handle-icon{
    width:11px;
    height:11px;
    border:3px solid #fff;
    border-radius:50%;
    background:var(--w2-gold);
    box-shadow:0 0 0 1px var(--w2-gold);
  }
  #determinant-area-widget canvas{
    height:clamp(270px,61.75vw,420px);
    aspect-ratio:auto;
    background:#fbfdfc;
    border:0;
    border-radius:0;
  }
  #determinant-area-widget canvas:focus-visible{
    outline:3px solid rgba(15,118,110,0.28);
    outline-offset:-3px;
  }
  #determinant-area-widget .w2-summary{
    display:grid;
    grid-template-columns:minmax(0,1fr) auto;
    align-items:center;
    gap:12px;
    margin-top:12px;
    padding:12px 14px;
    background:#f6faf9;
    border:1px solid #cddfdb;
    border-radius:8px;
  }
  #determinant-area-widget .w2-summary-label{
    display:block;
    margin-bottom:3px;
    color:#69757d;
    font:600 9.5px/1.3 var(--mono);
    letter-spacing:0.09em;
    text-transform:uppercase;
  }
  #determinant-area-widget .w2-summary-detail{
    display:block;
    color:#69757d;
    font:400 10.5px/1.45 var(--mono);
  }
  #determinant-area-widget .readout{
    min-width:112px;
    margin:0;
    padding:0;
    color:var(--w2-teal);
    background:transparent;
    border:0;
    font:600 22px/1.2 var(--mono);
    text-align:right;
    font-variant-numeric:tabular-nums;
  }
  #determinant-area-widget .readout .neg{color:var(--w2-rose);}
  #determinant-area-widget .w2-actions{
    display:grid;
    grid-template-columns:minmax(0,1fr) auto;
    align-items:center;
    gap:14px;
    margin-top:12px;
    padding:11px 12px;
    background:#fffaf0;
    border:1px solid #f0dfbd;
    border-radius:8px;
  }
  #determinant-area-widget .w2-stage-copy{display:grid; gap:3px;}
  #determinant-area-widget .w2-stage-label{
    color:#85520b;
    font:600 9.5px/1.3 var(--mono);
    letter-spacing:0.09em;
    text-transform:uppercase;
  }
  #determinant-area-widget .caption{
    margin:0;
    min-height:0;
    color:#42515a;
    font-family:var(--serif-body);
    font-size:15px;
    line-height:1.45;
  }
  #determinant-area-widget .btnrow{
    display:grid;
    grid-template-columns:auto auto;
    gap:7px;
    margin:0;
  }
  #determinant-area-widget button.step-btn{
    padding:7px 10px;
    background:#fff;
    border-color:#adcac5;
    border-radius:6px;
    font-size:11px;
    font-weight:500;
  }
  #determinant-area-widget button.step-btn:hover{
    background:var(--w2-teal);
    color:#fff !important;
  }
  #determinant-area-widget button.step-btn:focus-visible{
    outline:3px solid rgba(15,118,110,0.22);
    outline-offset:2px;
  }
  #determinant-area-widget #w2next{grid-column:1 / -1;}
  #determinant-area-widget .w2-equation{
    margin-top:12px;
    padding:17px 18px;
    background:#fff;
    border-color:#dce5e3;
    border-radius:8px;
    font-size:16px;
  }
  #determinant-area-widget .w2-equation .eq-label{
    margin-bottom:12px;
    color:var(--w2-teal);
    font-size:9.5px;
  }
  #determinant-area-widget .w2-equation .eq-step{
    margin-bottom:14px;
    padding-bottom:14px;
    border-bottom:1px solid #edf1f0;
  }
  #determinant-area-widget .w2-equation .eq-step:last-child{
    margin-bottom:0;
    padding-bottom:0;
    border-bottom:0;
  }
  #determinant-area-widget .w2-equation .eq-note{
    margin-bottom:7px;
    color:#5f6c74;
    font-size:14px;
  }
  #determinant-area-widget .w2-vector-pair{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:8px 18px;
  }
  #determinant-area-widget .eq-math-stack{
    display:grid;
    gap:2px;
    color:#172129;
  }
  #determinant-area-widget .eq-math-stack > span{
    min-width:0;
    overflow-x:auto;
    overflow-y:hidden;
  }
  #determinant-area-widget .eq-math-stack mjx-container{margin:0.15em 0 !important;}
  @media (max-width:520px){
    #determinant-area-widget .widget-titlewrap,
    #determinant-area-widget .widget-body{padding:14px;}
    #determinant-area-widget .widget-titlewrap{
      grid-template-columns:1fr;
      gap:9px;
      padding-right:58px;
    }
    #determinant-area-widget .widget-title{font-size:13px;}
    #determinant-area-widget .wt-sub{justify-self:start; font-size:9.5px;}
    #determinant-area-widget .w2-plot-key{gap:6px 10px; padding:8px 9px;}
    #determinant-area-widget .w2-key-item:last-child{width:100%; margin-left:0;}
    #determinant-area-widget .w2-summary{grid-template-columns:1fr; gap:6px;}
    #determinant-area-widget .readout{min-width:0; text-align:left;}
    #determinant-area-widget .w2-actions{grid-template-columns:1fr; align-items:start;}
    #determinant-area-widget .btnrow{grid-template-columns:1fr 1fr; width:100%;}
    #determinant-area-widget button.step-btn{width:100%;}
    #determinant-area-widget .w2-equation{padding:14px 12px; font-size:14px;}
    #determinant-area-widget .w2-equation .eq-note{font-size:13px;}
    #determinant-area-widget .w2-vector-pair{grid-template-columns:1fr;}
  }
</style>

<div id="determinant-area-widget" class="widget determinant-widget matrix-figure">
  <div class="widget-titlewrap">
    <div class="w2-heading">
      <span class="w2-kicker">From rectangle to parallelogram</span>
      <span class="widget-title">Deriving \(a_{11}a_{22}-a_{12}a_{21}\) by cutting up a box</span>
    </div>
    <span class="wt-sub">staged · drag the arrow tips</span>
  </div>
  <div class="widget-body">
    <div class="w2-plot-shell">
      <div class="w2-plot-key" aria-hidden="true">
        <span class="w2-key-item"><i class="w2-swatch"></i>column \(a_1\)</span>
        <span class="w2-key-item"><i class="w2-swatch gold"></i>column \(a_2\)</span>
        <span class="w2-key-item"><i class="w2-handle-icon"></i>drag either tip</span>
      </div>
      <canvas id="w2canvas" width="680" height="420" tabindex="0" role="application" aria-describedby="w2help w2caption" aria-label="Interactive determinant cut-up. Drag either column-vector tip."></canvas>
      <span class="figure-canvas-hint move-hint" data-w2-hint="u" aria-hidden="true">Drag &amp; Move Me!</span>
      <span class="figure-canvas-hint move-hint second" data-w2-hint="v" aria-hidden="true">Drag &amp; Move Me!</span>
    </div>
    <div class="w2-summary" aria-live="polite">
      <div>
        <span class="w2-summary-label">Signed area · determinant</span>
        <span class="w2-summary-detail">\(\det(\mathbf A)=a_{11}a_{22}-a_{12}a_{21}\) · <span id="w2orientation">orientation preserved</span></span>
      </div>
      <div class="readout" id="w2readout"><span id="w2area">—</span></div>
    </div>
    <div class="w2-actions">
      <div class="w2-stage-copy">
        <span class="w2-stage-label" id="w2stage">Stage 1 of 4</span>
        <div class="caption" id="w2caption">The two column vectors are drawn as arrows from the origin.</div>
      </div>
      <div class="btnrow">
        <button class="step-btn" id="w2next">Show the bounding box →</button>
        <button class="step-btn" id="w2back">← Back</button>
        <button class="step-btn" id="w2reset">Reset</button>
      </div>
    </div>
    <aside class="w2-equation" aria-live="polite">
      <div class="eq-label">The calculation</div>
      <div class="eq-step is-revealed" data-w2-stage="0">
        <p class="eq-note">The two arrows are the parallelogram's edges:</p>
        <div class="w2-vector-pair">
          <span>\(\mathbf a_1=(a_{11},a_{21})\)</span>
          <span>\(\mathbf a_2=(a_{12},a_{22})\)</span>
        </div>
      </div>
      <div class="eq-step" data-w2-stage="1">
        <p class="eq-note">Start from the full bounding box:</p>
        <div class="eq-math-stack">
          <span>\(\text{box area}\)</span>
          <span>\(=(a_{11}+a_{12})(a_{21}+a_{22})\)</span>
        </div>
      </div>
      <div class="eq-step" data-w2-stage="2">
        <p class="eq-note">Subtract the colored pieces shown above:</p>
        <div class="eq-math-stack">
          <span>\(\text{area}=(a_{11}+a_{12})(a_{21}+a_{22})\)</span>
          <span data-w2-piece="first-triangles">\(\quad-\color{#c43b54}{\left(\tfrac12a_{11}a_{21}+\tfrac12a_{11}a_{21}\right)}\)</span>
          <span data-w2-piece="second-triangles">\(\quad-\color{#147f7a}{\left(\tfrac12a_{12}a_{22}+\tfrac12a_{12}a_{22}\right)}\)</span>
          <span data-w2-piece="rectangles">\(\quad-\color{#b66a00}{a_{12}a_{21}}-\color{#7153b8}{a_{12}a_{21}}.\)</span>
        </div>
      </div>
      <div class="eq-step" data-w2-stage="3">
        <p class="eq-note">Expand the box, then cancel matching terms:</p>
        <div class="eq-math-stack">
          <span>\(\text{area}=(a_{11}+a_{12})(a_{21}+a_{22})\)</span>
          <span data-w2-piece="triangles">\(\quad-\color{#c43b54}{a_{11}a_{21}}-\color{#147f7a}{a_{12}a_{22}}\)</span>
          <span data-w2-piece="rectangles">\(\quad-\color{#b66a00}{a_{12}a_{21}}-\color{#7153b8}{a_{12}a_{21}}\)</span>
          <span>\(=a_{11}a_{21}+a_{11}a_{22}\)</span>
          <span>\(\quad+a_{12}a_{21}+a_{12}a_{22}\)</span>
          <span data-w2-piece="triangles">\(\quad-\color{#c43b54}{a_{11}a_{21}}-\color{#147f7a}{a_{12}a_{22}}\)</span>
          <span data-w2-piece="rectangles">\(\quad-\color{#b66a00}{a_{12}a_{21}}-\color{#7153b8}{a_{12}a_{21}}\)</span>
          <span>\(\boxed{\text{area}=a_{11}a_{22}-a_{12}a_{21}}\)</span>
        </div>
      </div>
    </aside>
    <p id="w2help" hidden="">Drag either column-vector tip. With the plot focused, press 1 or 2 to select a column, then use the arrow keys to move it. Hold Shift for larger steps. Use the stage controls to reveal the bounding-box cut-up, and hover over a colored piece to highlight its algebraic term.</p>
  </div>
</div>
<script>
(function(){
  const root = document.getElementById('determinant-area-widget');
  function fmt(x,n){ n=n===undefined?2:n; return (Math.abs(x)<1e-9?0:x).toFixed(n); }

  const canvas = root.querySelector('#w2canvas');
  const ctx = canvas.getContext('2d');
  const BASE_W = 680, BASE_H = 420;
  let W = BASE_W, H = BASE_H;
  let ox = 70, oy = H-70, scale = 55;
  const gridBounds = {left:20, right:BASE_W-20, top:20, bottom:BASE_H-20};
  const gridCoordinates = {
    minX:(gridBounds.left-ox)/scale,
    maxX:(gridBounds.right-ox)/scale,
    minY:(oy-gridBounds.bottom)/scale,
    maxY:(oy-gridBounds.top)/scale
  };
  const dragBounds = {
    minX:-0.5,
    maxX:0.8*gridCoordinates.maxX,
    minY:-0.5,
    maxY:0.8*gridCoordinates.maxY
  };
  let u = {x:2.2, y:0.6};   // (a₁₁,a₂₁)
  let v = {x:0.8, y:2.0};   // (a₁₂,a₂₂)
  const moveHints={u:root.querySelector('[data-w2-hint="u"]'),v:root.querySelector('[data-w2-hint="v"]')};
  const reducedMotion=window.matchMedia('(prefers-reduced-motion: reduce)');
  let selectedColumn='u';
  let stageProgress=1;
  let stageAnimation=null;
  let hoveredPiece=null;
  let pieces=[];
  let stage = 0; // 0..3
  const stages = [
    'The two column vectors are drawn as arrows from the origin.',
    'Enclose the vectors in their axis-aligned bounding box.',
    'Subtract the four corner triangles and the two leftover rectangles.',
    'What remains is the parallelogram: area = a₁₁a₂₂ − a₁₂a₂₁.'
  ];
  const capEl = root.querySelector('#w2caption');
  const stageEl = root.querySelector('#w2stage');
  const areaEl = root.querySelector('#w2area');
  const orientationEl = root.querySelector('#w2orientation');
  const equationSteps = root.querySelectorAll('[data-w2-stage]');
  const nextBtn = root.querySelector('#w2next'), backBtn = root.querySelector('#w2back'), resetBtn = root.querySelector('#w2reset');
  const nextLabels = ['Show the bounding box →', 'Subtract the outside pieces →', 'Simplify to a₁₁a₂₂ − a₁₂a₂₁ →'];

  function resizeCanvas(){
    const pixelRatio = Math.max(1, window.devicePixelRatio || 1);
    const cssWidth = Math.max(1, Math.round(canvas.clientWidth));
    const cssHeight = Math.max(1, Math.round(canvas.clientHeight));
    const width = Math.round(cssWidth * pixelRatio);
    const height = Math.round(cssHeight * pixelRatio);
    if(canvas.width===width && canvas.height===height) return false;
    canvas.width = width;
    canvas.height = height;
    W = cssWidth;
    H = cssHeight;
    const layoutScale = Math.min(1, W/BASE_W);
    scale = 55*layoutScale;
    ox = (W-BASE_W*layoutScale)/2 + 70*layoutScale;
    oy = H-Math.max(30,70*layoutScale);
    ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
    return true;
  }

  function toScreen(p){ return [ox + p.x*scale, oy - p.y*scale]; }

  let labelBoxes = [];
  let pendingAxisLabels = [];

  function roundedRect(x,y,width,height,radius){
    const r=Math.min(radius,width/2,height/2);
    ctx.beginPath();
    ctx.moveTo(x+r,y);
    ctx.arcTo(x+width,y,x+width,y+height,r);
    ctx.arcTo(x+width,y+height,x,y+height,r);
    ctx.arcTo(x,y+height,x,y,r);
    ctx.arcTo(x,y,x+width,y,r);
    ctx.closePath();
  }

  function overlapArea(a,b){
    const width=Math.max(0,Math.min(a.right,b.right)-Math.max(a.left,b.left));
    const height=Math.max(0,Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top));
    return width*height;
  }

  function placeBox(anchor,width,height,candidates){
    let best=null;
    candidates.forEach((offset,index)=>{
      const left=Math.max(7,Math.min(W-width-7,anchor[0]+offset[0]));
      const top=Math.max(7,Math.min(H-height-7,anchor[1]+offset[1]));
      const box={left,top,right:left+width,bottom:top+height};
      const overlap=labelBoxes.reduce((sum,other)=>sum+overlapArea(box,other),0);
      const score=overlap*1000+index;
      if(!best || score<best.score) best={box,score};
    });
    labelBoxes.push(best.box);
    return best.box;
  }

  function drawPill(anchor,text,color,options){
    const opts=options||{};
    ctx.save();
    ctx.font=opts.font||'500 12px "Helvetica Neue", Helvetica, Arial, sans-serif';
    const width=Math.ceil(ctx.measureText(text).width)+(opts.paddingX||14);
    const height=opts.height||25;
    const box=placeBox(anchor,width,height,opts.candidates);
    if(opts.connector){
      const targetX=Math.max(box.left,Math.min(box.right,anchor[0]));
      const targetY=Math.max(box.top,Math.min(box.bottom,anchor[1]));
      ctx.strokeStyle=color;
      ctx.globalAlpha=0.38;
      ctx.lineWidth=1;
      ctx.beginPath();
      ctx.moveTo(anchor[0],anchor[1]);
      ctx.lineTo(targetX,targetY);
      ctx.stroke();
      ctx.globalAlpha=1;
    }
    ctx.fillStyle=opts.fill||'rgba(255,255,255,0.94)';
    roundedRect(box.left,box.top,width,height,opts.radius||5);
    ctx.fill();
    if(opts.border!==false){
      ctx.strokeStyle=color;
      ctx.globalAlpha*=0.25;
      ctx.lineWidth=1;
      ctx.stroke();
      ctx.globalAlpha=1;
    }
    ctx.fillStyle=color;
    ctx.textBaseline='middle';
    ctx.textAlign='left';
    ctx.fillText(text,box.left+(opts.paddingX||14)/2,box.top+height/2+0.5);
    ctx.restore();
  }

  function drawArrow(p, color, label){
    const [sx,sy] = toScreen(p);
    const angle = Math.atan2(sy-oy, sx-ox);
    const leftOffset=label.length<=3?-35:-146;
    ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 2.5;
    ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(ox,oy); ctx.lineTo(sx,sy); ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(sx,sy);
    ctx.lineTo(sx-10*Math.cos(angle-0.3), sy-10*Math.sin(angle-0.3));
    ctx.lineTo(sx-10*Math.cos(angle+0.3), sy-10*Math.sin(angle+0.3));
    ctx.closePath(); ctx.fill();
    const rightward=Math.cos(angle)>=0;
    const upward=Math.sin(angle)<=0;
    drawPill([sx,sy],label,color,{
      connector:true,
      candidates:[
        [rightward?12:leftOffset,upward?-34:10],
        [rightward?leftOffset:12,upward?-34:10],
        [rightward?12:leftOffset,upward?10:-34],
        [leftOffset,-50],
        [leftOffset,18]
      ]
    });
  }

  function poly(pts, fill, stroke, piece){
    if(piece) pieces.push({points:pts,key:piece});
    ctx.beginPath();
    pts.forEach((p,i)=>{ const [sx,sy]=toScreen(p); i===0?ctx.moveTo(sx,sy):ctx.lineTo(sx,sy); });
    ctx.closePath();
    if(fill){ ctx.fillStyle=fill; ctx.fill(); }
    if(stroke){ ctx.strokeStyle=stroke; ctx.lineWidth=piece===hoveredPiece?2.8:1.5; ctx.stroke(); }
  }

  function axisMarker(value, axis, label, color){
    const p = axis==='x' ? toScreen({x:value,y:0}) : toScreen({x:0,y:value});
    ctx.strokeStyle=color; ctx.fillStyle=color; ctx.lineWidth=2;
    if(axis==='x'){
      ctx.beginPath(); ctx.moveTo(p[0],p[1]-5); ctx.lineTo(p[0],p[1]+5); ctx.stroke();
    } else {
      ctx.beginPath(); ctx.moveTo(p[0]-5,p[1]); ctx.lineTo(p[0]+5,p[1]); ctx.stroke();
    }
    pendingAxisLabels.push({point:p,axis,label,color});
  }

  function drawAxisLabel(item){
    const xCandidates=item.axis==='x'
      ? [[-18,8],[-18,-28],[6,8],[-42,8]]
      : [[-43,-10],[8,-10],[-43,8],[8,8]];
    drawPill(item.point,item.label,item.color,{
      font:'500 11px "Helvetica Neue", Helvetica, Arial, sans-serif',
      height:20,
      paddingX:10,
      radius:3,
      border:false,
      candidates:xCandidates
    });
  }

  function draw(){
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle='#fbfdfc';
    ctx.fillRect(0,0,W,H);
    labelBoxes=[];
    pendingAxisLabels=[];
    pieces=[];
    ctx.strokeStyle='rgba(33,41,49,0.12)'; ctx.lineWidth=1;
    for(let i=-1;i<=10;i++){
      const [sx]=toScreen({x:i,y:0});
      if(sx<12 || sx>W-12) continue;
      ctx.beginPath(); ctx.moveTo(sx,12); ctx.lineTo(sx,H-12); ctx.stroke();
    }
    for(let j=-1;j<=6;j++){
      const [,sy]=toScreen({x:0,y:j});
      if(sy<12 || sy>H-12) continue;
      ctx.beginPath(); ctx.moveTo(12,sy); ctx.lineTo(W-12,sy); ctx.stroke();
    }

    const a=u.x, c=u.y, b=v.x, d=v.y;
    const area = a*d-b*c;
    const orientationColor = area>=0 ? '#b66a00' : '#c43b54';
    axisMarker(a, 'x', 'a₁₁', '#147f7a');
    axisMarker(c, 'y', 'a₂₁', '#147f7a');
    axisMarker(b, 'x', 'a₁₂', '#7153b8');
    axisMarker(d, 'y', 'a₂₂', '#7153b8');

    if(stage>=1){
      // bounding box
      ctx.save();
      ctx.globalAlpha=stage===1?stageProgress:1;
      const minX = Math.min(0,a,b,a+b), maxX = Math.max(0,a,b,a+b);
      const minY = Math.min(0,c,d,c+d), maxY = Math.max(0,c,d,c+d);
      poly([{x:minX,y:minY},{x:maxX,y:minY},{x:maxX,y:maxY},{x:minX,y:maxY}], 'rgba(33,41,49,0.025)', 'rgba(33,41,49,0.35)');
      ctx.restore();
    }
    if(stage>=2){
      ctx.save();
      ctx.globalAlpha=stage===2?stageProgress:1;
      // Two ac/2 triangles
      poly([{x:0,y:0},{x:a,y:0},{x:a,y:c}], 'rgba(196,59,84,0.16)', '#c43b54','first-triangles');
      poly([{x:b,y:d},{x:b,y:c+d},{x:a+b,y:c+d}], 'rgba(196,59,84,0.16)', '#c43b54','first-triangles');
      // Two bd/2 triangles
      poly([{x:a,y:c},{x:a+b,y:c},{x:a+b,y:c+d}], 'rgba(20,127,122,0.14)', '#147f7a','second-triangles');
      poly([{x:0,y:0},{x:0,y:d},{x:b,y:d}], 'rgba(20,127,122,0.14)', '#147f7a','second-triangles');
      // Two bc corner rectangles
      poly([{x:a,y:0},{x:a+b,y:0},{x:a+b,y:c},{x:a,y:c}], 'rgba(182,106,0,0.12)', '#b66a00','rectangles');
      poly([{x:0,y:d},{x:b,y:d},{x:b,y:c+d},{x:0,y:c+d}], 'rgba(113,83,184,0.12)', '#7153b8','rectangles');
      ctx.restore();
    }
    // parallelogram outline always on top (dashed until final stage, solid at stage 3)
    ctx.save();
    ctx.globalAlpha=stage===3?stageProgress:1;
    ctx.setLineDash(stage>=3?[]:[5,4]);
    poly(
      [{x:0,y:0},{x:a,y:c},{x:a+b,y:c+d},{x:b,y:d}],
      stage>=3 ? (area>=0?'rgba(182,106,0,0.18)':'rgba(196,59,84,0.18)') : (area<0?'rgba(196,59,84,0.09)':'transparent'),
      orientationColor
    );
    ctx.setLineDash([]);
    ctx.restore();

    drawArrow(u, '#147f7a', 'a₁');
    drawArrow(v, '#7153b8', 'a₂');

    // draggable handles
    [u,v].forEach((p,i)=>{
      const [sx,sy]=toScreen(p);
      ctx.beginPath(); ctx.arc(sx,sy,7,0,Math.PI*2);
      ctx.fillStyle = i===0?'#147f7a':'#7153b8'; ctx.fill();
      ctx.strokeStyle='#ffffff'; ctx.lineWidth=2; ctx.stroke();
      if(document.activeElement===canvas&&selectedColumn===(i===0?'u':'v')){
        ctx.beginPath(); ctx.arc(sx,sy,12,0,Math.PI*2);
        ctx.strokeStyle=i===0?'#147f7a':'#7153b8'; ctx.lineWidth=1; ctx.stroke();
      }
      const hint=moveHints[i===0?'u':'v'];
      if(!hint.classList.contains('is-dismissed')){
        const hintBox=placeBox([sx,sy],hint.offsetWidth,hint.offsetHeight,i===0
          ? [[12,12],[12,-34],[-hint.offsetWidth-12,12]]
          : [[12,-64],[-hint.offsetWidth-12,-34],[12,12]]);
        hint.style.left=hintBox.left+'px';
        hint.style.top=canvas.offsetTop+hintBox.top+'px';
      }
    });
    pendingAxisLabels.forEach(drawAxisLabel);

    const stageLabel='Stage '+(stage+1)+' of 4';
    if(stageEl.textContent!==stageLabel) stageEl.textContent=stageLabel;
    if(capEl.textContent!==stages[stage]) capEl.textContent=stages[stage];
    const sign=area>=0?'pos':'neg';
    if(areaEl.textContent!==fmt(area)||!areaEl.querySelector('.'+sign)){
      areaEl.innerHTML='<span class="'+sign+'">'+fmt(area)+'</span>';
    }
    const orientation=Math.abs(area)<0.02
      ? 'collapsed'
      : (area<0?'orientation flipped':'orientation preserved');
    if(orientationEl.textContent!==orientation) orientationEl.textContent=orientation;
    canvas.setAttribute(
      'aria-label',
      'Interactive determinant cut-up. a 11 is '+fmt(a)+', a 21 is '+fmt(c)+
      ', a 12 is '+fmt(b)+', a 22 is '+fmt(d)+', determinant is '+fmt(area)+
      '. Selected column '+(selectedColumn==='u'?'1':'2')+'. Press 1 or 2 to select a column, then use the arrow keys to move it.'
    );
    equationSteps.forEach(step=>{
      step.classList.toggle('is-revealed',Number(step.dataset.w2Stage)<=stage);
      step.classList.toggle('is-current',Number(step.dataset.w2Stage)===stage);
    });
    root.querySelectorAll('[data-w2-piece]').forEach(term=>{
      const key=term.dataset.w2Piece;
      term.classList.toggle('is-emphasized',key===hoveredPiece||(key==='triangles'&&hoveredPiece?.endsWith('triangles')));
    });
    backBtn.disabled = stage===0;
    nextBtn.disabled = stage===3;
    nextBtn.textContent = stage===3 ? 'Derivation complete' : nextLabels[stage];
  }

  function changeStage(next){
    if(stageAnimation!==null) cancelAnimationFrame(stageAnimation);
    stageAnimation=null;
    const forward=next>stage;
    stage=next;
    hoveredPiece=null;
    stageProgress=forward&&!reducedMotion.matches?0:1;
    draw();
    if(stageProgress===1) return;
    const started=performance.now();
    function animate(now){
      stageProgress=Math.min(1,(now-started)/240);
      draw();
      stageAnimation=stageProgress<1?requestAnimationFrame(animate):null;
    }
    stageAnimation=requestAnimationFrame(animate);
  }
  nextBtn.addEventListener('click',()=>{if(stage<3) changeStage(stage+1);});
  backBtn.addEventListener('click',()=>{if(stage>0) changeStage(stage-1);});
  resetBtn.addEventListener('click',()=>{
    changeStage(0);
  });

  // dragging
  let dragging = null, dragPointerId = null;
  function pos(evt){
    const r = canvas.getBoundingClientRect();
    const cxp = evt.clientX - r.left;
    const cyp = evt.clientY - r.top;
    return { x:(cxp*(W/r.width) - ox)/scale, y:(oy - cyp*(H/r.height))/scale };
  }
  function moveColumn(key,point){
    const current=key==='u'?u:v;
    const other=key==='u'?v:u;
    const minX=Math.max(dragBounds.minX,gridCoordinates.minX-other.x);
    const maxX=Math.min(dragBounds.maxX,gridCoordinates.maxX-other.x);
    const minY=Math.max(dragBounds.minY,gridCoordinates.minY-other.y);
    const maxY=Math.min(dragBounds.maxY,gridCoordinates.maxY-other.y);
    const next={
      x:Math.max(minX,Math.min(maxX,point.x)),
      y:Math.max(minY,Math.min(maxY,point.y))
    };
    if(Math.hypot(next.x-current.x,next.y-current.y)>1e-9){
      moveHints[key].classList.add('is-dismissed');
    }
    if(key==='u') u=next; else v=next;
    hoveredPiece=null;
    draw();
  }
  function contains(point,polygon){
    let inside=false;
    for(let i=0,j=polygon.length-1;i<polygon.length;j=i++){
      const a=polygon[i],b=polygon[j];
      if((a.y>point.y)!==(b.y>point.y)&&point.x<(b.x-a.x)*(point.y-a.y)/(b.y-a.y)+a.x) inside=!inside;
    }
    return inside;
  }
  canvas.addEventListener('pointerdown', (e)=>{
    if(dragging) return;
    const m = pos(e);
    const du = Math.hypot(m.x-u.x,m.y-u.y), dv = Math.hypot(m.x-v.x,m.y-v.y);
    if(du<0.4 && du<dv) dragging='u'; else if(dv<0.4) dragging='v';
    if(!dragging) return;
    dragPointerId = e.pointerId;
    selectedColumn=dragging;
    canvas.focus({preventScroll:true});
    canvas.setPointerCapture(e.pointerId);
    canvas.classList.add('is-dragging');
    e.preventDefault();
  });
  canvas.addEventListener('pointermove',(e)=>{
    const m = pos(e);
    if(!dragging){
      const piece=pieces.find(piece=>contains(m,piece.points))?.key||null;
      if(piece!==hoveredPiece){hoveredPiece=piece;draw();}
      return;
    }
    if(e.pointerId!==dragPointerId) return;
    moveColumn(dragging,m);
    e.preventDefault();
  });
  function stopDragging(e){
    if(e.pointerId!==dragPointerId) return;
    dragging = null;
    dragPointerId = null;
    canvas.classList.remove('is-dragging');
  }
  canvas.addEventListener('pointerup', stopDragging);
  canvas.addEventListener('pointercancel', stopDragging);
  canvas.addEventListener('lostpointercapture', stopDragging);
  canvas.addEventListener('pointerleave',()=>{hoveredPiece=null;draw();});
  canvas.addEventListener('focus',draw);
  canvas.addEventListener('blur',draw);
  canvas.addEventListener('keydown',event=>{
    if(event.key==='1'||event.key==='2'){
      selectedColumn=event.key==='1'?'u':'v';
      draw();
    }else{
      const step=event.shiftKey?0.5:0.1;
      const movement={ArrowLeft:[-step,0],ArrowRight:[step,0],ArrowUp:[0,step],ArrowDown:[0,-step]}[event.key];
      if(!movement) return;
      const current=selectedColumn==='u'?u:v;
      moveColumn(selectedColumn,{x:current.x+movement[0],y:current.y+movement[1]});
    }
    event.preventDefault();
  });

  resizeCanvas();
  draw();
  window.addEventListener('resize', ()=>{ if(resizeCanvas()) draw(); });
  if(window.ResizeObserver){
    new ResizeObserver(()=>{if(resizeCanvas()) draw();}).observe(canvas);
  }
  if(document.fonts) document.fonts.ready.then(()=>{ resizeCanvas(); draw(); });
})();
</script>

<p>The magnitude $\lvert\det(\mathbf{A})\rvert$ is the area-scaling factor.
A determinant of $2$ doubles every area, while a determinant of $1/2$ halves it.
The sign keeps track of orientation: a positive determinant preserves the ordering of the two directions, while a negative determinant flips it, like turning a sheet of paper over.</p>

<p>This zero-area case is exactly the rank collapse from before.
When the columns are collinear (try it out by making both vectors of the parallelogram identical in their direction), the matrix maps the plane onto a line, so $\det(\mathbf{A})=0$ and the lost direction makes the matrix non-invertible.
When $\det(\mathbf{A})\neq 0$, the columns span the plane and the matrix is invertible.
In higher dimensions the same idea remains: the determinant is the signed scaling factor for $n$-dimensional volume.</p>

<p>The determinant is associative in the sense if we want to know how a matrix $\mathbf{A}$ and a matrix $\mathbf{B}$ change a vector, we can chain the operations and compute each determinant separately, and then multiply them.</p>

\[\det[AB] = \det[A] \det[B]\]

<p>This makes intuitively sense because applying two transformations in sequence should scale areas by the product of their individual scaling factors.</p>

<h3 id="trace-of-a-matrix">Trace of a Matrix</h3>

<p>The <strong>trace</strong> of a square matrix is the sum of its diagonal entries:</p>

\[\operatorname{Tr}(\mathbf{A})
=
\sum_j a_{jj}.\]

<p>For a two-dimensional matrix, this is simply</p>

\[\operatorname{Tr}
\left(
\begin{bmatrix}
a_{11} &amp; a_{12} \\
a_{21} &amp; a_{22}
\end{bmatrix}
\right)
=
a_{11}+a_{22}.\]

<p>Recall that $a_{ij}$ describes how input component $x_j$ contributes to output component $y_i$.
The diagonal entries are therefore the contributions that remain in the same coordinate: $a_{11}$ maps the first input direction back onto the first output direction, and $a_{22}$ does the same for the second.
The off-diagonal entries mix one direction into another and do not contribute to the trace.</p>

<p>At first, adding the diagonal entries may appear arbitrary.
Its geometric meaning becomes visible when we apply a very small transformation.
Consider the matrix $\mathbf{I}+\delta\mathbf{A}$, where $\delta$ is a small number:</p>

\[\mathbf{I}+\delta\mathbf{A}
=
\begin{bmatrix}
1 &amp; 0 \\
0 &amp; 1
\end{bmatrix}
+ \delta
\begin{bmatrix}
a_{11} &amp; a_{12} \\
a_{21} &amp; a_{22}
\end{bmatrix}
=
\begin{bmatrix}
1+\delta a_{11} &amp; \delta a_{12} \\
\delta a_{21} &amp; 1+\delta a_{22}
\end{bmatrix}.\]

<p>The identity matrix leaves an area unchanged.
The additional term $\delta\mathbf{A}$ applies a small deformation.
To see how this deformation changes area, we calculate the determinant:</p>

\[\begin{align*}
\det(\mathbf{I}+\delta\mathbf{A})
&amp;=
(1+\delta a_{11})(1+\delta a_{22})
-\delta^2 a_{12}a_{21} \\
&amp;=
1+\delta(a_{11}+a_{22})
+\delta^2(a_{11}a_{22}-a_{12}a_{21}).
\end{align*}\]

<p>Because $\delta$ is small, $\delta^2$ is much smaller.
If we focus only on the first-order change and ignore the $\delta^2$ term, we obtain</p>

\[\det(\mathbf{I}+\delta\mathbf{A})
\approx
1+\delta(a_{11}+a_{22})
=
1+\delta\operatorname{Tr}(\mathbf{A}).\]

<p>The trace therefore measures the immediate rate at which a small transformation expands or contracts area.
A positive trace means local expansion, a negative trace means local contraction, and a zero trace means that there is no first-order change in area.</p>

<p>The trace also has a useful cyclic property.
Although matrix multiplication generally does not commute, the trace operator actually does  $\operatorname{Tr}(\mathbf{B}\mathbf{C})=\operatorname{Tr}(\mathbf{C}\mathbf{B})$.
Writing out the diagonal entries makes the reason visible:</p>

\[\begin{align*}
\operatorname{Tr}(\mathbf{B}\mathbf{C})
&amp;=
\sum_i(\mathbf{B}\mathbf{C})_{ii}
=
\sum_i\sum_j b_{ij}c_{ji} \\
&amp;=
\sum_j\sum_i c_{ji}b_{ij}
=
\sum_j(\mathbf{C}\mathbf{B})_{jj} \\
&amp;=
\operatorname{Tr}(\mathbf{C}\mathbf{B}).
\end{align*}\]

<p>The entries $b_{ij}$ and $c_{ji}$ are ordinary numbers, so their multiplication commutes, and we can reorder the finite sums. The only thing we’ve really done is to switch the order of $c_{ji}$ and $b_{ji}$ while also switching out the sum operators.
This does not mean that $\mathbf{B}\mathbf{C}=\mathbf{C}\mathbf{B}$, only that their traces agree.
For three or more factors, this property allows us to rotate their order cyclically:</p>

\[\operatorname{Tr}(\mathbf{B}\mathbf{C}\mathbf{D})
=
\operatorname{Tr}(\mathbf{C}\mathbf{D}\mathbf{B})
=
\operatorname{Tr}(\mathbf{D}\mathbf{B}\mathbf{C}).\]

<p>We cannot generally swap arbitrary factors, but we can move the first factor to the back.</p>

<h3 id="the-eigenheiten-of-matrices">The “Eigenheiten” of Matrices</h3>

<p>What happens to individual directions under the transformation $\mathbf{A}$?
Is there a nonzero vector whose direction survives without being turned, changing only in length or orientation?</p>

<style>
  #eigenvector-widget{
    --line:#dce5e3;
    --ink:#212931;
    --dim:#626d78;
    --teal:#147f7a;
    --teal-soft:#e8f5f2;
    --amber:#a96100;
    --amber-soft:#fff5df;
    margin:26px 0;
    overflow:hidden;
    color:var(--ink);
    background:#fff;
    border:1px solid var(--line);
    border-radius:12px;
    box-shadow:0 14px 34px rgba(33,41,49,0.07);
    font:400 14px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif;
    text-align:left;
  }
  #eigenvector-widget,
  #eigenvector-widget *{box-sizing:border-box;}
  #eigenvector-widget .eigen-head{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    justify-content:space-between;
    gap:10px 18px;
    padding:18px 20px 17px;
    background:linear-gradient(135deg,#fff 0%,#f6faf9 100%);
    border-bottom:1px solid #e3ebe9;
  }
  #eigenvector-widget .eigen-heading{display:grid; min-width:0; gap:4px;}
  #eigenvector-widget .eigen-kicker{
    color:var(--teal);
    font:600 10px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;
    letter-spacing:0.11em;
    text-transform:uppercase;
  }
  #eigenvector-widget h4{
    margin:0;
    color:#172129;
    font:600 17px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
    letter-spacing:0;
    text-transform:none;
  }
  #eigenvector-widget .eigen-matrix{
    display:flex;
    align-items:center;
    gap:8px;
    color:#52616a;
    font:500 12px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;
    font-variant-numeric:tabular-nums;
  }
  #eigenvector-widget .eigen-matrix-grid{
    position:relative;
    display:grid;
    grid-template-columns:repeat(2,2.5em);
    gap:1px 5px;
    padding:3px 8px;
    text-align:center;
  }
  #eigenvector-widget .eigen-matrix-grid::before,
  #eigenvector-widget .eigen-matrix-grid::after{
    content:"";
    position:absolute;
    top:0;
    bottom:0;
    width:5px;
    border:1px solid currentColor;
  }
  #eigenvector-widget .eigen-matrix-grid::before{left:0; border-right:0;}
  #eigenvector-widget .eigen-matrix-grid::after{right:0; border-left:0;}
  #eigenvector-widget .eigen-body{padding:20px;}
  #eigenvector-widget .eigen-plot-shell{
    overflow:hidden;
    background:#fbfdfc;
    border:1px solid var(--line);
    border-radius:9px;
  }
  #eigenvector-widget .eigen-legend{
    display:flex;
    flex-wrap:wrap;
    align-items:center;
    gap:7px 16px;
    min-height:40px;
    padding:9px 12px;
    color:#5a6871;
    background:#f8fbfa;
    border-bottom:1px solid #e2eae8;
    font:500 11px/1.35 ui-monospace,SFMono-Regular,Consolas,monospace;
  }
  #eigenvector-widget .eigen-key{display:inline-flex; align-items:center; gap:6px;}
  #eigenvector-widget .eigen-key::before{
    content:"";
    width:18px;
    border-top:3px solid var(--teal);
    border-radius:2px;
  }
  #eigenvector-widget .eigen-key.output::before{border-color:var(--amber);}
  #eigenvector-widget .eigen-key.guide::before{
    border-top-width:1px;
    border-top-style:dashed;
    border-color:#8f9a9d;
  }
  #eigenvector-widget .eigen-plot{
    display:block;
    width:100%;
    height:auto;
    max-height:520px;
    background:#fbfdfc;
    touch-action:none;
    user-select:none;
  }
  #eigenvector-widget .eigen-plot text{
    font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
    paint-order:stroke;
    stroke:#fbfdfc;
    stroke-width:5px;
    stroke-linejoin:round;
  }
  #eigenvector-widget [data-eigen-handle]{
    cursor:grab;
    outline:none;
  }
  #eigenvector-widget [data-eigen-handle]:active{cursor:grabbing;}
  #eigenvector-widget [data-eigen-handle]:focus-visible .eigen-handle-ring{
    stroke:#172129;
    stroke-width:3;
  }
  #eigenvector-widget .eigen-readout{
    display:grid;
    grid-template-columns:repeat(4,minmax(0,1fr));
    gap:10px;
    margin-top:12px;
  }
  #eigenvector-widget .eigen-metric{
    min-width:0;
    padding:12px 13px 11px;
    background:#fbfdfc;
    border:1px solid var(--line);
    border-radius:8px;
  }
  #eigenvector-widget .eigen-metric[data-active="true"]{
    background:var(--teal-soft);
    border-color:#b7d7d3;
  }
  #eigenvector-widget .eigen-metric-label{
    display:block;
    margin-bottom:4px;
    color:#69757d;
    font:600 9.5px/1.3 ui-monospace,SFMono-Regular,Consolas,monospace;
    letter-spacing:0.08em;
    text-transform:uppercase;
  }
  #eigenvector-widget .eigen-metric strong{
    display:block;
    overflow-wrap:anywhere;
    color:#172129;
    font:600 16px/1.35 ui-monospace,SFMono-Regular,Consolas,monospace;
    font-variant-numeric:tabular-nums;
  }
  #eigenvector-widget .eigen-metric.input strong{color:var(--teal);}
  #eigenvector-widget .eigen-metric.output strong{color:var(--amber);}
  #eigenvector-widget .eigen-actions{
    display:flex;
    align-items:center;
    gap:14px;
    margin-top:12px;
    padding:11px 12px;
    color:#42515a;
    background:#fffaf0;
    border:1px solid #f0dfbd;
    border-radius:8px;
  }
  #eigenvector-widget .eigen-feedback{
    flex:1 1 auto;
    margin:0;
    color:#85520b;
    font-size:12.5px;
    line-height:1.5;
  }
  #eigenvector-widget .eigen-feedback[data-found="true"]{
    color:#0f6e69;
    font-weight:600;
  }
  #eigenvector-widget .eigen-buttons{display:flex; flex:0 0 auto; gap:8px;}
  #eigenvector-widget button{
    height:auto;
    padding:7px 11px;
    color:var(--teal) !important;
    background:#fff;
    border:1px solid #adcac5;
    border-radius:6px;
    box-shadow:none;
    font:500 11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;
    letter-spacing:0;
    text-transform:none;
    cursor:pointer;
  }
  #eigenvector-widget button:hover{color:#fff !important; background:var(--teal);}
  #eigenvector-widget button:focus-visible{
    outline:3px solid rgba(15,118,110,0.22);
    outline-offset:2px;
  }
  #eigenvector-widget .eigen-sr-only{
    position:absolute;
    width:1px;
    height:1px;
    padding:0;
    margin:-1px;
    overflow:hidden;
    clip-path:inset(50%);
    white-space:nowrap;
    border:0;
  }
  @media (max-width:680px){
    #eigenvector-widget .eigen-readout{grid-template-columns:repeat(2,minmax(0,1fr));}
  }
  @media (max-width:480px){
    #eigenvector-widget .eigen-head,
    #eigenvector-widget .eigen-body{padding:14px 12px;}
    #eigenvector-widget h4{font-size:15px;}
    #eigenvector-widget .eigen-matrix{font-size:11px;}
    #eigenvector-widget .eigen-legend{gap:6px 12px; padding:8px 9px; font-size:10px;}
    #eigenvector-widget .eigen-plot{min-height:300px;}
    #eigenvector-widget .eigen-actions{align-items:flex-start; flex-direction:column;}
    #eigenvector-widget .eigen-buttons,
    #eigenvector-widget button{width:100%;}
    #eigenvector-widget .eigen-buttons button{width:auto; flex:1 1 0;}
  }
</style>

<div id="eigenvector-widget" class="matrix-figure" role="group" aria-labelledby="eigenvector-title">
  <div class="eigen-head">
    <div class="eigen-heading">
      <span class="eigen-kicker">Find the direction the matrix preserves</span>
      <h4 id="eigenvector-title">Finding an eigenvector</h4>
    </div>
    <span class="eigen-matrix" role="img" aria-label="A equals the matrix with rows 1.2, 0.8 and 0.8, 1.2">
      <span aria-hidden="true"><i>A</i> =</span>
      <span class="eigen-matrix-grid" aria-hidden="true">
        <span>1.2</span><span>0.8</span><span>0.8</span><span>1.2</span>
      </span>
    </span>
  </div>
  <div class="eigen-body">
    <div class="eigen-plot-shell">
      <div class="eigen-legend" aria-hidden="true">
        <span class="eigen-key">input x · drag its tip</span>
        <span class="eigen-key output">transformed Ax</span>
        <span class="eigen-key guide">eigendirection hint</span>
      </div>
      <svg class="eigen-plot" data-eigen-plot="" viewBox="0 0 680 500" role="application" aria-label="Interactive coordinate plane comparing x and A x." aria-describedby="eigenvector-help"></svg>
    </div>
    <div class="eigen-readout" aria-live="polite">
      <div class="eigen-metric input">
        <span class="eigen-metric-label">Input x</span>
        <strong data-eigen-x="">—</strong>
      </div>
      <div class="eigen-metric output">
        <span class="eigen-metric-label">Output Ax</span>
        <strong data-eigen-ax="">—</strong>
      </div>
      <div class="eigen-metric">
        <span class="eigen-metric-label">Misalignment</span>
        <strong data-eigen-angle="">—</strong>
      </div>
      <div class="eigen-metric" data-eigen-lambda-card="">
        <span class="eigen-metric-label">Inferred scale λ</span>
        <strong data-eigen-lambda="">not aligned</strong>
      </div>
    </div>
    <div class="eigen-actions">
      <p class="eigen-feedback" data-eigen-feedback="">Move the teal tip. Most directions turn under the transformation.</p>
      <div class="eigen-buttons">
        <button type="button" data-eigen-hint="" aria-pressed="false">Show hint</button>
        <button type="button" data-eigen-reset="">Reset</button>
      </div>
    </div>
    <p id="eigenvector-help" class="eigen-sr-only">Drag the teal input tip. With the tip focused, use the arrow keys to move it, or hold Shift for larger steps. The input cannot be moved onto the zero vector.</p>
    <div class="eigen-sr-only" data-eigen-status="" role="status" aria-live="polite" aria-atomic="true"></div>
  </div>
</div>
<script>
(function(){
  const root=document.getElementById('eigenvector-widget');
  const svg=root.querySelector('[data-eigen-plot]');
  const xReadout=root.querySelector('[data-eigen-x]');
  const axReadout=root.querySelector('[data-eigen-ax]');
  const angleReadout=root.querySelector('[data-eigen-angle]');
  const lambdaReadout=root.querySelector('[data-eigen-lambda]');
  const lambdaCard=root.querySelector('[data-eigen-lambda-card]');
  const feedback=root.querySelector('[data-eigen-feedback]');
  const status=root.querySelector('[data-eigen-status]');
  const hintButton=root.querySelector('[data-eigen-hint]');
  const resetButton=root.querySelector('[data-eigen-reset]');
  const NS='http://www.w3.org/2000/svg';
  const frame={left:44,right:636,top:28,bottom:472};
  const narrowFrame={left:108,right:572,top:18,bottom:482};
  const narrowPlot=window.matchMedia('(max-width:480px)');
  const origin={x:340,y:250};
  const scale=72;
  const maxRadius=1.72;
  const minRadius=0.28;
  const initial={x:1.28,y:0.42};
  const colors={input:'#147f7a',output:'#a96100'};
  let vector={x:initial.x,y:initial.y};
  let dragging=false;
  let pointerId=null;
  let hintVisible=false;
  let hasMoved=false;
  let lastAnnounced='';

  function make(tag,attributes,parent,text){
    const node=document.createElementNS(NS,tag);
    Object.entries(attributes||{}).forEach(([key,value])=>node.setAttribute(key,value));
    if(text!==undefined) node.textContent=text;
    (parent||svg).appendChild(node);
    return node;
  }

  const defs=make('defs');
  Object.entries(colors).forEach(([name,color])=>{
    const marker=make('marker',{
      id:'eigen-arrow-'+name,viewBox:'0 0 10 10',refX:8.5,refY:5,
      markerWidth:8,markerHeight:8,markerUnits:'userSpaceOnUse',orient:'auto'
    },defs);
    make('path',{d:'M0 0L10 5L0 10z',fill:color},marker);
  });

  const grid=make('g',{'aria-hidden':'true'});
  for(let tick=-4;tick<=4;tick++){
    const x=origin.x+tick*scale;
    if(x>=frame.left&&x<=frame.right){
      make('line',{x1:x,y1:frame.top,x2:x,y2:frame.bottom,stroke:'#edf1f1','stroke-width':1},grid);
      if(tick!==0) make('text',{x,y:origin.y+17,fill:'#82939d','font-size':10,'text-anchor':'middle'},grid,tick);
    }
  }
  for(let tick=-3;tick<=3;tick++){
    const y=origin.y-tick*scale;
    if(y>=frame.top&&y<=frame.bottom){
      make('line',{x1:frame.left,y1:y,x2:frame.right,y2:y,stroke:'#edf1f1','stroke-width':1},grid);
      if(tick!==0) make('text',{x:origin.x-10,y:y+4,fill:'#82939d','font-size':10,'text-anchor':'end'},grid,tick);
    }
  }
  make('line',{x1:frame.left,y1:origin.y,x2:frame.right,y2:origin.y,stroke:'#aeb9bd','stroke-width':1.2},grid);
  make('line',{x1:origin.x,y1:frame.top,x2:origin.x,y2:frame.bottom,stroke:'#aeb9bd','stroke-width':1.2},grid);
  const xAxisLabel=make('text',{x:frame.right-3,y:origin.y-9,fill:'#758187','font-size':12,'text-anchor':'end'},grid,'x₁');
  const yAxisLabel=make('text',{x:origin.x+10,y:frame.top+14,fill:'#758187','font-size':12},grid,'x₂');
  make('text',{x:origin.x-9,y:origin.y+17,fill:'#758187','font-size':11,'text-anchor':'end'},grid,'0');

  const guides=make('g',{'aria-hidden':'true','data-eigen-guides':'',class:'eigen-guides'});
  const guideLines=[
    make('line',{stroke:'#7f918f','stroke-width':1.4,'stroke-dasharray':'6 7'},guides),
    make('line',{stroke:'#7f918f','stroke-width':1.4,'stroke-dasharray':'6 7'},guides)
  ];
  const guideLabels=[
    make('text',{fill:'#687875','font-size':11,'font-weight':600},guides,'preserved line'),
    make('text',{fill:'#687875','font-size':11,'font-weight':600},guides,'preserved line')
  ];

  const outputLine=make('line',{
    stroke:colors.output,'stroke-width':4,'stroke-linecap':'round',
    'marker-end':'url(#eigen-arrow-output)'
  });
  const inputLine=make('line',{
    stroke:colors.input,'stroke-width':3,'stroke-linecap':'round',
    'marker-end':'url(#eigen-arrow-input)'
  });
  const angleArc=make('path',{
    fill:'none',stroke:'#8fa3af','stroke-width':1.4,
    class:'eigen-angle-arc','data-eigen-angle-arc':'','aria-hidden':'true'
  });
  const outputLabel=make('text',{fill:colors.output,'font-size':14,'font-weight':600},svg,'Ax');
  const inputLabel=make('text',{fill:colors.input,'font-size':14,'font-weight':600},svg,'x');

  const handle=make('g',{
    'data-eigen-handle':'',tabindex:0,role:'button',
    'aria-label':'Move input vector x in two dimensions',
    'aria-describedby':'eigenvector-help'
  });
  const hitTarget=make('circle',{r:16,fill:'transparent'},handle);
  make('circle',{r:7,fill:colors.input,stroke:'#fff','stroke-width':2,class:'eigen-handle-ring'},handle);

  const moveHint=make('g',{'aria-hidden':'true',class:'move-hint','data-eigen-move-hint':''});
  const hintBox=make('rect',{x:0,y:0,width:112,height:25,rx:3,fill:'rgba(255,255,255,0.97)',stroke:colors.input,'stroke-opacity':0.25,'stroke-width':1},moveHint);
  const hintText=make('text',{x:56,y:16.5,fill:colors.input,'font-size':10,'font-weight':500,'text-anchor':'middle',stroke:'none'},moveHint,'Drag & Move Me!');

  function point(value){
    return {x:origin.x+value.x*scale,y:origin.y-value.y*scale};
  }

  function fmt(value){
    const clean=Math.abs(value)<0.005?0:value;
    return clean.toFixed(2);
  }

  function coordinates(value){
    return '('+fmt(value.x)+', '+fmt(value.y)+')';
  }

  function transformed(value){
    return {x:1.2*value.x+0.8*value.y,y:0.8*value.x+1.2*value.y};
  }

  function clampVector(candidate,fallback){
    let length=Math.hypot(candidate.x,candidate.y);
    if(length>maxRadius){
      candidate={x:candidate.x*maxRadius/length,y:candidate.y*maxRadius/length};
      length=maxRadius;
    }
    if(length<minRadius){
      const base=length>1e-6?candidate:fallback;
      const baseLength=Math.hypot(base.x,base.y)||1;
      candidate={x:base.x*minRadius/baseLength,y:base.y*minRadius/baseLength};
    }
    return candidate;
  }

  function lineInFrame(direction){
    const dx=direction.x;
    const dy=-direction.y;
    const limits=[];
    if(Math.abs(dx)>1e-8){
      limits.push((frame.left-origin.x)/dx,(frame.right-origin.x)/dx);
    }
    if(Math.abs(dy)>1e-8){
      limits.push((frame.top-origin.y)/dy,(frame.bottom-origin.y)/dy);
    }
    const negative=Math.max(...limits.filter(value=>value<0));
    const positive=Math.min(...limits.filter(value=>value>0));
    return {
      start:{x:origin.x+negative*dx,y:origin.y+negative*dy},
      end:{x:origin.x+positive*dx,y:origin.y+positive*dy}
    };
  }

  const directions=[{x:1,y:1},{x:1,y:-1}];
  directions.forEach((direction,index)=>{
    const segment=lineInFrame(direction);
    guideLines[index].setAttribute('x1',segment.start.x);
    guideLines[index].setAttribute('y1',segment.start.y);
    guideLines[index].setAttribute('x2',segment.end.x);
    guideLines[index].setAttribute('y2',segment.end.y);
    const labelPoint=index===0?segment.end:segment.start;
    guideLabels[index].setAttribute('x',labelPoint.x+(index===0?-8:8));
    guideLabels[index].setAttribute('y',labelPoint.y+(index===0?16:-9));
    guideLabels[index].setAttribute('text-anchor',index===0?'end':'start');
  });

  function directionDistance(value,direction){
    const angle=Math.atan2(value.y,value.x);
    const target=Math.atan2(direction.y,direction.x);
    let distance=Math.abs(angle-target)%Math.PI;
    if(distance>Math.PI/2) distance=Math.PI-distance;
    return distance*180/Math.PI;
  }

  function setLine(line,end){
    const tip=point(end);
    line.setAttribute('x1',origin.x);
    line.setAttribute('y1',origin.y);
    line.setAttribute('x2',tip.x);
    line.setAttribute('y2',tip.y);
    return tip;
  }

  function placeLabel(label,tip,value,side){
    const bounds=narrowPlot.matches?narrowFrame:frame;
    const length=Math.hypot(value.x,value.y)||1;
    const ux=value.x/length;
    const uy=-value.y/length;
    const normal={x:-uy*side,y:ux*side};
    let x=tip.x+normal.x*19+ux*7;
    let y=tip.y+normal.y*19+uy*7;
    x=Math.max(bounds.left+21,Math.min(bounds.right-21,x));
    y=Math.max(bounds.top+16,Math.min(bounds.bottom-12,y));
    label.setAttribute('x',x);
    label.setAttribute('y',y);
    label.setAttribute('text-anchor',ux<-.2?'end':ux>.2?'start':'middle');
  }

  function announce(message){
    if(message===lastAnnounced) return;
    status.textContent=message;
    lastAnnounced=message;
  }

  function markMoved(){
    if(hasMoved) return;
    hasMoved=true;
    moveHint.classList.add('is-dismissed');
  }

  function draw(shouldAnnounce){
    const output=transformed(vector);
    const inputTip=setLine(inputLine,vector);
    const outputTip=setLine(outputLine,output);
    handle.setAttribute('transform','translate('+inputTip.x+' '+inputTip.y+')');
    handle.setAttribute('aria-label','Move input vector x. Current x is '+coordinates(vector)+'.');
    placeLabel(inputLabel,inputTip,vector,1);
    placeLabel(outputLabel,outputTip,output,-1);

    if(!hasMoved){
      const bounds=narrowPlot.matches?narrowFrame:frame;
      const hintWidth=narrowPlot.matches?160:112;
      const hintHeight=narrowPlot.matches?30:25;
      hintBox.setAttribute('width',hintWidth);
      hintBox.setAttribute('height',hintHeight);
      hintText.setAttribute('x',hintWidth/2);
      hintText.setAttribute('y',hintHeight/2+4);
      const hintX=Math.max(bounds.left+4,Math.min(bounds.right-hintWidth-4,inputTip.x+13));
      const hintY=Math.max(bounds.top+4,Math.min(bounds.bottom-hintHeight-4,inputTip.y+13));
      moveHint.setAttribute('transform','translate('+hintX+' '+hintY+')');
    }

    const inputLength=Math.hypot(vector.x,vector.y);
    const outputLength=Math.hypot(output.x,output.y);
    const cosine=Math.max(-1,Math.min(1,(vector.x*output.x+vector.y*output.y)/(inputLength*outputLength)));
    const angle=Math.acos(cosine)*180/Math.PI;
    const lambda=(vector.x*output.x+vector.y*output.y)/(inputLength*inputLength);
    const inputAngle=Math.atan2(-vector.y,vector.x);
    const outputAngle=Math.atan2(-output.y,output.x);
    const sweep=Math.atan2(Math.sin(outputAngle-inputAngle),Math.cos(outputAngle-inputAngle));
    const arcRadius=34;
    const arcStart={x:origin.x+arcRadius*Math.cos(inputAngle),y:origin.y+arcRadius*Math.sin(inputAngle)};
    const arcEnd={x:origin.x+arcRadius*Math.cos(inputAngle+sweep),y:origin.y+arcRadius*Math.sin(inputAngle+sweep)};
    angleArc.setAttribute('d','M'+arcStart.x+' '+arcStart.y+' A'+arcRadius+' '+arcRadius+' 0 0 '+(sweep>=0?1:0)+' '+arcEnd.x+' '+arcEnd.y);
    angleArc.setAttribute('opacity',angle>0.5?'0.8':'0');
    const nearest=Math.min(...directions.map(direction=>directionDistance(vector,direction)));
    const found=angle<=2.25;
    const near=!found&&nearest<=10;
    const showGuides=hintVisible||near||found;
    guides.setAttribute('opacity',showGuides?(hintVisible?'0.7':'0.48'):'0');

    xReadout.textContent=coordinates(vector);
    axReadout.textContent=coordinates(output);
    angleReadout.textContent=angle.toFixed(1)+'°';
    lambdaReadout.textContent=found?'≈ '+lambda.toFixed(2):'not aligned';
    lambdaCard.dataset.active=String(found);
    feedback.dataset.found=String(found);

    let message;
    if(found){
      message='Approximately aligned — an eigendirection has scale λ ≈ '+lambda.toFixed(2)+'.';
    }else if(near){
      message='Almost aligned — keep nudging the teal tip along the revealed line.';
    }else{
      message='The matrix turns this direction by '+angle.toFixed(1)+'°. Hunt for an alignment.';
    }
    feedback.textContent=message;
    svg.setAttribute('aria-label','Interactive coordinate plane. Input x is '+coordinates(vector)+
      ', transformed A x is '+coordinates(output)+', and their misalignment is '+angle.toFixed(1)+
      ' degrees.'+(found?' Approximately aligned with an eigendirection, with scale '+lambda.toFixed(2)+'.':''));
    if(shouldAnnounce) announce(message);
  }

  function clientPoint(event){
    const svgPoint=svg.createSVGPoint();
    svgPoint.x=event.clientX;
    svgPoint.y=event.clientY;
    return svgPoint.matrixTransform(svg.getScreenCTM().inverse());
  }

  function vectorFromEvent(event){
    const location=clientPoint(event);
    return clampVector({
      x:(location.x-origin.x)/scale,
      y:(origin.y-location.y)/scale
    },vector);
  }

  handle.addEventListener('pointerdown',event=>{
    if(dragging) return;
    dragging=true;
    pointerId=event.pointerId;
    handle.setPointerCapture(pointerId);
    event.preventDefault();
  });
  svg.addEventListener('pointermove',event=>{
    if(!dragging||event.pointerId!==pointerId) return;
    const next=vectorFromEvent(event);
    if(Math.hypot(next.x-vector.x,next.y-vector.y)>1e-9) markMoved();
    vector=next;
    draw(false);
    event.preventDefault();
  });
  function stopDragging(event){
    if(!dragging||event.pointerId!==pointerId) return;
    dragging=false;
    if(handle.hasPointerCapture(pointerId)) handle.releasePointerCapture(pointerId);
    pointerId=null;
    draw(true);
  }
  handle.addEventListener('pointerup',stopDragging);
  handle.addEventListener('pointercancel',stopDragging);
  handle.addEventListener('lostpointercapture',event=>{
    if(dragging&&event.pointerId===pointerId){
      dragging=false;
      pointerId=null;
      draw(true);
    }
  });
  handle.addEventListener('keydown',event=>{
    const delta=(event.shiftKey?0.15:0.05);
    const movement={ArrowLeft:[-delta,0],ArrowRight:[delta,0],ArrowUp:[0,delta],ArrowDown:[0,-delta]}[event.key];
    if(!movement) return;
    const next=clampVector({x:vector.x+movement[0],y:vector.y+movement[1]},vector);
    if(Math.hypot(next.x-vector.x,next.y-vector.y)>1e-9) markMoved();
    vector=next;
    draw(true);
    event.preventDefault();
  });
  hintButton.addEventListener('click',()=>{
    hintVisible=!hintVisible;
    hintButton.setAttribute('aria-pressed',String(hintVisible));
    hintButton.textContent=hintVisible?'Hide hint':'Show hint';
    draw(true);
  });
  resetButton.addEventListener('click',()=>{
    vector={x:initial.x,y:initial.y};
    hasMoved=false;
    moveHint.classList.remove('is-dismissed');
    hintVisible=false;
    hintButton.setAttribute('aria-pressed','false');
    hintButton.textContent='Show hint';
    draw(true);
    handle.focus();
  });
  function resizePlot(){
    const bounds=narrowPlot.matches?narrowFrame:frame;
    svg.setAttribute('viewBox',narrowPlot.matches?'108 18 464 464':'0 0 680 500');
    hitTarget.setAttribute('r',narrowPlot.matches?32:16);
    xAxisLabel.setAttribute('x',bounds.right-10);
    yAxisLabel.setAttribute('y',bounds.top+14);
    draw(false);
  }
  narrowPlot.addEventListener('change',resizePlot);
  resizePlot();
})();
</script>

<p>Such a vector satisfies</p>

\[\mathbf{A}\mathbf{v}=\lambda\mathbf{v} \quad \text{with } \mathbf{v} \neq \mathbf{0}\]

<p>We call $\mathbf{v}$ an <strong>eigenvector</strong> and $\lambda$ its <strong>eigenvalue</strong>.
To be a bit on the nose: An eigenvector only changes its length via its corresponding eigenvalue and not its direction.
Rearranging gives</p>

\[(\mathbf{A}-\lambda\mathbf{I})\mathbf{v}=\mathbf{0}.\]

<p>Now remember our null space conversation: a vector lies in the null space of a matrix if and only if the matrix maps it to the zero vector.
For a particular eigenvalue $\lambda$, the corresponding eigenvector $\mathbf{v}$ satisfies $(\mathbf{A}-\lambda\mathbf{I})\mathbf{v}=\mathbf{0}$.</p>

<p>Above we stated that the eigenvector $\mathbf{v}$ is nonzero.
If it were nonzero, we would obtain the trivial solution $\mathbf{A} \mathbf{0}=\lambda\mathbf{0}$, which is a trivial eigenvector and more importantly would hold for any $\mathbf{A}$ and for any $\lambda$ at all times. So it’s kind of pointless, to be honest.
A nontrivial null space means that some nonzero direction with arbitrary length is mapped to zero.
Moving an input along this direction does not change its output:</p>

\[(\mathbf A-\lambda\mathbf I)(\mathbf x+\mathbf v)
= (\mathbf A-\lambda\mathbf I)\mathbf x + \underbrace{(\mathbf A-\lambda\mathbf I)\mathbf v}_{=0}
=
(\mathbf A-\lambda\mathbf I)\mathbf x.\]

<p>Different inputs along the direction of $\mathbf v$ therefore produce the same output, so no inverse can uniquely recover the original input. Hence, $\mathbf A-\lambda\mathbf I$ is not invertible.
A non-invertible matrix has a determinant of zero, which gives us the characteristic equation for finding eigenvalues:</p>

\[\det(\mathbf{A}-\lambda\mathbf{I})=0.\]

<p>For a two-dimensional matrix, we can write this equation out component by component:</p>

\[\begin{align*}
\det(\mathbf{A}-\lambda\mathbf{I})
&amp;=
\det\left(
\begin{bmatrix}
a_{11} &amp; a_{12} \\
a_{21} &amp; a_{22}
\end{bmatrix}
-
\begin{bmatrix}
\lambda &amp; 0 \\
0 &amp; \lambda
\end{bmatrix}
\right) \\
&amp;=
\det\left(
\begin{bmatrix}
a_{11}-\lambda &amp; a_{12} \\
a_{21} &amp; a_{22}-\lambda
\end{bmatrix}
\right) \\
&amp;=
(a_{11}-\lambda)(a_{22}-\lambda)-a_{12}a_{21} \\
&amp;=
\lambda^2-(a_{11}+a_{22})\lambda
+a_{11}a_{22}-a_{12}a_{21}
=0.
\end{align*}\]

<p>This equation allows us to find the eigenvalues.
Once $\lambda$ is known, the corresponding eigenvectors are the nonzero vectors in $\ker(\mathbf{A}-\lambda\mathbf{I})$.</p>

<p>Geometrically, an eigenvector marks a line preserved by the transformation, while its eigenvalue tells us whether that line is stretched, compressed, flipped, or collapsed.
These directions are the natural axes of the transformation.</p>

<p>Drag $\mathbf{x}$ below and hunt for a direction that the matrix does not turn.</p>

<p>We can make the relationship between eigenvectors and the column space precise using our rank-one matrix from before:</p>

\[\mathbf{A}
=
\begin{bmatrix}
2.5 &amp; 0.5 \\
1 &amp; 0.2
\end{bmatrix}
=
\begin{bmatrix}
\mathbf{a}_1 &amp; \mathbf{a}_2
\end{bmatrix},
\qquad
\mathbf{a}_2=0.2\mathbf{a}_1,
\qquad
\mathbf{a}_1=
\begin{bmatrix}2.5\\1\end{bmatrix}.\]

<p>Its characteristic equation is</p>

\[\det(\mathbf{A}-\lambda\mathbf{I})
=
(2.5-\lambda)(0.2-\lambda)-0.5
=
\lambda(\lambda-2.7)
=0,\]

<p>so the eigenvalues are $\lambda=2.7$ and $\lambda=0$.
Substituting $\lambda=2.7$ into $(\mathbf{A}-\lambda\mathbf{I})\mathbf{v}=\mathbf{0}$ gives</p>

\[\begin{bmatrix}
-0.2 &amp; 0.5 \\
1 &amp; -2.5
\end{bmatrix}
\mathbf{v}
=
\begin{bmatrix}
-0.2 v_1 &amp; 0.5 v_2 \\
1 v_1 &amp; -2.5 v_2
\end{bmatrix}
= \begin{bmatrix}
0 \\
0
\end{bmatrix}.\]

<p>This is just a set of linear equations which we can solve to find the eigenvector $\mathbf{v} = [2.5 \; 1]^\mathsf{T}$ corresponding to $\lambda=2.7$.
Substituting $\lambda=0$ similarly gives multiples of $\mathbf{v}=(-1,5)^\mathsf{T}$.</p>

<h3 id="decompositions-of-a-matrix">Decompositions of a Matrix</h3>

<p>The eigenvector relationship actually gives us a neat entry into a matrix decomposition.
We have the eigenvectors and eigenvalues relationship for each individual eigenvector, and by stacking them into matrices, we can express the entire decomposition compactly.</p>

<p>For a single eigenvector, we have</p>

\[\mathbf{A}\mathbf{v}_i=\lambda_i\mathbf{v}_i.\]

<p>If $\mathbf{A}$ has enough linearly independent eigenvectors, we can stack them as the columns of a matrix</p>

\[\mathbf{V}
=
\begin{bmatrix}
\mathbf{v}_1 &amp; \mathbf{v}_2 &amp; \cdots &amp; \mathbf{v}_n
\end{bmatrix}.\]

<p>Each column of $\mathbf{V}$ interacts with the matrix $\mathbf{A}$ independently of the other eigenvectors.
That’s just the property of a matrix matrix multiply. Conversely, on the right hand side with the eigenvalues $\lambda_i$, we have to scale each of the eigenvectors correspondingly by its eigenvalue:</p>

\[\boldsymbol{\Lambda}
=
\begin{bmatrix}
\lambda_1 &amp; 0 &amp; \cdots &amp; 0 \\
0 &amp; \lambda_2 &amp; \cdots &amp; 0 \\
\vdots &amp; \vdots &amp; \ddots &amp; \vdots \\
0 &amp; 0 &amp; \cdots &amp; \lambda_n
\end{bmatrix}.\]

<p>Multiplying $\mathbf{V}$ by this diagonal matrix scales each column by its corresponding eigenvalue:</p>

\[\mathbf{V}\boldsymbol{\Lambda}
=
\begin{bmatrix}
\lambda_1\mathbf{v}_1 &amp;
\lambda_2\mathbf{v}_2 &amp;
\cdots &amp;
\lambda_n\mathbf{v}_n
\end{bmatrix}.\]

<p>We have therefore bundled all the individual eigenvector equations into</p>

\[\mathbf{A}\mathbf{V}=\mathbf{V}\boldsymbol{\Lambda}.\]

<p>Since we’re assuming a full rank matrix, the columns of $\mathbf{V}$ are linearly independent and $\mathbf{V}$ is invertible.
Multiplying both sides from the right by $\mathbf{V}^{-1}$ gives</p>

\[\mathbf{A}
=
\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}.\]

<p>This is the <strong>eigendecomposition</strong>.
Read from right to left, $\mathbf{V}^{-1}$ changes into eigenvector coordinates, $\boldsymbol{\Lambda}$ scales each eigendirection independently, and $\mathbf{V}$ changes back.
A matrix with enough independent eigenvectors for this construction is called <strong>diagonalizable</strong>.</p>

<p>The <strong>singular value decomposition</strong> extends the same intuition to every real matrix:</p>

\[\mathbf{A}=\mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^{\mathsf T}.\]

<p>Here, orthogonal changes of coordinates surround independent scalings.
The number of nonzero singular values in $\boldsymbol{\Sigma}$ is exactly the rank of the matrix.</p>

<h3 id="matrix-exponentials">Matrix Exponentials</h3>

<p>A matrix can describe not only one transformation, but also a transformation applied continuously.
Consider the linear differential equation</p>

\[\frac{d\mathbf{x}}{dt}=\mathbf{A}\mathbf{x}.\]

<p>Over a tiny interval $\Delta t$, the state changes approximately as</p>

\[\mathbf{x}(t+\Delta t)
\approx
\left(\mathbf{I}+\Delta t\,\mathbf{A}\right)\mathbf{x}(t).\]

<p>Dividing a duration $t$ into $n$ tiny updates gives the matrix analogue of repeated scalar compounding:</p>

\[\mathbf{x}(t)
=
\lim_{n\to\infty}
\left(\mathbf{I}+\frac{t}{n}\mathbf{A}\right)^n
\mathbf{x}(0)
=
e^{t\mathbf{A}}\mathbf{x}(0).\]

<p>Expanding this limit defines the <strong>matrix exponential</strong>:</p>

\[e^{t\mathbf{A}}
=
\mathbf{I}
+t\mathbf{A}
+\frac{t^2\mathbf{A}^2}{2!}
+\frac{t^3\mathbf{A}^3}{3!}
+\cdots.\]

<p>Thus, $\mathbf{A}$ specifies the infinitesimal change, while $e^{t\mathbf{A}}$ accumulates it into the finite transformation after time $t$.</p>

<p>Previously we’ve encountered the eigendecomposition of a matrix in the form of $\mathbf{A}=\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}$. Let’s plug that eigendecomposition into the first three components that we have in the infinite sum of the matrix exponential:</p>

\[\begin{align*}
e^{t\mathbf{A}}
&amp;=
\mathbf{I}
+t\mathbf{A}
+\frac{t^2\mathbf{A}^2}{2!}
+\frac{t^3\mathbf{A}^3}{3!}
+\cdots \\
&amp;= \mathbf{I}
+t\mathbf{A}
+\frac{t^2\mathbf{A} \mathbf A}{2!}
+\frac{t^3\mathbf{A} \mathbf{A} \mathbf{A}}{3!}
+\cdots \\
&amp;= \mathbf{I}
+t\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}
+\frac{t^2\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1} \mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}}{2!}
+\frac{t^3\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1} \mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1} \mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}}{3!}
+\cdots \\
\end{align*}\]

<p>Since $\mathbf V^{-1} \mathbf V = \mathbf I$ by definition of the inverse the intermediate terms between teh $\mathbf \Lambda$ disappear and we get</p>

\[\begin{align*}
e^{t\mathbf{A}}
&amp;=
\mathbf{I}
+t\mathbf{A}
+\frac{t^2\mathbf{A}^2}{2!}
+\frac{t^3\mathbf{A}^3}{3!}
+\cdots \\

&amp;= \mathbf{I}
+t\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}
+\frac{t^2\mathbf{V}\boldsymbol{\Lambda}^2\mathbf{V}^{-1}}{2!}
+\frac{t^3\mathbf{V}\boldsymbol{\Lambda}^3\mathbf{V}^{-1}}{3!}
+\cdots \\
\end{align*}\]

<p>and we can ever so slightly rearrange the $\mathbf V$ and $\mathbf V^{-1}$ out to isolate the exponential terms:</p>

\[\begin{align*}
e^{t \mathbf A} &amp;= 
\overbrace{\mathbf{ V I V^{-1}}}^{=\mathbf I}
+ \mathbf V t \mathbf \Lambda \mathbf V^{-1} 
+ \mathbf V \frac{t^2 \mathbf \Lambda^2}{2!} \mathbf V^{-1} 
+ \mathbf V \frac{t^3 \mathbf \Lambda^3}{3!} \mathbf V^{-1}
+ \cdots \\
&amp;= \mathbf V \underbrace{\left[
I
+  t \mathbf \Lambda 
+ \frac{t^2 \mathbf \Lambda^2}{2!}
+ \frac{t^3 \mathbf \Lambda^3}{3!}
+ \cdots \right]}_{=e^{t \boldsymbol{\Lambda}}} \mathbf V^{-1} \\
&amp;= \mathbf V e^{t \mathbf \Lambda} \mathbf V^{-1}
\end{align*}\]

<p>so each eigendirection evolves independently by $e^{t\lambda_i}$.
The power-series definition remains valid even when $\mathbf{A}$ is not diagonalizable.</p>

<p>We can now connect the matrix exponential to two familiar quantities: the determinant and the trace.
For a diagonalizable matrix, set $t=1$ and take the determinant.
Since determinants multiply and are thus commutative, the change of coordinates and its inverse cancel:</p>

\[\begin{align*}
\det(e^{\mathbf{A}})
&amp;=
\det(\mathbf{V})\det(e^{\boldsymbol{\Lambda}})\det(\mathbf{V}^{-1}) \\
&amp;=
\underbrace{\det(\mathbf{V})\det(\mathbf{V}^{-1})}_{=1}
\det(e^{\boldsymbol{\Lambda}})
=
\det(e^{\boldsymbol{\Lambda}}).
\end{align*}\]

<p>The exponential of a diagonal matrix simply exponentiates each diagonal entry.
Its determinant therefore multiplies the exponentials of the eigenvalues:</p>

\[\det(e^{\boldsymbol{\Lambda}})
=
\prod_i e^{\lambda_i}
=
e^{\sum_i\lambda_i}.\]

<p>What is the sum of the eigenvalues?
It is exactly the trace of $\mathbf{A}$.
Using the cyclic property from the trace section, we move $\mathbf{V}$ from the front to the back of $\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1}$.
This brings $\mathbf{V}^{-1}$ and $\mathbf{V}$ together, where they cancel:</p>

\[\operatorname{Tr}(\mathbf{A})
=
\operatorname{Tr}(\mathbf{V}\boldsymbol{\Lambda}\mathbf{V}^{-1})
=
\operatorname{Tr}(\boldsymbol{\Lambda}\mathbf{V}^{-1}\mathbf{V})
=
\operatorname{Tr}(\boldsymbol{\Lambda})
=
\sum_i\lambda_i.\]

<p>Putting these steps together gives</p>

\[\det(e^{\mathbf{A}})=e^{\operatorname{Tr}(\mathbf{A})}.\]

<p>Although we used an eigendecomposition to derive it, this identity holds for every square matrix, including those that are not diagonalizable.</p>

<h3 id="einstein-notation-you-dont-have-to-be-einstein-to-take-derivatives-of-matrices">Einstein Notation: You don’t have to be Einstein to take derivatives of matrices</h3>

<p>For one component of a matrix-vector multiplication, we write</p>

\[y_1=A_{11}x_1+A_{12}x_2+\cdots.\]

<p>Every output component follows the same pattern: fix the output index $i$ and sum over all input indices $j$,</p>

\[y_i=\sum_j A_{ij}x_j.\]

<p>Here, $i$ is a <strong>free index</strong>: it selects which component of $\mathbf{y}$ we are computing.
The index $j$ is a <strong>repeated</strong>, or <strong>dummy</strong>, index: it runs over all input components and is summed away.
Since a repeated index means “sum over this index,” we can drop the summation sign and write</p>

\[y_i=A_{ij}x_j.\]

<p>This is the Einstein summation convention: less notation, but exactly the same matrix-vector multiplication.</p>

<p>Now let’s use it to take a derivative.
Consider a product of three matrices of compatible sizes,</p>

\[\mathbf{Y}=\mathbf{A}\mathbf{B}\mathbf{C},\]

<p>and suppose we want to differentiate an output entry $Y_{ij}$ with respect to an entry $B_{pq}$ in the middle matrix.
We keep $\mathbf{A}$ and $\mathbf{C}$ fixed and treat the entries of $\mathbf{B}$ as independent variables.
The matrix product may look complicated, but writing out one output component reveals nothing more than a sum:</p>

\[Y_{ij}
=
\sum_k\sum_l A_{ik}B_{kl}C_{lj}
=
A_{ik}B_{kl}C_{lj}.\]

<p>The indices $i$ and $j$ select the output entry, while the repeated indices $k$ and $l$ are summed over.
For two-dimensional matrices, we can write all four terms explicitly:</p>

\[\begin{align*}
Y_{ij}
&amp;=
A_{i1}B_{11}C_{1j}
+A_{i1}B_{12}C_{2j} \\
&amp;\quad+
A_{i2}B_{21}C_{1j}
+A_{i2}B_{22}C_{2j}.
\end{align*}\]

<p>Despite all the matrix notation, this is just a linear expression in the entries of $\mathbf{B}$.
Let’s differentiate with respect to $B_{12}$.
The terms containing $B_{11}$, $B_{21}$, and $B_{22}$ do not depend on $B_{12}$, so their derivatives are zero.
In the remaining term, $B_{12}$ differentiates to $1$, while the surrounding factors stay:</p>

\[\begin{align*}
\frac{\partial Y_{ij}}{\partial B_{12}}
&amp;=
0+A_{i1}\underbrace{\frac{\partial B_{12}}{\partial B_{12}}}_{=1}C_{2j}+0+0
\\
&amp;=
A_{i1}C_{2j}.
\end{align*}\]

<p>For an arbitrary entry $B_{pq}$, the same idea can be written compactly using the <strong>Kronecker delta</strong>:</p>

\[\delta_{kp}
=
\begin{cases}
1, &amp; k=p, \\
0, &amp; k\neq p.
\end{cases}\]

<p>An entry $B_{kl}$ depends on $B_{pq}$ only when both its row and its column match:</p>

\[\frac{\partial B_{kl}}{\partial B_{pq}}
=
\delta_{kp}\delta_{lq}.\]

<p>Substituting this into the component-wise derivative gives</p>

\[\begin{align*}
\frac{\partial Y_{ij}}{\partial B_{pq}}
&amp;=
\sum_k\sum_l
A_{ik}\frac{\partial B_{kl}}{\partial B_{pq}}C_{lj} \\
&amp;=
\sum_k\sum_l A_{ik}\delta_{kp}\delta_{lq}C_{lj} \\
&amp;=
A_{ip}C_{qj}.
\end{align*}\]

<p>The deltas select $k=p$ and $l=q$, so only the coefficient of $B_{pq}$ survives.
In Einstein notation, we can skip the summation signs altogether:</p>

\[\frac{\partial Y_{ij}}{\partial B_{pq}}
=
A_{ik}\delta_{kp}\delta_{lq}C_{lj}
=
A_{ip}C_{qj}.\]

<p>So the trick is not to wrestle with the whole matrix expression at once.
Write one output component, identify which terms contain the entry we are differentiating, and keep its coefficient.
The chosen entry differentiates to $1$; all terms that do not contain it disappear.</p>

<p>Let’s try three slightly more involved examples.
All matrices below are real and have compatible dimensions, and only $\mathbf{B}$ varies.
The matrix gradient collects the entry-wise derivatives into a matrix with the same shape as $\mathbf{B}$.</p>

<h4 id="1-the-trace-of-three-matrices">1. The trace of three matrices</h4>

\[\frac{\partial \operatorname{Tr}(\mathbf{A}\mathbf{B}\mathbf{C})}{\partial \mathbf B_{pq}.\]

<p>How do we differentiate with respect to the middle entry $B_{pq}$, and how do we collect the result into the matrix gradient?
Remember that the trace sums the diagonal entries of the output matrix.</p>

<style>
  .einstein-solution{
    margin:18px 0 30px;
    overflow:hidden;
    color:#243340;
    background:#f9fbfc;
    border:1px solid #e2e8ec;
    border-radius:8px;
  }
  .einstein-solution > summary{
    padding:14px 18px;
    color:#147f7a;
    cursor:pointer;
    font:500 13px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif;
  }
  .einstein-solution > summary:hover{background:#edf7f4;}
  .einstein-solution > summary:focus-visible{
    outline:2px solid #147f7a;
    outline-offset:-3px;
  }
  .einstein-solution[open] > summary{border-bottom:1px solid #e2e8ec;}
  .einstein-solution-body{
    padding:6px 18px 18px;
    min-width:0;
    animation:einstein-solution-reveal 180ms ease-out;
  }
  .einstein-solution-body p{text-align:left;}
  .einstein-solution-body mjx-container[display="true"]{
    min-width:0 !important;
    max-width:100%;
    overflow-x:auto;
    overflow-y:hidden;
    padding:4px 0;
  }
  @keyframes einstein-solution-reveal{
    from{opacity:0; transform:translateY(3px);}
    to{opacity:1; transform:translateY(0);}
  }
  @media (prefers-reduced-motion:reduce){
    .einstein-solution-body{animation:none;}
  }
</style>

<details class="einstein-solution" id="einstein-solution-trace">
  <summary>Show derivation</summary>
  <div class="einstein-solution-body">

    <p>First, write the trace as a sum of components.
Since the output entry is diagonal, its first and last index are both $i$:</p>

\[\operatorname{Tr}(\mathbf{A}\mathbf{B}\mathbf{C})
=
\sum_i\sum_k\sum_l A_{ik}B_{kl}C_{li}
=
A_{ik}B_{kl}C_{li}.\]

    <p>Differentiate the middle factor, just as before:</p>

\[\begin{align*}
\frac{\partial \operatorname{Tr}(\mathbf{A}\mathbf{B}\mathbf{C})}{\partial B_{pq}}
&amp;= \frac{\partial A_{ik}B_{kl}C_{li}}{\partial B_{pq}} \\
&amp;= A_{ik} \frac{\partial B_{kl}}{\partial B_{pq}} C_{li} \\
&amp;= A_{ik}\delta_{kp}\delta_{lq}C_{li} \\
&amp;= A_{ip}C_{qi} \\
&amp;= C_{qi} A_{ip} \\
&amp;=
(\mathbf{C}\mathbf{A})_{qp} \\
&amp;= (\mathbf{C}\mathbf{A})^{\mathsf T}_{pq}
\end{align*}\]

    <p>The remaining index $i$ is still summed over.
Notice that the result is the $qp$ entry of $\mathbf{C}\mathbf{A}$, while the gradient needs its $pq$ entry.
This is where the transpose comes from:</p>

\[\boxed{\nabla_{\mathbf{B}}\operatorname{Tr}(\mathbf{A}\mathbf{B}\mathbf{C})
=(\mathbf{C}\mathbf{A})^{\mathsf T}}.\]

  </div>
</details>

<h4 id="2-a-matrix-that-appears-twice">2. A matrix that appears twice</h4>

\[\frac{\partial \operatorname{Tr}(\mathbf{B}^{\mathsf T}\mathbf{A}\mathbf{B})}{\partial \mathbf B_{pq}}.\]

<p>Find the derivative with respect to $B_{pq}$ and the matrix gradient.
This time $\mathbf{B}$ appears twice, so both occurrences contribute to the derivative.
Do not assume that $\mathbf{A}$ is symmetric.</p>

<details class="einstein-solution" id="einstein-solution-quadratic">
  <summary>Show derivation</summary>
  <div class="einstein-solution-body">

    <p>The transpose changes which entries we select, but it does not create independent variables.
Writing out the trace gives</p>

\[\operatorname{Tr}(\mathbf{B}^{\mathsf T}\mathbf{A}\mathbf{B})
=
(B^{\mathsf T})_{ji}A_{ik}B_{kj}
=
B_{ij}A_{ik}B_{kj}.\]

    <p>Apply the product rule to the two factors containing $\mathbf{B}$:</p>

\[\begin{align*}
\frac{\partial \operatorname{Tr}(\mathbf{B}^{\mathsf T}\mathbf{A}\mathbf{B})}{\partial B_{pq}}
&amp;=
\underbrace{\delta_{ip}\delta_{jq}}_{\partial B_{ij}/\partial B_{pq}}
A_{ik}B_{kj}
+
B_{ij}A_{ik}
\underbrace{\delta_{kp}\delta_{jq}}_{\partial B_{kj}/\partial B_{pq}} \\
&amp;=
A_{pk}B_{kq}
+
B_{iq}A_{ip} \\
&amp;=
(\mathbf{A}\mathbf{B})_{pq}
+
(\mathbf{A}^{\mathsf T}\mathbf{B})_{pq}.
\end{align*}\]

    <p>Each occurrence of $\mathbf{B}$ differentiates in turn, leaving the other occurrence intact.
Collecting the derivatives gives</p>

\[\boxed{\nabla_{\mathbf{B}}\operatorname{Tr}(\mathbf{B}^{\mathsf T}\mathbf{A}\mathbf{B})
=(\mathbf{A}+\mathbf{A}^{\mathsf T})\mathbf{B}}.\]

    <p>Only when $\mathbf{A}$ is symmetric does this simplify to $2\mathbf{A}\mathbf{B}$.</p>

  </div>
</details>

<h4 id="3-a-squared-error-through-three-matrices">3. A squared error through three matrices</h4>

<p>Suppose we want $\mathbf{A}\mathbf{B}\mathbf{C}$ to match a fixed target matrix $\mathbf{D}$.
Consider the loss</p>

\[\frac{1}{2}\|\mathbf{A}\mathbf{B}\mathbf{C}-\mathbf{D}\|_F^2,\]

<p>where the squared Frobenius norm is the sum of the squares of all entries.
Find the derivative with respect to $B_{pq}$ and the matrix gradient.
The outer square introduces a chain rule, but the derivative of the inner matrix product is one we already know.</p>

<details class="einstein-solution" id="einstein-solution-loss">
  <summary>Show derivation</summary>
  <div class="einstein-solution-body">

    <p>Give the residual a short name:</p>

\[\mathbf{R}=\mathbf{A}\mathbf{B}\mathbf{C}-\mathbf{D},
\qquad
R_{ij}=A_{ik}B_{kl}C_{lj}-D_{ij}.\]

    <p>The loss is just a sum of ordinary squares:</p>

\[\frac{1}{2}\|\mathbf{A}\mathbf{B}\mathbf{C}-\mathbf{D}\|_F^2
=
\frac{1}{2}\sum_i\sum_j R_{ij}^2
=
\frac{1}{2}R_{ij}R_{ij}.\]

    <p>The chain rule differentiates the square first.
Its factor of $2$ cancels the $1/2$, leaving the residual times its derivative:</p>

\[\begin{align*}
\frac{\partial \left(\frac{1}{2}\|\mathbf{A}\mathbf{B}\mathbf{C}-\mathbf{D}\|_F^2\right)}{\partial B_{pq}}
&amp;=
R_{ij}\frac{\partial R_{ij}}{\partial B_{pq}} \\
&amp;=
R_{ij}A_{ik}\delta_{kp}\delta_{lq}C_{lj} \\
&amp;=
R_{ij}A_{ip}C_{qj}.
\end{align*}\]

    <p>The target $\mathbf{D}$ is fixed, so its derivative is zero.
We still sum over $i$ and $j$, while $p$ and $q$ select the gradient entry.
Reordering the scalar factors reveals the matrix product:</p>

\[A_{ip}R_{ij}C_{qj}
=
(A^{\mathsf T})_{pi}R_{ij}(C^{\mathsf T})_{jq}.\]

    <p>Thus,</p>

\[\boxed{\nabla_{\mathbf{B}}\left(\frac{1}{2}\|\mathbf{A}\mathbf{B}\mathbf{C}-\mathbf{D}\|_F^2\right)
=
\mathbf{A}^{\mathsf T}
(\mathbf{A}\mathbf{B}\mathbf{C}-\mathbf{D})
\mathbf{C}^{\mathsf T}}.\]

    <p>Even this longer expression reduces to the same ingredients: a sum of components, the chain rule, and deltas selecting the entry we differentiate.</p>

  </div>
</details>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Back to University Math 101]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/blog/BasicsWithMatrics/BasicsWithMatrics.png" /><media:content medium="image" url="/blog/BasicsWithMatrics/BasicsWithMatrics.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Korea and Switzerland</title><link href="/blog/KoreaSwitzerland26/" rel="alternate" type="text/html" title="Korea and Switzerland" /><published>2026-07-25T00:00:00+00:00</published><updated>2026-07-25T00:00:00+00:00</updated><id>/blog/KoreaSwitzerland26</id><content type="html" xml:base="/blog/KoreaSwitzerland26/"><![CDATA[<!-- ## Berlin Over The Years -->

<style>
    .image-gallery {
        overflow: auto;
        margin-left: -1% !important;
    }

    .image-gallery li {
        float: left;
        float: top;
        display: block;
        margin: 0 0 1% 1%;
        width: 99%;
    }

    .image-gallery li a {
        text-align: top;
        text-decoration: none !important;
        color: #777;
    }

    .image-gallery li a span {
        display: block;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        padding: 3px 0;
    }

    .image-gallery li a img {
        width: 100%;
        height: 100%;
        display: flex;
        vertical-align: top;
    }
</style>

<ul class="image-gallery">
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/01.jpg"><img src="/photo_gallery/korea_switzerland26/01.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/03.jpg"><img src="/photo_gallery/korea_switzerland26/03.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/04.jpg"><img src="/photo_gallery/korea_switzerland26/04.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/05.jpg"><img src="/photo_gallery/korea_switzerland26/05.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/06.jpg"><img src="/photo_gallery/korea_switzerland26/06.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/10.jpg"><img src="/photo_gallery/korea_switzerland26/10.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/11.jpg"><img src="/photo_gallery/korea_switzerland26/11.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/12.jpg"><img src="/photo_gallery/korea_switzerland26/12.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/13.jpg"><img src="/photo_gallery/korea_switzerland26/13.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/korea_switzerland26/14.jpg"><img src="/photo_gallery/korea_switzerland26/14.jpg" /></a></li>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
</ul>

<!--  -->

<!--  -->
<p><!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 --></p>]]></content><author><name></name></author><category term="photography" /><summary type="html"><![CDATA[Of Peninsulas and Mountains]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/photo_gallery/korea_switzerland26/06.jpg" /><media:content medium="image" url="/photo_gallery/korea_switzerland26/06.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Monte Carlo (git) Tree Search</title><link href="/blog/MCgitS/" rel="alternate" type="text/html" title="Monte Carlo (git) Tree Search" /><published>2026-05-10T00:00:00+00:00</published><updated>2026-05-10T00:00:00+00:00</updated><id>/blog/MCgitS</id><content type="html" xml:base="/blog/MCgitS/"><![CDATA[<script>
MathJax = {
  tex: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\\[','\\]']],
    processEscapes: true,
    tags: 'all'
  },
  options: {
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
  }
};
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async=""></script>

<style>
.demo-card {
  background: #ffffff;
  border: 1px solid rgba(0,0,0,0.12);
  border-radius: 10px;
  padding: 20px;
  margin: 2em 0;
  color: #1a1a1a;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.demo-card h4 {
  margin: 0 0 12px;
  color: #1e6fc4;
  font-size: 0.85em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.demo-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  align-items: center;
}
.demo-btn {
  background: rgba(100,180,255,0.12);
  border: 1px solid rgba(100,180,255,0.6);
  box-shadow: none;
  color: #1e6fc4 !important;
  padding: 6px 18px;
  height: auto;
  line-height: 1.5;
  border-radius: 5px;
  cursor: pointer;
  font-size: 0.83em;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  transition: background 0.15s, color 0.15s;
}
.demo-btn:hover {
  background: rgba(100,180,255,0.24);
  color: #0a4a8c !important;
}
.demo-status {
  font-size: 0.78em;
  color: rgba(0,0,0,0.65);
  font-family: monospace;
  padding: 5px 10px;
  background: rgba(0,0,0,0.05);
  border-radius: 4px;
  min-height: 1.4em;
  margin-bottom: 8px;
}
.demo-stats {
  display: flex;
  gap: 20px;
  margin-top: 8px;
  font-size: 0.8em;
  color: rgba(0,0,0,0.65);
}
.demo-stats b { color: #b8860b; }
canvas.demo-canvas {
  display: block;
  width: 100%;
  border-radius: 6px;
  background: #ffffff;
}
.mc-readout {
  display: flex;
  gap: 18px;
  margin-top: 8px;
}
.mc-readout-left {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.mc-readout-left .demo-stats,
.mc-readout-left .legend-row { margin-top: 0; }
.mc-key {
  flex: 0 0 auto;
  align-self: flex-start;
  pointer-events: none;
}
.legend-row {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 10px;
  font-size: 0.75em;
  color: rgba(0,0,0,0.65);
}
.ldot {
  display: inline-block;
  width: 9px; height: 9px;
  border-radius: 50%;
  margin-right: 3px;
  vertical-align: middle;
}
/* PUCT explorer */
.puct-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 22px;
}
@media (max-width: 600px) { .puct-grid { grid-template-columns: 1fr; } }
.slider-grp { margin-bottom: 11px; }
.slider-grp label {
  display: flex;
  justify-content: space-between;
  font-size: 0.8em;
  color: rgba(0,0,0,0.7);
  margin-bottom: 3px;
}
.slider-grp label span { color: #b8860b; font-family: monospace; }
.slider-grp input[type=range] { width: 100%; accent-color: #1e6fc4; }
.puct-formula {
  font-size: 0.82em;
  background: rgba(0,0,0,0.04);
  border-left: 3px solid #1e6fc4;
  border-radius: 0 6px 6px 0;
  padding: 12px 14px;
  margin-bottom: 12px;
  line-height: 2;
  font-family: monospace;
  color: #1a1a1a;
}
.puct-score-big {
  text-align: center;
  font-size: 1.5em;
  font-family: monospace;
  color: #2a7d2a;
  margin-top: 6px;
}
/* Callout */
.callout {
  background: rgba(100,180,255,0.08);
  border-left: 4px solid #1e6fc4;
  border-radius: 0 8px 8px 0;
  padding: 14px 18px;
  margin: 1.8em 0;
  color: #1a2238;
  font-size: 0.95em;
  line-height: 1.7;
}
</style>

<p>There has been a tremendous interest in doing autoresearch with LLM agents.
The core idea is that since LLM’s have intelligence (the amount for real world problem solving is still being debated), have infinite stamina and have access to the abundance of information on the web including all recent research, then they could automate parts of the research pipeline that we all do.
Evidently, the writing was on the wall and there is a whole swath of start ups that try to capitalize on this idea with the idea of an AI scientist (Periodic Labs, FutureHouse etc).</p>

<p>LLM’s rely on the iterative filling of their context (to have context at all) and you can add a RAG system, but then you’re still dependent on what’s in your context.
So whatever a LLM does to solve a problem is highly influenced by what’s in it’s context.
Of course there is a lot of baseline intelligence in LLM’s, but the context is still a huge factor in what it can do.</p>

<p>But research is not a linear process in the same way that we linearly and monotonically fill up a context.
Research consist of many rabbit holes, dead ends and resets.
This resetting is something that LLM’s are not ideally placed to do due to the their dependence on the context when trying to find solutions to problems by trial and error.</p>

<p>That begs the question: can we wrap a harness around a LLM to make it more like a human researcher that’s able to deal with setbacks, rabbit hole failures and resetting the context when needed?</p>

<h3 id="graduate-student-descent">Graduate student descent</h3>

<p>There’s an old joke in machine learning that the real optimization algorithm behind most papers isn’t Adam or SGD.
It’s <em>graduate student descent</em>: a PhD student sits in front of a screen, semi-informed, and tweaks things. Bump the learning rate. 
Swap out a loss term. Try a slightly different architecture. Rerun. Squint at the validation number. Try again.</p>

<p>Most of this is busywork — hyperparameter fiddling, “what if I change this one thing,” waiting for the run to finish. Only a handful of the tries actually matter, and the student usually can’t tell in advance which ones, which is exactly why they have to keep trying.</p>

<p>Having done a PhD in machine learing I’m all too familiar with this process.
I have spent countless hours doing this exact loop, it’s unfortunately necessary to get a good result, but it’s also mind numbing and not very fun when you haven’t found the magic combination yet.
I doubt we’ll be able to do away with graduate student descent as machine learning is intrinsically a latent model problem where try to explain/model/predict data and generalize from it.
If you’ve done machine learning research you’ll know th Bermuda triangle of hope between model architecture, parameter optimization and data quirks.</p>

<p>Your graduate student of choice would</p>
<ul>
  <li><strong>proposes</strong> a change, based on a hunch about what might help,</li>
  <li><strong>evaluates</strong> it by running a training job and reading off a number,</li>
  <li>and then <strong>decides what to try next</strong> — usually by leaning into whatever’s been working, but every so often taking a flyer on a direction they’ve barely touched.</li>
</ul>

<p>It’s a semi-informed search over the tree of possible experiments, steered by intuition and metrics.
Intuition is hard to come by if you haven’t done a lot of research before, but LLM agents offer boundless stamina and will never stop trying stuff out.
This has been epitomized in the Ralph loop where a semi-intelligent actor just hammers the problem over and over again.
Or it’s your new grad student is still optimistic about the next experiment.</p>

<h3 id="branching-and-resetting">Branching and Resetting</h3>

<p>The other modus operandi we usually face in research is that we often have to reset our context and start over.
That’s the humiliating act of realizing that your ideas have failed and we need to figuratively and literally clean the white board.</p>

<p>Here’s my biggest gripe with using LLM’s for autoresearch: They’re hard at resetting their context and starting over in a principled way.
The original idea of autoresearch by Karpathy was to use git to reset to the previous state if something failed but it still left the context unchanged.
So fundamentally, while the code was reset the context wasn’t.
It essentially set the autoresearch agent on a linear trajectory of git states with no way of completely changing track and pursuing a different line of research.</p>

<p>But git allows branching such that we could try different ideas from the same root node.
We can try experiments and log the code state in the git tree and then branch off to try a different idea.
Importantly, this allows us to keep a ledge or diary of sorts of the many experiments which can develop in a multitude of different ways.
If we want to revisit a prior experiment, we simply have to checkout the git commit and we have the exact code state that was used to run that experiment.
That allows us to jump around in the tree and pursue different lines of research.</p>

<h3 id="monte-carlo-tree-search">Monte Carlo Tree Search</h3>

<p>So up to now we’ve concluded that LLM are useful testing out ideas with unwavering enthusiasm and that git is a perfect way to log the experiments and keep track of the many different lines of research.
The last missing piece of the puzzle is to combine the two in a principled way such that we can explore the tree of experiments in a way that balances exploration and exploitation.</p>

<p>A long while ago in 2017, I gave a talk on AlphaGo and the Monte Carlo Tree Search algorithm that they used as a fast rollout policy to explore the state space tree of a game of Go.
Mind you, Go is a game with a huge state space and the number of possible moves is astronomical.
So they used a combination of a neural network to evaluate the board state and a Monte Carlo Tree Search to explore the tree of possible moves.</p>

<p>In the Monte Carlo Tree Search algorithm we work fundamentally on a tree structure where each node is a state and each edge is a possible action that can be taken from that state.</p>

<p>MCTS is a way to explore the tree of possible states in a principled way that balances exploration and exploitation.
In the case of AlphaGo we simply counted the number of times a node was visited and the number of wins that were achieved from that node.</p>

<p>As the saying goes “An image is worth a thousand words” so let’s look at the MCTS algorithm in a visual way:</p>

<style>
.mcts-flip {
  margin: 1.6em auto;
  max-width: 100%;
  border: 1px solid rgba(0,0,0,0.12);
  border-radius: 10px;
  padding: 14px 14px 12px;
  background: #ffffff;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.mcts-flip-stage {
  position: relative;
  background: #ffffff;
  border-radius: 6px;
  overflow: hidden;
  text-align: center;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mcts-flip-stage img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 0 auto;
}
.mcts-flip-stage img:not(.is-active) { display: none; }
.mcts-flip-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
}
.mcts-flip-btn {
  background: rgba(100,180,255,0.12);
  border: 1px solid rgba(100,180,255,0.6);
  color: #1e6fc4;
  padding: 5px 14px;
  border-radius: 5px;
  cursor: pointer;
  font-size: 0.83em;
  font-weight: 600;
  font-family: inherit;
  transition: background 0.15s, color 0.15s;
}
.mcts-flip-btn:hover { background: rgba(100,180,255,0.24); color: #0a4a8c; }
.mcts-flip-btn:disabled { opacity: 0.35; cursor: default; }
.mcts-flip-label {
  flex: 1 1 auto;
  text-align: center;
  font-size: 0.85em;
  color: #1a1a1a;
  letter-spacing: 0.02em;
}
.mcts-flip-label b { color: #b8860b; font-family: monospace; margin-right: 6px; }
.mcts-flip-dots {
  display: flex;
  justify-content: center;
  gap: 7px;
  margin-top: 9px;
}
.mcts-flip-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: rgba(0,0,0,0.2);
  border: none;
  padding: 0;
  cursor: pointer;
  transition: background 0.15s;
}
.mcts-flip-dot:hover { background: rgba(0,0,0,0.4); }
.mcts-flip-dot.is-active { background: #1e6fc4; }
</style>

<div class="mcts-flip" id="mcts-flip" tabindex="0" aria-label="MCTS step-by-step flipchart">
  <div class="mcts-flip-stage">
    <img src="/blog/MCgitS/MCTS_GameTree.png" alt="Game tree" data-step="Game tree" class="is-active" />
    <img src="/blog/MCgitS/MCTS_Selection.png" alt="Selection" data-step="Selection" />
    <img src="/blog/MCgitS/MCTS_Expansion.png" alt="Expansion" data-step="Expansion" />
    <img src="/blog/MCgitS/MCTS_Simulation.png" alt="Simulation" data-step="Simulation" />
    <img src="/blog/MCgitS/MCTS_Backpropagation.png" alt="Backpropagation" data-step="Backpropagation" />
    <img src="/blog/MCgitS/MCTS_Action.png" alt="Action" data-step="Action" />
  </div>
  <div class="mcts-flip-controls">
    <button class="mcts-flip-btn" id="mcts-flip-prev" aria-label="Previous">← Prev</button>
    <div class="mcts-flip-label"><b id="mcts-flip-count">1 / 6</b><span id="mcts-flip-name">Game tree</span></div>
    <button class="mcts-flip-btn" id="mcts-flip-next" aria-label="Next">Next →</button>
  </div>
  <div class="mcts-flip-dots" id="mcts-flip-dots"></div>
</div>

<script>
(function() {
  var root  = document.getElementById('mcts-flip');
  if (!root) return;
  var imgs  = root.querySelectorAll('.mcts-flip-stage img');
  var prev  = document.getElementById('mcts-flip-prev');
  var next  = document.getElementById('mcts-flip-next');
  var count = document.getElementById('mcts-flip-count');
  var name  = document.getElementById('mcts-flip-name');
  var dots  = document.getElementById('mcts-flip-dots');
  var i = 0, n = imgs.length;

  var dotEls = [];
  for (var k = 0; k < n; k++) {
    (function(idx) {
      var d = document.createElement('button');
      d.className = 'mcts-flip-dot';
      d.setAttribute('aria-label', 'Step ' + (idx + 1));
      d.addEventListener('click', function() { go(idx); });
      dots.appendChild(d);
      dotEls.push(d);
    })(k);
  }

  function go(j) {
    i = (j + n) % n;
    for (var k = 0; k < n; k++) {
      imgs[k].classList.toggle('is-active', k === i);
      dotEls[k].classList.toggle('is-active', k === i);
    }
    count.textContent = (i + 1) + ' / ' + n;
    name.textContent  = imgs[i].getAttribute('data-step') || '';
    prev.disabled = (i === 0);
    next.disabled = (i === n - 1);
  }

  prev.addEventListener('click', function() { go(i - 1); });
  next.addEventListener('click', function() { go(i + 1); });
  root.addEventListener('keydown', function(e) {
    if (e.key === 'ArrowLeft')  { e.preventDefault(); go(i - 1); }
    if (e.key === 'ArrowRight') { e.preventDefault(); go(i + 1); }
  });

  go(0);
})();
</script>

<p>Initially we start out with a state tree with all the moves with their wins and visits.</p>

<ul style="list-style: none; padding-left: 0; margin-left: 0;">
  <li><strong>Select</strong>: We use a formula to compare all non-terminal child nodes in the tree and decide to which child node to go first. In this simple case we simply compare the number of wins in the subtree to all visits of the subtree. </li>
  <li><strong>Expand</strong>: Reaching a terminal leaf node, we chose an action on how to change the state. In Go we would place a stone on the game board.</li>
  <li><strong>Simulate</strong>: We then simulate the game from the new state to see the outcome. This could be a cheap and fast rollout policy until we win or loose.</li>
  <li><strong>Backpropagate</strong>: We log the win (or loss) and backpropagate the ratio up the tree, automatically increasing the number of wins of any node between the leaf node and the root. This increases the percentage of wins all of those recursive subtrees.</li>
  <li><strong>Action</strong>: With one win or loss recorded, we can choose the best action based on the updated statistics. We use a policy that does a bit of exploration and chooses the middle subtree.</li>
</ul>

<p>Below is an animation of a MCTS search for a simple analytical case to visualize the search character.
The reward is a simple function of a binary action whether to add a 0 or a 1 to a string of length 5. 
The reward is the number of 1’s in the string. The search starts at the root node and explores the tree of possible strings until it finds the optimal string of all 1’s.
After having found the gren and dark green combinations the algorithms keeps on exploring the lesser valuable paths to make sure that it doesn’t miss a better solution.</p>

<p><img src="/blog/MCgitS/k5_tree.gif" alt="MCTS tree visualization" width="100%" style="max-width: 800px;" /></p>

<h3 id="translating-mcts-to-autoresearch">Translating MCTS to Autoresearch</h3>

<p>MCTS gives us a principled way to explore a tree of possible states all originating from a common root node and searching the tree for the best state.</p>

<p>In order to get the performance of a particular node, we define a validation script that isn’t allowed to change and returns a single metric that gauges the performance of the state. This metric is then used to compare the performance of different nodes in the tree.</p>

<p>MCTS was originally formulated for games which have a binary outcome: win or loss.
In machine learning research we often have a continuous outcome (loss) and we want to minimize that loss.
The simple switch is to set an upper and lower (preferabbly zero but negative loglikelihoods can go lower) bound and then just rescaling the validated performance in those bounds to a [0,1] range.</p>

<p>The selection is a formula that compares the performance of all child nodes and chooses the best one to explore further.
A particular node can have children which are already validated or (pre-generated) proposals which still require an expansion.</p>

<p>An expansion is the code change that we did that changes the code base of a particular node in the tree.
This could be a change in the model architecture, a change in the loss function, a change in the optimizer or a change in the data set.</p>

<p>After each expansion we run the validation script to see how good our change performed.
This is the simulation step in MCTS.
Of course our proposal can be so bad that we run immediatley into NaN’s or other problems.
In that case we simply ignore the proposal and mark it as failed.
Failed nodes have no repercussion and are simply skipped in the selection step and represent a dead end.</p>

<p>Once the simulation/validation ran, we backpropagate the [0,1] value up the path between the leaf node and the root node, updating the performance of all nodes in between.</p>

<p>Here’s an interactive example with random best values. You can play it a couple of times ot see how</p>

<div class="demo-card">
<h4>Live MCTS tree simulation</h4>
<div class="demo-controls">
  <button class="demo-btn" id="mc-step">Step →</button>
  <button class="demo-btn" id="mc-auto">Auto</button>
  <button class="demo-btn" id="mc-reset">Reset</button>
  <span style="font-size:0.78em;color:rgba(0,0,0,0.5)">30-iteration budget</span>
</div>
<div class="demo-status" id="mc-status">Ready — press Step to begin.</div>
<canvas class="demo-canvas" id="mc-canvas" width="700" height="160"></canvas>
<div class="mc-readout">
<div class="mc-readout-left">
<div class="demo-stats">
  <span>Iter <b id="mc-iter">0</b> / 30</span>
  <span>Best loss <b id="mc-best">0.0740</b></span>
  <span>Pending proposals <b id="mc-open">4</b></span>
</div>
<div class="legend-row">
  <span><span class="ldot" style="background:hsl(120,65%,42%)"></span>low loss (good)</span>
  <span><span class="ldot" style="background:hsl(55,70%,47%)"></span>medium</span>
  <span><span class="ldot" style="background:hsl(0,65%,45%)"></span>high loss</span>
  <span><span class="ldot" style="background:#2a2a3a;border:1px solid #555"></span>failed verify</span>
  <span><span class="ldot" style="background:#e8a030"></span>queued proposal</span>
  <span><span class="ldot" style="background:#64b4ff"></span>selected path</span>
  <span><span class="ldot" style="background:transparent;border:2px solid #ffd700"></span>newly expanded</span>
</div>
</div>
<svg class="mc-key" width="178" height="88" viewBox="0 0 178 88" aria-label="What each node shows">
  <rect x="0.5" y="0.5" width="177" height="87" rx="8" fill="#ffffff" stroke="#000000" stroke-opacity="0.2" />
  <!-- sample node -->
  <circle cx="30" cy="44" r="14" fill="#97b927" stroke="#000000" stroke-opacity="0.45" stroke-width="2" />
  <text x="30" y="44" fill="#ffffff" font-family="monospace" font-size="8" text-anchor="middle" dominant-baseline="central">0.06</text>
  <text x="38.5" y="35" fill="#1e5ab4" font-family="monospace" font-size="8" font-weight="bold">N9</text>
  <circle cx="19" cy="69" r="4" fill="#e8a030" stroke="#b8730e" />
  <circle cx="30" cy="69" r="4" fill="#e8a030" stroke="#b8730e" />
  <circle cx="41" cy="69" r="4" fill="#e8a030" stroke="#b8730e" />
  <!-- leaders + labels -->
  <g stroke="#000000" stroke-opacity="0.35" stroke-width="1">
    <line x1="40" y1="34" x2="58" y2="18" />
    <line x1="44" y1="44" x2="58" y2="44" />
    <line x1="30" y1="73" x2="58" y2="72" />
  </g>
  <g fill="#000000" fill-opacity="0.75" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="9" dominant-baseline="central">
    <text x="61" y="18">subtree size</text>
    <text x="61" y="44">loss</text>
    <text x="61" y="72">open proposals</text>
  </g>
</svg>
</div>
</div>

<p>Notice how the search keeps coming back to the best branch — but never <em>only</em> that branch. Every so often it wanders off to try something it has mostly ignored. That tension, between milking what already works and checking on what you’ve neglected, is the whole game. Hold onto it; in a moment we’ll see the exact formula that produces it.</p>

<hr />

<h3 id="keeping-things-simple">Keeping things simple</h3>

<p>The idea is all nice but the devil’s in the detail as we all know.</p>

<p>For the implementation I opted for a some very hard boundaries.</p>
<ul style="list-style: none; padding-left: 0; margin-left: 0;">
  <li><strong>Simple</strong>: The whole setup should consist of a single functional file and maybe an additional utils file</li>
  <li><strong>A Single Contract</strong>: There is no try/except (looking at you coding agents), defaults, if/else and catch mechanism. There is a single way to run this simple set up, if you don't it will fail.</li>
  <li><strong>YAGNI (Recycling git)</strong>: Git provides everything we need via inheritance, branching, checking out commits in a super convenient package. We will use as much of git as possible.</li>
  <li><strong>Extendable Base Case</strong>: With the three paradigms above in a single functional python script, it can easily be extended for more complex scenarios to your liking.</li>
</ul>

<p>Wait, how do we inject “intelligence” into the whole setup?
It’s simple: we use the Python SDK for Copilot/Claude.
With everything we’ve talked about so far, every step of the way is a simple prompt to the LLM with a very specific task and a very specific context.
Essentially, we have a very clear idea of what we want to do and we can prompt the LLM to do it for us.
The thing is that we can actually isolate the LLM to whatever it’s supposed to do and not let it wander off into the weeds of the code base.
Since everything runs on git, we can run git commands manually from within the research loop, and start a coding session with the LLM to implement a particular proposal.
After the part with the intelligence is done, we can switch back to normal programming and run the validation script from within our loop.
Since the agents get a fresh context and a clear cut, localized and fence off task to do, we manage everything else around them with a mini harness.</p>

<p>Basically, the MCTS algorithm, the validation script, the git plumbing, the proposals calls, the implementation calls are all simply python commands run from a single script.
The agents might screw up, but we have the harness around them to make sure that they don’t screw up the whole system and can simply pursue a different line of research through the MCTS formalism.</p>

<hr />

<h3 id="the-plumbing-under-the-hood">The Plumbing under the hood</h3>

<p>Once we’ve expanded the tree to a new node, it gets validated and we run a proposer loop that attaches five distinct proposals in text format to the node.
Each proposal has a promise score and a rationale for why it might work.</p>

<p>Then we run a selection algorithm on the node and its children to decide which child node to explore next.
The most basic form would be to run PUCTS (Predictor + Upper Confidence bound applied to Trees) which is a simple formula that balances exploration and exploitation:</p>

<div style="overflow-x: auto;">
\begin{align*}
\text{score}(s, a) = \underbrace{Q(s,a)}_{\text{exploitation}} + \underbrace{c \cdot \pi(a \mid s) \cdot \frac{\sqrt{N(s)}}{1 + N(s,a)}}_{\text{exploration bonus } U}
\end{align*}
</div>

<p>Here, $Q(s,a)$ is the average value of the child node $a$ from state $s$, $N(s)$ is the number of times state $s$ has been visited (includes the subtree attached to that node), and $N(s,a)$ is the number of times action $a$ has been taken from state $s$. The prior $\pi(a \mid s)$ is the promise score of the proposal that created this child node, and $c$ is a constant that controls how much we favor exploration over exploitation.</p>

<p>It looks like a mouthful, but it’s just those two instincts sitting side by side:</p>

<ul>
  <li>$Q(s,a)$ is <strong>how good this looks</strong> — the best score seen so far down this branch (and for a brand-new idea, we optimistically borrow the parent’s score, so it isn’t punished for being untried).</li>
  <li>The second term, $U$, is <strong>how under-explored it is</strong> — large when we’ve barely visited this branch and when our gut feeling about it, the prior $\pi$, was high. It shrinks a little every time we come back.</li>
  <li>$c = 0.5$ is just a knob for how adventurous we are overall.</li>
</ul>

<p>The lovely part is how the balance shifts on its own. Early on, $U$ dominates and the search fans out, sampling lots of directions. As a branch piles up visits, its $U$ collapses and its hard-won $Q$ takes over. Exploration quietly hands off to exploitation, with no schedule to tune by hand.</p>

<p>With a small probability $\varepsilon$ we can also flat-sample a node and try a proposal from there, which is a simple way to keep the search from getting stuck in a local optimum.
So essentially from time to time, we’ll look at all $N(s)$ and pick a node at random to explore, instead of always picking the best $Q(s,a) + U$.
That’s a little bit of jitter to make the search jump out of a local optimum and explore other branches which might be blocked due to a series of bad validations.</p>

<p><strong>What gets stapled to each commit.</strong> Earlier I waved vaguely at git “notes.” Concretely, each live commit carries a small JSON note recording its score, the proposal that created it, and the proposals still waiting to be tried from here:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"metrics"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"loss"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.0623</span><span class="p">},</span><span class="w">
  </span><span class="nl">"winner"</span><span class="p">:</span><span class="w">  </span><span class="p">{</span><span class="nl">"plan"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CODE. Edit losses.py…"</span><span class="p">,</span><span class="w"> </span><span class="nl">"promise"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.72</span><span class="p">,</span><span class="w"> </span><span class="nl">"rationale"</span><span class="p">:</span><span class="w"> </span><span class="s2">"…"</span><span class="p">},</span><span class="w">
  </span><span class="nl">"open"</span><span class="p">:</span><span class="w">    </span><span class="p">[{</span><span class="nl">"plan"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HYPERPARAM…"</span><span class="p">,</span><span class="w"> </span><span class="nl">"promise"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.45</span><span class="p">,</span><span class="w"> </span><span class="nl">"rationale"</span><span class="p">:</span><span class="w"> </span><span class="s2">"…"</span><span class="p">}],</span><span class="w">
  </span><span class="nl">"state"</span><span class="p">:</span><span class="w">   </span><span class="s2">"evaluated"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The nice thing about this is that the entire tree is self-contained in git. Git does all the tracking. The <code class="language-plaintext highlighter-rouge">metrics</code> are the result of the validation script.
I also opted for the script to parse in the entire tree into a nested tree data structure and do all the calculations in memory, then write the notes back to git at the end of each iteration. That way, if the machine dies mid-run, the tree is still consistent and we can pick up where we left off.
The requires us to parse the whole git tree once per iteration, but the tree is small enough that it doesn’t matter. Most of our time will be spent anyways on GPU compute.</p>

<p><code class="language-plaintext highlighter-rouge">winner</code> is the proposal that produced <em>this</em> commit (it lives on the child, not the parent); <code class="language-plaintext highlighter-rouge">open</code> is the queue of ideas not yet tried from here. The entire tree is rebuilt from these notes on every wake-up, so there’s no second copy of the state to fall out of sync. Each run namespaces its notes under an ID like <code class="language-plaintext highlighter-rouge">YYYYMMDD_HHMMSS_µs-happy-otter-abc12345</code>, so several searches can branch off the same starting commit without colliding.</p>

<p><strong>The verify contract.</strong> After each commit the orchestrator runs your evaluation command and reads back a JSON file that has to contain at least <code class="language-plaintext highlighter-rouge">{"loss": &lt;number&gt;}</code>. To stop the agents from “improving” the metric by quietly rewriting the scorer, the verify script is <em>hash-locked</em> on the first run: its checksum is pinned, and if it ever changes, the run aborts on the spot.</p>

<p><strong>The three states a node can be in.</strong> Not every experiment succeeds, and the tree has to cope. A node is one of:</p>

<p><strong><code class="language-plaintext highlighter-rouge">evaluated</code></strong> — verify succeeded and a loss was recorded. The search can keep descending from here.</p>

<p><strong><code class="language-plaintext highlighter-rouge">failed</code></strong> — verify errored or timed out. The search can <em>still</em> descend from here, since a fix might live one commit below, but the node is scored pessimistically so the search routes around it without giving up on the branch.</p>

<p><strong><code class="language-plaintext highlighter-rouge">terminal</code></strong> — a genuine dead end, either declared by the verifier or because every one of its children is itself terminal. The search will not descend here again.</p>

<p>That last rule is worth dwelling on, because <em>terminal</em> deadness bubbles upward. The moment a node has no ideas left in its queue and every one of its children has gone terminal, it turns terminal too — so the exhaustion of an entire subtree propagates quietly back toward the root, one dead node at a time, and the search never wastes another thought on it.</p>

<p><strong>One full iteration</strong>, end to end:</p>

<ol>
  <li><strong>Rebuild</strong> the tree from git notes (pure reads)</li>
  <li><strong>Backpropagate</strong> once — post-order walk filling <code class="language-plaintext highlighter-rouge">n</code>, <code class="language-plaintext highlighter-rouge">subtree_value</code>, cascading <code class="language-plaintext highlighter-rouge">terminal</code> states</li>
  <li>With prob. $\varepsilon$: <strong>flat-sample</strong> a node and append one proposal → go to 7<br />Otherwise: <strong>PUCT-descend</strong> to pick <code class="language-plaintext highlighter-rouge">(node, proposal_index)</code></li>
  <li><strong>Checkout</strong> that node’s commit</li>
  <li><strong>Implement</strong>: run the implementer agent, verify the diff, commit the child</li>
  <li><strong>Evaluate</strong>: run the verify script, record <code class="language-plaintext highlighter-rouge">loss</code> and <code class="language-plaintext highlighter-rouge">state</code></li>
  <li><strong>Seed</strong>: run the proposer at the new node to populate its <code class="language-plaintext highlighter-rouge">open</code> list</li>
  <li><strong>Save</strong>: write notes for child and parent</li>
</ol>

<p>The order is deliberately crash-proof: if the machine dies anywhere in the middle, the tree on disk is still consistent — either the new commit’s note exists and we carry on, or it doesn’t and the parent simply re-tries that idea next time. The entire footprint on disk is a couple of git refs, a checksum, and a log file. No database, no cache, nothing to corrupt.</p>

<p>Every time you start a new run, a random string is created with the root hash in it.
This serves as a marker that we’ll only consider any <code class="language-plaintext highlighter-rouge">git notes</code> that are marked under this ID. It also automatically denotes our root such that we can keep running multiple runs from the same root in parallel and from different machines without any collisions.
This also enables the script to run iteratively such that you can generate new runs on the fly based off previous autoresearch runs. If you know git, you don’t have to learn new concepts.</p>

<p><strong>The whole thing is a single Python file of around 1700 lines, standard library only. Drop it into a git repo, point it at your task and your evaluation command, and let it climb overnight.</strong></p>

<hr />

<p>The thing I keep coming back to is <em>what</em> got automated and what stubbornly didn’t. The busywork — the “let me just bump this and see,” the overnight grind of running and re-running and squinting at numbers — turns out to be genuinely mechanizable. A tree, a metric, and a formula will happily do all of it while you sleep. But the <em>taste</em> — knowing which direction is worth a hunch in the first place, recognizing that a boring-looking number is secretly the interesting result.</p>

<p>So we’ve automated graduate student descent. We have not, it seems, automated the graduate student.</p>

<p>You can find the code <a href="https://github.com/ludwigwinkler/MCgitS">here</a>.</p>

<hr />

<script>
(function() {
'use strict';

/* ── Constants ── */
var C = 0.5, LO = 0.0, HI = 0.1, TAU = Math.PI * 2;
var NODE_R = 16, LEVEL_H = 84, MARGIN = 34, BOTTOM = 30;
var PIP_R = 4.2, PIP_GAP = 11, PIP_DROP = 14;   /* proposal pips, fanned below a node */
var ANIM_MS = 520;
var TYPES = ['HYPERPARAM','CODE','NEW','BLEND'];
var nid = 0;

/* ── Node ── */
function mkNode(parent, loss, proposal, state) {
  return {
    id: nid++, parent: parent, children: [],
    loss: loss, proposal: proposal,
    openProposals: [],
    state: state || (loss !== null ? 'evaluated' : null),
    n: 0, sv: 0, px: 0, py: 0, lx: 0, depth: 0
  };
}

function l2v(loss) {
  return (HI - Math.max(LO, Math.min(HI, loss))) / (HI - LO);
}

function v2c(v) {
  var h = Math.round(v * 120);
  return 'hsl(' + h + ',65%,44%)';
}

function randProps(n) {
  var out = [];
  for (var i = 0; i < n; i++) {
    out.push({
      label: TYPES[Math.floor(Math.random() * TYPES.length)],
      promise: Math.round((0.15 + Math.random() * 0.80) * 100) / 100
    });
  }
  return out;
}

/* ── Backprop ── */
function walkBP(node, parentSV) {
  node.sv = parentSV;
  for (var i = 0; i < node.children.length; i++) walkBP(node.children[i], node.sv);
  var cnt = (node.loss !== null && node.state !== 'failed') ? 1 : 0;
  for (var j = 0; j < node.children.length; j++) cnt += node.children[j].n;
  node.n = cnt;
  var vals = [];
  if (node.loss !== null && node.state !== 'failed') vals.push(l2v(node.loss));
  for (var k = 0; k < node.children.length; k++) {
    var c = node.children[k];
    if (c.state !== 'failed' && c.n > 0) vals.push(c.sv);
  }
  if (vals.length > 0) {
    var mx = vals[0];
    for (var m = 1; m < vals.length; m++) if (vals[m] > mx) mx = vals[m];
    node.sv = mx;
  }
}

function backprop(root) { walkBP(root, 1.0); }

/* ── Select ── */
function selectFrom(node, path) {
  if (!node.openProposals.length && !node.children.length) return {node: node, pi: null, path: path};
  var live = [];
  for (var i = 0; i < node.children.length; i++) {
    if (node.children[i].state !== 'terminal') live.push(node.children[i]);
  }
  var totalP = 0;
  for (var j = 0; j < live.length; j++) totalP += live[j].proposal.promise;
  for (var k = 0; k < node.openProposals.length; k++) totalP += node.openProposals[k].promise;
  if (totalP <= 0) return {node: node, pi: null, path: path};

  var best = -Infinity, bestType = null, bestIdx = -1;
  for (var ci = 0; ci < live.length; ci++) {
    var ch = live[ci];
    var pr = ch.proposal.promise / totalP;
    var sc = ch.sv + C * pr * Math.sqrt(Math.max(node.n, 1)) / (1 + ch.n);
    if (sc > best) { best = sc; bestType = 'child'; bestIdx = ci; }
  }
  for (var oi = 0; oi < node.openProposals.length; oi++) {
    var op = node.openProposals[oi];
    var opr = op.promise / totalP;
    var osc = node.sv + C * opr * Math.sqrt(Math.max(node.n, 1)) / 1;
    if (osc > best) { best = osc; bestType = 'open'; bestIdx = oi; }
  }
  if (bestType === 'child') {
    var child = live[bestIdx];
    return selectFrom(child, path.concat([child]));
  }
  return {node: node, pi: bestIdx, path: path};
}

/* ── Layout ── */
function assignLeaves(node, counter) {
  if (node.children.length === 0) {
    node.lx = counter.v++;
  } else {
    for (var i = 0; i < node.children.length; i++) assignLeaves(node.children[i], counter);
    var sum = 0;
    for (var j = 0; j < node.children.length; j++) sum += node.children[j].lx;
    node.lx = sum / node.children.length;
  }
}

function applyPos(node, depth, sx, offX) {
  node.px = offX + node.lx * sx;
  node.py = MARGIN + depth * LEVEL_H;
  node.depth = depth;
  for (var i = 0; i < node.children.length; i++) applyPos(node.children[i], depth + 1, sx, offX);
}

function maxDepth(node, d) {
  var mx = d;
  for (var i = 0; i < node.children.length; i++) {
    var v = maxDepth(node.children[i], d + 1);
    if (v > mx) mx = v;
  }
  return mx;
}

function allNodes(node, out) {
  out.push(node);
  for (var i = 0; i < node.children.length; i++) allNodes(node.children[i], out);
  return out;
}

/* ── Small helpers ── */
function lerp(a, b, t) { return a + (b - a) * t; }
function easeOut(t) { return 1 - (1 - t) * (1 - t); }
function pipX(node, i, count) { return node.px - (count - 1) * PIP_GAP / 2 + i * PIP_GAP; }
function pipY(node) { return node.py + NODE_R + PIP_DROP; }

/* ── Render ── */
function render(canvas, root, selSet, lastNew, anim, now) {
  var ctr = {v: 0};
  assignLeaves(root, ctr);
  var nLeaves = Math.max(ctr.v, 1);

  var dpr = window.devicePixelRatio || 1;
  var cssW = Math.max(canvas.clientWidth || 700, 280);
  var avail = cssW - 2 * (MARGIN + NODE_R);
  var sx = Math.min(112, Math.max(34, nLeaves > 1 ? avail / (nLeaves - 1) : avail));
  var usedW = (nLeaves - 1) * sx;
  var offX = (cssW - usedW) / 2;
  applyPos(root, 0, sx, offX);

  var md = maxDepth(root, 0);
  var logicalH = MARGIN + md * LEVEL_H + NODE_R + BOTTOM;
  var logicalW = cssW;

  canvas.width = Math.round(logicalW * dpr);
  canvas.height = Math.round(logicalH * dpr);
  canvas.style.height = logicalH + 'px';

  var ctx = canvas.getContext('2d');
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, logicalW, logicalH);
  var bg = ctx.createLinearGradient(0, 0, 0, logicalH);
  bg.addColorStop(0, '#ffffff');
  bg.addColorStop(1, '#f4f6fa');
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, logicalW, logicalH);

  var nodes = allNodes(root, []);

  /* animation interpolation (a proposal pip flying down into the new child) */
  var ap = 1, ix = 0, iy = 0, ir = NODE_R;
  if (anim) {
    ap = easeOut(Math.max(0, Math.min(1, (now - anim.t0) / anim.dur)));
    ix = lerp(anim.parent.px, anim.child.px, ap);
    iy = lerp(pipY(anim.parent), anim.child.py, ap);
    ir = lerp(PIP_R, NODE_R, ap);
  }

  /* edges */
  ctx.lineCap = 'round';
  for (var ei = 0; ei < nodes.length; ei++) {
    var nd = nodes[ei];
    for (var ci = 0; ci < nd.children.length; ci++) {
      var ch = nd.children[ci];
      var ex = ch.px, ey = ch.py;
      var animEdge = anim && ch === anim.child;
      if (animEdge) { ex = ix; ey = iy; }
      var hi = (selSet[nd.id] && selSet[ch.id]) || animEdge;
      ctx.beginPath();
      ctx.moveTo(nd.px, nd.py);
      ctx.lineTo(ex, ey);
      ctx.strokeStyle = hi ? 'rgba(40,110,210,0.95)' : 'rgba(0,0,0,0.2)';
      ctx.lineWidth = hi ? 2.6 : 1.1;
      ctx.stroke();
    }
  }

  /* proposal pips beneath each node (the queue of things still to try) */
  for (var pq = 0; pq < nodes.length; pq++) {
    var pn = nodes[pq];
    var k = pn.openProposals.length;
    for (var pp = 0; pp < k; pp++) {
      var prop = pn.openProposals[pp];
      ctx.beginPath();
      ctx.arc(pipX(pn, pp, k), pipY(pn), PIP_R, 0, TAU);
      ctx.fillStyle = 'rgba(232,160,48,' + (0.4 + 0.55 * prop.promise).toFixed(3) + ')';
      ctx.fill();
      ctx.lineWidth = 1;
      ctx.strokeStyle = 'rgba(245,180,70,0.85)';
      ctx.stroke();
    }
  }

  /* nodes */
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  for (var ni = 0; ni < nodes.length; ni++) {
    var n = nodes[ni];
    var isNew = (n === lastNew);
    var inSel = !!selSet[n.id];
    var isRoot = !n.parent;
    var animating = anim && n === anim.child;
    var x = animating ? ix : n.px;
    var y = animating ? iy : n.py;
    var r = animating ? ir : NODE_R;

    /* blue glow trailing the expansion, so the eye follows it down */
    if (animating) {
      var halo = ctx.createRadialGradient(x, y, r, x, y, r + 14);
      halo.addColorStop(0, 'rgba(100,180,255,0.5)');
      halo.addColorStop(1, 'rgba(100,180,255,0)');
      ctx.beginPath();
      ctx.arc(x, y, r + 14, 0, TAU);
      ctx.fillStyle = halo;
      ctx.fill();
    }

    ctx.beginPath();
    ctx.arc(x, y, r, 0, TAU);
    ctx.fillStyle = n.state === 'failed' ? '#2a2a3a'
                  : n.loss !== null ? v2c(l2v(n.loss))
                  : '#2c3050';
    ctx.fill();
    ctx.strokeStyle = animating ? '#1e7ad6' : isNew ? '#d99b00' : inSel ? '#1e7ad6' : isRoot ? 'rgba(0,0,0,0.55)' : 'rgba(0,0,0,0.3)';
    ctx.lineWidth = animating ? 3.2 : isNew ? 3 : inSel ? 2.5 : isRoot ? 2 : 1;
    ctx.stroke();

    /* label (fades in as an animating child grows) */
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.globalAlpha = animating ? ap : 1;
    ctx.fillStyle = '#fff';
    if (n.state === 'failed') {
      ctx.font = 'bold 13px sans-serif';
      ctx.fillText('×', x, y);
    } else if (n.loss !== null) {
      ctx.font = '8.5px monospace';
      ctx.fillText(n.loss.toFixed(3), x, y);
    } else {
      ctx.font = '11px sans-serif';
      ctx.fillText('…', x, y);
    }
    ctx.globalAlpha = 1;

    /* visit counter N, tucked at the top-right corner; grows as visits accrue */
    if (!animating && n.n > 0) {
      ctx.font = 'bold 8px monospace';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'alphabetic';
      ctx.shadowColor = 'rgba(255,255,255,0.9)';
      ctx.shadowBlur = 2;
      ctx.fillStyle = 'rgba(30,90,180,0.95)';
      ctx.fillText('N' + n.n, x + NODE_R * 0.6, y - NODE_R * 0.58);
      ctx.shadowBlur = 0;
    }
  }
}

/* ── Simulation state ── */
var root, selPath, lastNew, lastAction, iteration, bestLoss, autoTimer, anim, rafId;

function init() {
  nid = 0;
  root = mkNode(null, 0.074, null, 'evaluated');
  root.openProposals = randProps(4);
  backprop(root);
  selPath = [root];
  lastNew = null;
  anim = null;
  lastAction = 'Initialized — baseline loss 0.0740, 4 proposals queued.';
  iteration = 0;
  bestLoss = 0.074;
}

function openCount() {
  var ns = allNodes(root, []), s = 0;
  for (var i = 0; i < ns.length; i++) s += ns[i].openProposals.length;
  return s;
}

function step() {
  var res = selectFrom(root, [root]);
  selPath = res.path;

  if (res.pi === null) {
    res.node.openProposals = randProps(3);
    backprop(root);
    lastAction = 'Node ' + res.node.id + ' was out of ideas — seeded 3 fresh proposals.';
    lastNew = null;
    anim = null;
    return;
  }

  var node = res.node;
  var prop = node.openProposals.splice(res.pi, 1)[0];   /* one proposal leaves the queue */
  var fail = Math.random() < 0.10;
  var pLoss = node.loss !== null ? node.loss : 0.074;
  var raw = pLoss + (Math.random() - 0.48) * 0.022;
  var loss = Math.max(0.012, Math.min(0.115, raw));

  var child = mkNode(node, fail ? null : loss, prop, fail ? 'failed' : 'evaluated');
  if (!fail) child.openProposals = randProps(3);
  node.children.push(child);
  backprop(root);

  selPath = res.path.concat([child]);   /* keep the blue path running all the way to the new node */
  lastNew = child;
  iteration++;
  if (!fail && loss < bestLoss) bestLoss = loss;
  anim = { parent: node, child: child, t0: (window.performance || Date).now(), dur: ANIM_MS };

  lastAction = fail
    ? '[' + prop.label + '] promise ' + prop.promise + ' → verify FAILED'
    : '[' + prop.label + '] promise ' + prop.promise + ' → loss ' + loss.toFixed(4) + (loss <= bestLoss + 0.0001 ? '  ★ new best' : '');
}

/* ── Wire up ── */
document.addEventListener('DOMContentLoaded', function() {
  var canvas = document.getElementById('mc-canvas');
  var stepBtn = document.getElementById('mc-step');
  var autoBtn = document.getElementById('mc-auto');
  var resetBtn = document.getElementById('mc-reset');
  var statusEl = document.getElementById('mc-status');
  var iterEl = document.getElementById('mc-iter');
  var bestEl = document.getElementById('mc-best');
  var openEl = document.getElementById('mc-open');

  if (!canvas) return;

  function now() { return (window.performance || Date).now(); }

  function selSet() {
    var s = {};
    for (var i = 0; i < selPath.length; i++) s[selPath[i].id] = true;
    return s;
  }

  function paint(t) { render(canvas, root, selSet(), lastNew, anim, t); }

  function refreshStats() {
    statusEl.textContent = lastAction;
    iterEl.textContent = iteration;
    bestEl.textContent = bestLoss.toFixed(4);
    if (openEl) openEl.textContent = openCount();
  }

  function stopRaf() { if (rafId) { cancelAnimationFrame(rafId); rafId = null; } }

  function runAnim() {
    function frame(t) {
      paint(t);
      if (anim && (t - anim.t0) < anim.dur) {
        rafId = requestAnimationFrame(frame);
      } else {
        anim = null; rafId = null; paint(now());
      }
    }
    rafId = requestAnimationFrame(frame);
  }

  function doStep() {
    stopRaf();
    anim = null;          /* settle any in-flight animation to its final frame */
    step();
    refreshStats();
    if (anim) runAnim(); else paint(now());
  }

  init();
  paint(now());
  refreshStats();

  stepBtn.addEventListener('click', function() {
    if (iteration >= 30) { statusEl.textContent = 'Budget of 30 iterations exhausted — press Reset.'; return; }
    doStep();
  });

  autoBtn.addEventListener('click', function() {
    if (autoTimer) {
      clearInterval(autoTimer); autoTimer = null; autoBtn.textContent = 'Auto';
    } else {
      autoTimer = setInterval(function() {
        if (iteration >= 30) { clearInterval(autoTimer); autoTimer = null; autoBtn.textContent = 'Auto'; return; }
        doStep();
      }, 900);
      autoBtn.textContent = 'Pause';
    }
  });

  resetBtn.addEventListener('click', function() {
    if (autoTimer) { clearInterval(autoTimer); autoTimer = null; autoBtn.textContent = 'Auto'; }
    stopRaf();
    init(); paint(now()); refreshStats();
  });

  window.addEventListener('resize', function() { if (!rafId) paint(now()); });
});

})();
</script>

<script>
(function() {
'use strict';

function drawPUCT() {
  var Q     = parseFloat(document.getElementById('ps-Q').value);
  var pi    = parseFloat(document.getElementById('ps-pi').value);
  var Np    = parseInt(document.getElementById('ps-Np').value);
  var Nc    = parseInt(document.getElementById('ps-Nc').value);
  var c     = 0.5;
  var U     = c * pi * Math.sqrt(Math.max(Np, 1)) / (1 + Nc);
  var score = Q + U;

  document.getElementById('pv-Q').textContent  = Q.toFixed(2);
  document.getElementById('pv-pi').textContent = pi.toFixed(2);
  document.getElementById('pv-Np').textContent = Np;
  document.getElementById('pv-Nc').textContent = Nc;
  document.getElementById('pf-Q').textContent  = Q.toFixed(3);
  document.getElementById('pf-pi').textContent = pi.toFixed(2);
  document.getElementById('pf-Np').textContent = Np;
  document.getElementById('pf-Nc').textContent = Nc;
  document.getElementById('pf-U').textContent  = U.toFixed(3);
  document.getElementById('pf-score').textContent = score.toFixed(3);

  var canvas = document.getElementById('puct-canvas');
  if (!canvas) return;
  var ctx = canvas.getContext('2d');
  var w = canvas.width, h = canvas.height;
  ctx.clearRect(0, 0, w, h);
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, w, h);

  var maxV = 1.8;
  var bw = 52, gap = 18, base = h - 20;

  function bar(x, val, color, label, valStr) {
    var bh = Math.max(0, val / maxV * (h - 28));
    ctx.fillStyle = color;
    ctx.fillRect(x, base - bh, bw, bh);
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.font = '9px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(label, x + bw / 2, base + 12);
    ctx.fillStyle = color;
    ctx.font = '8px monospace';
    ctx.fillText(valStr, x + bw / 2, base - bh - 6);
  }

  bar(gap,           Q,     '#4a90d9', 'Q',   Q.toFixed(3));
  bar(gap*2 + bw,    U,     '#e8a030', 'U',   U.toFixed(3));
  bar(gap*3 + bw*2,  score, '#55cc55', 'Q+U', score.toFixed(3));
}

document.addEventListener('DOMContentLoaded', function() {
  ['ps-Q','ps-pi','ps-Np','ps-Nc'].forEach(function(id) {
    var el = document.getElementById(id);
    if (el) el.addEventListener('input', drawPUCT);
  });
  drawPUCT();
});
})();
</script>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Autoresearch with Monte Carlo Tree Search on Git Trees]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/blog/MCgitS/git_tree.gif" /><media:content medium="image" url="/blog/MCgitS/git_tree.gif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">New York</title><link href="/blog/NYC26/" rel="alternate" type="text/html" title="New York" /><published>2026-04-11T00:00:00+00:00</published><updated>2026-04-11T00:00:00+00:00</updated><id>/blog/NYC26</id><content type="html" xml:base="/blog/NYC26/"><![CDATA[<!-- ## Berlin Over The Years -->

<style>
    .image-gallery {
        overflow: auto;
        margin-left: -1% !important;
    }

    .image-gallery li {
        float: left;
        float: top;
        display: block;
        margin: 0 0 1% 1%;
        width: 99%;
    }

    .image-gallery li a {
        text-align: top;
        text-decoration: none !important;
        color: #777;
    }

    .image-gallery li a span {
        display: block;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        padding: 3px 0;
    }

    .image-gallery li a img {
        width: 100%;
        height: 100%;
        display: flex;
        vertical-align: top;
    }
</style>

<ul class="image-gallery">
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5387.jpg"><img src="/photo_gallery/nyc26/DSC5387.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5407.jpg"><img src="/photo_gallery/nyc26/DSC5407.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5462.jpg"><img src="/photo_gallery/nyc26/DSC5462.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5612.jpg"><img src="/photo_gallery/nyc26/DSC5612.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5615.jpg"><img src="/photo_gallery/nyc26/DSC5615.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5658.jpg"><img src="/photo_gallery/nyc26/DSC5658.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5692.jpg"><img src="/photo_gallery/nyc26/DSC5692.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5709.jpg"><img src="/photo_gallery/nyc26/DSC5709.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5913.jpg"><img src="/photo_gallery/nyc26/DSC5913.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5943.jpg"><img src="/photo_gallery/nyc26/DSC5943.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC5970.jpg"><img src="/photo_gallery/nyc26/DSC5970.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6007.jpg"><img src="/photo_gallery/nyc26/DSC6007.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6209.jpg"><img src="/photo_gallery/nyc26/DSC6209.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6426.jpg"><img src="/photo_gallery/nyc26/DSC6426.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6507.jpg"><img src="/photo_gallery/nyc26/DSC6507.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6623.jpg"><img src="/photo_gallery/nyc26/DSC6623.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6638.jpg"><img src="/photo_gallery/nyc26/DSC6638.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6725.jpg"><img src="/photo_gallery/nyc26/DSC6725.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC6906.jpg"><img src="/photo_gallery/nyc26/DSC6906.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/nyc26/DSC7008.jpg"><img src="/photo_gallery/nyc26/DSC7008.jpg" /></a></li>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
</ul>

<!--  -->

<!--  -->
<p><!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 --></p>]]></content><author><name></name></author><category term="photography" /><summary type="html"><![CDATA[Up and Down the Hudson to Boston]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/photo_gallery/nyc26/DSC5658.jpg" /><media:content medium="image" url="/photo_gallery/nyc26/DSC5658.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Feynman-Kac Correctors</title><link href="/blog/FKC/" rel="alternate" type="text/html" title="Feynman-Kac Correctors" /><published>2026-03-18T00:00:00+00:00</published><updated>2026-03-18T00:00:00+00:00</updated><id>/blog/FKC</id><content type="html" xml:base="/blog/FKC/"><![CDATA[<script>
MathJax = {
  tex: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\\[','\\]']],
    processEscapes: true,
    tags: 'all'
  },
  options: {
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
  }
};
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async=""></script>

<h2 id="fokker-planck-equation">Fokker-Planck Equation</h2>

<p>The Fokker-Planck equation describes the time evolution of the probability density function $p(\mathbf{x}, t)$ of a stochastic process:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\frac{\partial p(x, t)}{\partial t} = -\nabla \cdot \left[ \mu(x, t) p(x, t) \right] + \Delta \left[ \sigma^2(x, t) p(x, t) \right]
\end{align*}
$$
</div>

<p>where:</p>
<ul>
  <li>$\mu(x, t)$ is the drift coefficient (deterministic force)</li>
  <li>$\sigma^2(x, t)$ is the diffusion coefficient (related to the noise strength)</li>
  <li>$\nabla$ is the gradient operator</li>
  <li>$\Delta = \nabla^2$ is the Laplacian operator</li>
</ul>

<p>What this essential equation tells us is how the probability density of a system’s state changes over time due to both deterministic forces (drift) and random fluctuations (diffusion). The first term on the right-hand side represents the effect of the drift, while the second term accounts for the diffusion.
If we compute $\partial_t p(x,t)$ for a given $p(x,t)$, we can determine how the probability distribution evolves over time, which is crucial for understanding the dynamics of stochastic systems.
The nice thing is that normalization is automatically baked into this equation. The total probability is conserved, meaning that if we integrate $p(x, t)$ over all possible states $x$, it will always equal 1 for all time $t$.</p>

<p><img src="/blog/FKC/fokker_planck_gmm.gif" alt="Fokker-Planck Equation Evolution" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" /></p>

<h2 id="the-product-of-distributions">The product of distributions</h2>

<p>The product of two distributions $p(x)$ and $q(x)$ is defined as:</p>
<div style="overflow-x: auto;">
$$
(p \cdot q)(x) = \frac{p(x) q(x)}{\int p(x') q(x') dx'} = \frac{1}{Z} p(x) q(x)
$$
</div>

<p>Why does the integral in the denominator appear? It ensures that the resulting distribution is properly normalized, meaning that the total probability integrates to 1.
The product $p(x) q(x)$ represents the unnormalized joint distribution and therefore has the correct shape, and dividing by the integral $\int p(x’) q(x’) dx’$ normalizes it to become a valid probability distribution.</p>

<h2 id="reward-tilted-sde">Reward Tilted SDE</h2>

<p>We consider the product of the original distribution $q_t(x)$ and an exponential of a reward function $r_t(x)$, scaled by a factor $\beta_t$:</p>
<div style="overflow-x: auto;">
$$
p_t(x) = \frac{1}{Z} \ q_t(x) \ \exp( \ \beta_t \ r(x))
$$
</div>

<p><strong>The big question is: what is the Fokker Planck equation of $p_t(x)$ from which we can easily read off the SDE if the process evolves according to the original process dynamics of $q_t(x)$?</strong>
Said differently, what’s the probability of a sample $x$ under the tilted $p_t$ if it evolves according to the stochastic differential equation of $q_t$?</p>

<p>So we need</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t p_t(x) &amp;= - \nabla \cdot [\mu \ p_t] + \frac{\sigma_p^2}{2} \Delta p_t \\
% &amp;= - \nabla \mu \cdot p_t - \mu \cdot \nabla p_t + \frac{\sigma_p^2}{2} \nabla \cdot [ \nabla p_t] \\
\end{align*}
$$
</div>

<p>where $\mu$ and $\sigma_p^2$ are the drift and diffusion of the SDE that generates $q_t$.</p>

<p>In order to obtain the FPE for the tilted distribution $p_t(x)$, we will exploit the FPE in log space, which offers some nice algebraic advantages.
To realize that we seek to obtain the log FPE, namely not $\partial_t p_t(x)$, but</p>
<div style="overflow-x: auto;">
$$
\partial_t \log p_t(x) =  \partial_t\log q_t(x) + \partial_t \beta_t \ r(x) - \partial_t \log Z
$$
</div>

<h3 id="fpe-in-log-space">FPE in log space</h3>

<p>Let us first recall the log-derivative trick which we will use extensively in the following calculations. For any function (or distribution) $q_t(x)$, we have the following identities:</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t \log p_t = \frac{1}{p_t} \partial_t p_t \quad \rightarrow \quad \partial_t p_t &amp;= p_t \ \partial_t \log p_t \\
\nabla \log p_t = \frac{1}{p_t} \nabla p_t \quad \rightarrow \quad \nabla p_t &amp;= p_t \ \nabla \log p_t \\
\Delta p_t = \nabla \cdot \nabla p_t \quad \rightarrow \quad \Delta p_t &amp;= \nabla \cdot [p_t \nabla \log p_t]  \\
&amp;= p_t \Delta \log p_t + \underbrace{\nabla p_t}_{=p_t \nabla \log p_t} \cdot \nabla \log p_t  \\
&amp;= p_t \Delta \log p_t + p_t ||\nabla \log p_t||^2
\end{align*}
$$
</div>

<p>This allows us to substitute all the gradient calculations in the FPE of our original process $q_t$ with the log derivatives,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t q_t(x) &amp;= - \nabla \cdot [\mu \ q_t] + \frac{\sigma_q^2}{2} \Delta q_t \\
&amp;= - \nabla \mu_q \cdot q_t - \mu_q \cdot \nabla q_t + \frac{\sigma_q^2}{2} \nabla \cdot [ \nabla q_t] \\
&amp; \downarrow \quad \text{transform into log space} \\
q_t \ \partial_t \log q_t 
&amp;= - \nabla \mu_q \cdot q_t - \mu_q \cdot q_t \ \nabla \log q_t + \frac{\sigma_q^2}{2} q_t \ \left( \Delta \log q_t + ||\nabla \log q_t||^2 \right) \\ 
&amp; \downarrow \quad \text{divide by} \ q_t \\
\partial_t \log q_t 
&amp;= - \nabla \mu_q - \mu_q \cdot \nabla \log q_t + \frac{\sigma_q^2}{2} \Delta \log q_t + \frac{\sigma_q^2}{2} ||\nabla \log q_t||^2
\end{align*}
$$
</div>

<p>The term $\partial_t \log q_t$ is log transformed Fokker Planck equation and governs the evolution of the original distribution $q_t$ in log space.</p>

<p>How $q_t$ (or $\log q_t$) evolves over time is determined by the drift $\mu_q$ and the diffusion $\sigma_q^2$ of the original process.
We’re able to model that with the FPE of the original process $q_t$, but here we’re actually interested in the dynamics of $p_t$.
So the question is whether we can somehow massage the term</p>
<div style="overflow-x: auto;">
$$
\partial_t \log p_t(x) =  \partial_t\log q_t(x) + \partial_t \beta_t \ r(x) - \partial_t \log Z
$$
</div>
<p>into the form of the FPE from which we can then read off the drift and diffusion of the SDE the tilted process $p_t$.</p>

<p>In fact, we can express the change $\log q_t$ in terms of the sought-after process $p_t$ by rearranging the terms,</p>
<div style="overflow-x: auto;">
$$
\log q_t = \log p_t - \beta_t \ r(x) + \log Z
$$
</div>

<p>The derivatives of $\log q_t$ are</p>
<div style="overflow-x: auto;">
$$
\nabla \log q_t = \nabla \log p_t - \beta_t \nabla r(x) \quad \text{and} \quad \Delta \log q_t = \Delta \log p_t - \beta_t \Delta r(x)
$$
</div>

<p>Substituting these into the log FPE of $q_t$ gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t \log q_t 
&amp;= - \nabla \mu_q - \mu_q \cdot {\color{red}\nabla \log q_t} + \frac{\sigma_q^2}{2} {\color{blue}\Delta \log q_t} + \frac{\sigma_q^2}{2} {\color{green}||\nabla \log q_t||^2} \\
&amp;= - \nabla \mu_q - \mu_q \cdot {\color{red}(\nabla \log p_t - \beta_t \nabla r(x))} + \frac{\sigma_q^2}{2} {\color{blue}(\Delta \log p_t - \beta_t \Delta r(x))} + \frac{\sigma_q^2}{2} {\color{green}||\nabla \log p_t - \beta_t \nabla r(x)||^2} \\
&amp;= \underbrace{- \nabla \mu_q - \mu_q \cdot {\color{red}\nabla \log p_t} + \frac{\sigma_q^2}{2} {\color{blue}\Delta \log p_t} + \frac{\sigma_q^2}{2} {\color{green}||\nabla \log p_t||^2}}_{\text{log FPE of} \ p_t}
+ {\color{red} \mu_q \cdot \beta_t \nabla r(x)} - \frac{\sigma_q^2}{2} {\color{blue}\beta_t \Delta r(x)} - \frac{\sigma_q^2}{2} \beta_t^2 ({\color{green} 2 \nabla\log p_t \cdot \nabla r(x) +||\nabla r(x)||^2)}
\end{align*}
$$
</div>

<p>And what do we spy with our little eyes?
The first four terms are actually the log FPE of $p_t$ itself!</p>

<p>We we plug in $\partial_t \log q_t$ into $\partial_t \log p_t(x)$, we get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t \log p_t(x)
&amp;=  \partial_t\log q_t(x) + \partial_t \beta_t \ r(x) - \partial_t \log Z \\
&amp;= \overbrace{\underbrace{- \nabla \mu_q - \mu_q \cdot {\color{red}\nabla \log p_t} + \frac{\sigma_q^2}{2} {\color{blue}\Delta \log p_t} + \frac{\sigma_q^2}{2} {\color{green}||\nabla \log p_t||^2}}_{\text{log FPE of} \ p_t}
+ {\color{red} \mu_q \cdot \beta_t \nabla r(x)} - \frac{\sigma_q^2}{2} {\color{blue}\beta_t \Delta r(x)} - \frac{\sigma_q^2}{2} \beta_t^2 ({\color{green} 2 \nabla\log p_t \cdot \nabla r(x) +||\nabla r(x)||^2)}}^{\partial_t \log q_t} + \partial_t \beta_t \ r(x) - \partial_t \log Z 
\end{align*}
$$
</div>

<p>In order to go back to the non-log FPE, we just have to multiply both sides by $p_t$ and apply the log derivative identities from above in reverse,</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
p_t \partial_t \log p_t(x)
&amp;=  p_t \left( \partial_t\log q_t(x) + \partial_t \beta_t \ r(x) - \partial_t \log Z \right) \\
\partial_t p_t &amp;= - \nabla \mu_q \cdot p_t - \mu_q \cdot p_t \nabla \log p_t + \frac{\sigma_q^2}{2} p_t \left( \Delta \log p_t + ||\nabla \log p_t||^2 \right)
+ p_t \left(\mu_q \cdot \beta_t \nabla r(x) - \frac{\sigma_q^2}{2} \beta_t \Delta r(x) - \frac{\sigma_q^2}{2} \beta_t^2 (2 \nabla\log p_t \cdot \nabla r(x) +||\nabla r(x)||^2) + \partial_t \beta_t \ r(x) - \partial_t \log Z \right) \\
\partial_t p_t &amp;= - \nabla \mu_q \cdot p_t - \mu_q \cdot \nabla p_t + \frac{\sigma_q^2}{2} \left(  p_t \nabla \cdot [ \nabla \log p_t] + p_t \nabla \log p_t \cdot \nabla \log p_t \right)
+ p_t \left(\ldots \right) \\
\partial_t p_t &amp;= - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \left( p_t \nabla \cdot [ \nabla \log p_t] + \nabla p_t  \cdot \nabla \log p_t \right)
+ p_t \left(\ldots \right) \\
\partial_t p_t &amp;= - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \nabla \cdot [ \underbrace{p_t \nabla \log p_t}_{\nabla p_t}]
+ p_t \left(\ldots \right) \\
\partial_t p_t &amp;= - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t
+ p_t \Big(\underbrace{\mu_q \cdot \beta_t \nabla r(x) - \frac{\sigma_q^2}{2} \beta_t \Delta r(x) - \frac{\sigma_q^2}{2} \beta_t^2 (2 \nabla\log p_t \cdot \nabla r(x) +||\nabla r(x)||^2) + \partial_t \beta_t \ r(x)}_{g(x) } - \partial_t \log Z \Big) \\
\end{align*}
$$
</div>

<p>The last thing is how we actually compute $\partial_t \log Z$.
We can compute $\partial_t \log Z$ by differentiating the definition of $Z$.
For notational simplicity, let us define $\tilde{p} = \int q_t \exp(\beta_t r(x)) dx$</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t \log Z &amp;= \partial_t \log \int q_t(x) \exp(\beta_t r(x)) dx \\
&amp;= \frac{1}{Z} \ \partial_t \int q_t(x) \exp(\beta_t r(x)) dx \\
&amp;= \frac{1}{Z} \int \partial_t \ \tilde{p}_t \ dx \\
&amp;= \frac{1}{Z} \int \tilde{p} \ \partial_t \log \tilde{p}_t \ dx \\
&amp;= \int p_t \ \partial_t \log \tilde{p}_t \ dx \\
&amp;= \int p_t \ \left[  \partial_t \ \log q_t + \partial_t \beta_t \ r(x) \ \right] dx \\
\end{align*}
$$
</div>

<p>It should immediately be obvious that the term $\partial_t \log q_t + \partial_t \beta_t \ r(x)$ is the same term into which we’ve inserted the log derivatives.
So we expand the $\log q_t$ in the same way as we did above, and then multiply it with the $p_t$ in the integral, we get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t \log Z_t 
&amp;= \int  - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t + p_t g(x) \ dx \\
&amp;= \int  \partial_t p_t + p_t g(x) \ dx \\
&amp;= \underbrace{\int  \partial_t p_t dx}_{=0} + \int p_t g(x) \ dx \\
&amp;= \mathbb{E}_{p_t} \left[ g(x) \right]
\end{align*}
$$
</div>
<p>which is literally just repeating the same steps that we did above to transform the log FPE to the FPE.</p>

<p>One of the key properties of the Fokker-Planck equation is that it conserves probability, meaning that the total probability integrates to 1 for all time $t$.
So if we integrate all the outflows and inflows of probability across the entire state space, they should balance out to zero, ensuring that the total probability remains constant.
If at any time step $t$, the change in probability $\partial_t$ over the entire space $x$ would not be zero, it would imply that probability is either being created or destroyed, which violates the fundamental principle of probability conservation.
So if from one $t$ to the next $t+dt$, all the changes in probability across the state space would be $-0.1$, it would imply that the total probability has decreased by 0.1 which would render our distribution $p_t$ invalid as it would no longer integrate to 1.</p>

<p>But in fact, we can show this analytically by integrating the FPE of $p_t$ over the entire state space.
For the left hand side, we have</p>
<div style="overflow-x: auto;">
$$
\int \partial_t p_t dx = \partial_t \int p_t dx = \partial_t 1 = 0
$$
</div>
<p>where we’ve used Leibniz rule where we can interchange differentiation and integration for different variables.
For the right hand side of the FPE, we have</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
&amp;\int - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t \ dx \\
= &amp; \int - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t \ dx \\
= &amp; \int - \nabla \mu \ p_t - \nabla p_t \mu + \frac{\sigma_q^2}{2} \nabla^2 p_t  \ dx \\
\end{align*}
$$
</div>

<p>Using integration by parts,</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\int f \ \nabla g \ dx = [f \ g]^{\infty}_{-\infty} - \int g \ \nabla f \ dx
\end{align*}
$$
</div>
<p>we can exploit that fact that for any reasonable probability distribution $p_t$, the probability density at the boundaries of the state space should be zero, meaning that $\lim_{x \to \infty} p_t(x) = 0$ and $\lim_{x \to -\infty} p_t(x) = 0$.</p>

<p>Thus we have,</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
&amp; \int - \nabla \mu \ p_t - \nabla p_t \mu + \frac{\sigma_q^2}{2} \nabla^2 p_t  \ dx \\
= &amp; \int - \nabla \mu \ p_t - [ p_t \ \mu ]^{\infty}_{-\infty} + p_t \nabla \mu + \frac{\sigma_q^2}{2} \nabla^2 p_t \ dx \\
= &amp; \int \frac{\sigma_q^2}{2} \nabla \ [p_t \nabla \log p_t ] \ dx \\
= &amp; \frac{\sigma_q^2}{2} \int  \nabla \ [p_t \nabla \log p_t ] \ dx \\
= &amp; \frac{\sigma_q^2}{2} [p_t \nabla \log p_t ]^{\infty}_{-\infty} \\
= &amp; \frac{\sigma_q^2}{2} \ 0
\end{align*}
$$
</div>
<p>where we’ve used the fact integration and derivatives cancel each other out while preserving the boundary evaluation conditions of $p_t$ at $\pm \infty$.</p>

<p>We can also play the integration by parts game one more time and show that</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
&amp; \frac{\sigma_q^2}{2} \int  \nabla \ [p_t \nabla \log p_t ] \ dx \\
= &amp; \frac{\sigma_q^2}{2} \int  \nabla p_t \cdot \nabla \log p_t + p_t \nabla^2 \log p_t \ dx \\
= &amp; \frac{\sigma_q^2}{2} \int  [p_t \nabla \log p_t]^\infty_{-\infty} - p_t \nabla^2 \log p_t + p_t \nabla^2 \log p_t \ dx \\
= &amp; \frac{\sigma_q^2}{2} \ 0
\end{align*}
$$
</div>

<p>All of this let’s us rewrite the FPE of our tilted distribution $p_t$ as</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t p_t &amp;= - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t
+ p_t \Big(g(x) - \mathbb{E}_{p_t} \left[ g(x) \right] \Big)
\end{align*}
$$
</div>

<p>Is it in fact quite interesting that the change of probability equation that we’ve derived consists of two different parts: the original FP equation of $p_t$ and an additional term that captures the effect of the reward tilt on the probability distribution.</p>

<p>The first term consists only of the original drift $\mu_q$ and diffusion $\sigma_q^2$ of the original process $q_t$, which implies that we can model the original SDE as is.
It is only the second term that introduces the effect of the reward tilt, modifying the probability distribution according to the function $g(x)$.
Every term in the second part $g(x)$ contains in fact the reward function $r(x)$, whether it be $r(x)$ directly or its gradients $\nabla r(x)$ and $\Delta r(x)$, which means that the reward function is the only source of change in the probability distribution $p_t$.</p>

<h2 id="the-feynman-kac-equation">The Feynman-Kac Equation</h2>

<p>The Feynman-Kac equation is a fundamental result that links partial differential equations (PDEs) to expectations of stochastic processes. For a function $p$ satisfying certain boundary conditions, it defines a PDE.</p>

<p>For a SDE for a particle $x_t$ defined as</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
dx_s = \mu(x_s) ds + \sigma(x_s) dW_s
\end{align*}
$$
</div>
<p>We can define two key operators:
The first one is the the generator that acts on the test function and describes its instantaneous rate of change of the test function $\theta$ along the paths of the SDE. We’ve seen this in the Feynman Kac equation where we used it to define</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\mathcal{L}_t [\phi] = \mu(x) \cdot \nabla \phi + \frac{\sigma^2(x)}{2} \nabla^2 \phi
\end{align*}
$$
</div>
<p>where the FK equation is defined as $\partial_t \phi +\mathcal{L}_t[\phi] + g \ \phi = 0$.</p>

<p>The adjoint generator acts on the probability distribution $p$ and describes how the probability distribution evolves over time under the dynamics of the SDE. It is defined as</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\mathcal{L}^*_t [p] = - \nabla \cdot [\mu(x) p] + \frac{\sigma^2(x)}{2} \nabla^2 p
\end{align*}
$$
</div>

<p>Both generators are linked through the identity</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\int \phi \ \mathcal{L}^*_t [p] dx = \int p \ \mathcal{L}_t [\phi] dx
\end{align*}
$$
</div>

<p>Now, we’re interested in some test function on $x$ that we name $\phi(x, t)$ and we’re interested in what the distribution of $\phi(x,0)$ and the end of our reverse diffusion process is.
We have defined the probability evolution of $p$ to be governed by the Feynman-Kac PDE,</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t p_t &amp;= - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t
+ p_t \overbrace{\Big(g(x) - \mathbb{E}_{p_t} \left[ g(x) \right] \Big)}^{\bar{g}}
\end{align*}
$$
</div>

<p>This tells us how the probability distribution $p_t$ evolves over time under the influence of both the original dynamics of the SDE (captured by the first two terms) and the reward tilt (captured by the last term).</p>

<h2 id="connecting-it-to-the-feynman-kac-equation">Connecting it to the Feynman Kac equation</h2>

<p>In a previous <a href="https://ludwigwinkler.github.io/blog/FeynmanKac/">blog post on the Feynman-Kac equation</a>, we saw that for very particular PDE that are defined in the state space of the SDE, we can express the solution of the PDE as an expectation over the trajectories of the SDE.
Namely, we have a PDE of the form</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
-\partial_t u(x,t) = \mu(x,t) \nabla u(x,t) + \frac{1}{2}\sigma^2(x,t) \nabla^2 u(x,t) + g(x,t) u(x,t)
\end{align*}
$$
</div>
<p>subject to the terminal condition $u(x,T) = \Phi_T(x)$, then the solution of this PDE is given by the Feynman-Kac formula.
A straightforward option would be to evolve this very particular PDE backward in time form $T$ to an earlier $t$ and then read off the solution at $t=0$. This would give us $u(x,t)$.</p>

<p>If the dynamics of $x$ are governed by the Ito drift-diffusion process defined above, then the solution of the PDE is given by the Feynman-Kac formula</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
u(x,t) = \mathbb{E} \Big[ \Phi_T(x_T) \exp \left( \int_t^T g(x_s, s) ds \right) \Big| x_t = x \Big]
\end{align*}
$$
</div>
<p>where the expectation is taken over all trajectories of the SDE that start at $x$ at time $t$ and evolve according to the dynamics of the SDE until time $T$ with values $x_T$.</p>

<p>We now have two differential equations</p>
<div style="overflow-x: auto;">
$$
\begin{align*}\partial_t p_t &amp;= - \nabla \cdot [ \mu \ p_t ] + \frac{\sigma_q^2}{2} \Delta p_t
+ p_t \Big(g(x) - \mathbb{E}_{p_t} \left[ g(x) \right] \Big) \\
g(x,t) = &amp; \mu_q \cdot \beta_t \nabla r(x) - \frac{\sigma_q^2}{2} \beta_t \Delta r(x) - \frac{\sigma_q^2}{2} \beta_t^2 (2 \nabla\log p_t \cdot \nabla r(x) +||\nabla r(x)||^2) + \partial_t \beta_t \ r(x) \\
-\partial_t u(x,t) &amp;= \mu(x,t) \nabla u(x,t) + \frac{1}{2}\sigma^2(x,t) \nabla^2 u(x,t) + g(x,t) u(x,t)
\end{align*}
$$
</div>

<p>The samples $x$ evolve according to the $dx = \mu_t dt + \sigma_t dW_t$ and induce the stochastic process $q_t$ but we’re interested in the distribution $p_t(x) = \frac{1}{Z} q_t(x) \exp(\beta r(x))$ and want to know how likely it is to sample $x$ from $p_t$ if we evolve according to the SDE of $q_t$.
The function $g$ consists almost exclusively of terms that contain the reward function $r(x)$, which nicely shows that any change in the probability distribution $p_t$ is solely due to the reward tilt encapsulated in $g$.</p>

<p>Let’s assume a test function $u(x,t)$ with some terminal condition $u(x,T)$ and we evolve it backward in time according to the Feynman-Kac PDE over the trajectories of the SDE defined by $\mu$ and $\sigma$. As we’re evolving some terminal function backwards in time, this is obviously very closely linked to the Kolmogorov backward equation.</p>

<p><strong>The question the FKC correctors try to answer is: Given my samples $x$ generated from $q_t$ but tilted towards some reward function $r(x)$, what is the expectation of $u(x,T)$ if I were to evolve further from $t$ to $T$ according to the original SDE of $q_t$?</strong></p>

<p>We’re interested in the expectation</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}_{p_T} \Big[ u(x,T) \ \Big| \ x_t = x \Big] = \int u(x,T) \ p_T(x) dx = \int u(x,T) \ \frac{1}{Z_T} q_T(x) \exp(\beta_T r(x)) dx
\end{align*}
$$
</div>

<p>In the equation above, $q$ generates the samples $x$ according to the SDE, $r(x)$ tilts our distribution towards higher rewards such that our samples $x$ should exhibit certain characteristics favoured by $r(x)$ and then we want to know the expected value of $u(x,T)$ under this tilted distribution.</p>

<p>Now we can ask ourselves how $\int u(x,T) \ p_T(x) dx$ evolves over time. To make things a tad easier, we’ll for now just consider the unnormalized distribution $\tilde{p}_t(x) = q_t(x) \exp(\beta_t r(x))$ and then we can later on divide by (some estimate of) $Z_t$ to get the normalized distribution $p_t$.
Fortunately, every component in $\partial_t p_t$ is a function of $p_t$, so we extract $1/Z_t$ out of every component to get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\partial_t \tilde{p}_t &amp;= - \nabla \cdot [ \mu \ \tilde{p}_t ] + \frac{\sigma_q^2}{2} \Delta \tilde{p}_t
+ \tilde{p}_t \bar{g}(x)
\end{align*}
$$
</div>
<p>The proof for this is in the FKC paper, but it is quite intuitive if you think about it. The original FPE of $p_t$ is linear in $p_t$, so if we replace $p_t$ with $\tilde{p}_t / Z_t$, we can just pull out the $1/Z_t$ from every term and get the FPE of $\tilde{p}_t$.
The authors show the consistency of this in the appendix.</p>

<p>Thus we’re want to know</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\frac{d}{dt} \int u(x,t) \ \tilde{p}_t(x) dx
\end{align*}
$$
</div>
<p>behaves.
Using Leibniz rule, we can interchange differentiation and integration for different variables, which gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\frac{d}{dt} \int u(x,t) \ \tilde{p}_t(x) dx 
&amp;= \int \partial_t u(x,t) \ \tilde{p}_t(x) dx + \int u(x,t) \ \partial_t \tilde{p}_t(x) dx \\
&amp;= \int \left(-\mu(x,t) \nabla u(x,t) - \frac{1}{2}\sigma^2(x,t) \nabla^2 u(x,t) - g(x,t) u(x,t) \right) \ \tilde{p}_t(x) dx \\
&amp; \quad + \int u(x,t) \ \left(- \nabla \cdot [ \mu \ \tilde{p}_t ] + \frac{\sigma_q^2}{2} \Delta \tilde{p}_t + \tilde{p}_t \bar{g}(x) \right) dx \\
&amp;= \int \left(-\mu(x,t) \nabla u(x,t) - \frac{1}{2}\sigma^2(x,t) \nabla^2 u(x,t) - g(x,t) u(x,t) \right) \ \tilde{p}_t(x) dx \\
&amp; \quad + \int \left(\mu(x,t) \nabla u(x,t) + \frac{1}{2}\sigma^2(x,t) \nabla^2 u(x,t) + g(x,t) u(x,t) \right) \ \tilde{p}_t(x) dx \quad \quad | \quad \quad \text{integration by parts with } \tilde{p}_t(\pm \infty) = 0 \\
&amp;= 0
\end{align*} 
$$
</div>
<p>The integration by parts substitution is a pure algebraic busy work and I skipped it here. It’s just applying the product rule many times to fully separate every term, then applying integration by parts to every term by pulling in $u$ and recombining the terms.</p>

<p>Integrating the dynamics from $0$ to $T$ gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\int_0^T \frac{d}{dt} \int u(x,t) \ \tilde{p}_t(x) dx dt
&amp;= \left[ \int u(x,t) \ \tilde{p}_t(x) dx \right]^T_0 \\
&amp;= \int u(x,T) \ \tilde{p}_T(x) dx - \int u(x,0) \ \tilde{p}_0(x) dx \\ 
&amp;= 0 \\
&amp; \downarrow \quad \text{rearranging} \\
\int u(x,T) \ \tilde{p}_T(x) dx &amp;= \int u(x,0) \ \tilde{p}_0(x) dx
\end{align*}
$$
</div>

<p>All that remains is substituing back the Feynman-Kac formula for $u(x,0)$ and substituting $\tilde{p}_t = p_t Z_t$, which gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\int u(x,T) \ \tilde{p}_T(x) dx &amp;= \int \mathbb{E} \Big[ \Phi_T(x_T) \exp \left( \int_0^T g(x_s, s) ds \right) \Big| x_0 = x \Big] \ \tilde{p}_0(x) dx \\ 
Z_T \int u(x,T) \ p_T(x) dx &amp;= Z_0 \int \mathbb{E} \Big[ \Phi_T(x_T) \exp \left( \int_0^T g(x_s, s) ds \right) \Big| x_0 = x \Big] \ p_0(x) dx
\end{align*}
$$
</div>

<p>Since at $t=0$, we have $p_0(x) = q_0(x) \exp(\beta_0 r(x)) / Z_0$ with $\beta_0 = 0$ we get $p_0(x) = q_0(x) / Z_0$ with $Z_0 = 1$, we can simplify the right hand side to get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
Z_T \int u(x,T) \ p_T(x) dx &amp;= \int \mathbb{E} \Big[ \Phi_T(x_T) \exp \left( \int_0^T g(x_s, s) ds \right) \Big| x_0 = x \Big] \ q_0(x) dx \\
Z_T \mathbb{E}_{p_T} [ u(x,T)] &amp;= \mathbb{E}_{q_0} \Big[ \mathbb{E} \Big[ \Phi_T(x_T) \exp \left( \int_0^T g(x_s, s) ds \right) \Big| x_0 = x \Big] \Big] \\
\mathbb{E}_{p_T} [ u(x,T)] &amp;= \frac{1}{Z_T} \mathbb{E} \Big[ \Phi_T(x_T) \exp \left( \int_0^T g(x_s, s) ds \right) \Big| x_0 = x \Big] \\
\end{align*}
$$
</div>

<p>This is pure importance sampling on a trajectory level, where we sample trajectories from the original SDE, calculate $g(x_s, s)$ along the trajectory and simply integrating it in log space as a simple sum over the time steps.
In the <a href="https://ludwigwinkler.github.io/blog/ESSSMC/">SMC blog post</a> we saw that we can use the weights from SMC to estimate $\hat{Z}_T = 1/K \sum_k^K \exp(\int_0^T g(x,s) ds)$, which gives us a practical way to compute the expectation of $u(x,T)$ under the tilted distribution $p_T$ by sampling trajectories from the original SDE and weighting them according to the reward tilt encapsulated in $g$.</p>

<p><img src="/blog/FKC/importance_weights.gif" alt="Importance Weighting of Generated Samples" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" /></p>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Correct your steps.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/blog/FKC/steered_diffusion.gif" /><media:content medium="image" url="/blog/FKC/steered_diffusion.gif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pacific Northwest</title><link href="/blog/Seattle26/" rel="alternate" type="text/html" title="Pacific Northwest" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>/blog/Seattle26</id><content type="html" xml:base="/blog/Seattle26/"><![CDATA[<!-- ## Berlin Over The Years -->

<style>
    .image-gallery {
        overflow: auto;
        margin-left: -1% !important;
    }

    .image-gallery li {
        float: left;
        float: top;
        display: block;
        margin: 0 0 1% 1%;
        width: 99%;
    }

    .image-gallery li a {
        text-align: top;
        text-decoration: none !important;
        color: #777;
    }

    .image-gallery li a span {
        display: block;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        padding: 3px 0;
    }

    .image-gallery li a img {
        width: 100%;
        height: 100%;
        display: flex;
        vertical-align: top;
    }
</style>

<ul class="image-gallery">
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC4878.jpg"><img src="/photo_gallery/seattle26/DSC4878.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC5055.jpg"><img src="/photo_gallery/seattle26/DSC5055.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC5100.jpg"><img src="/photo_gallery/seattle26/DSC5100.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC5132-Pano.jpg"><img src="/photo_gallery/seattle26/DSC5132-Pano.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC5200.jpg"><img src="/photo_gallery/seattle26/DSC5200.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC5239.jpg"><img src="/photo_gallery/seattle26/DSC5239.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/seattle26/DSC62681.jpg"><img src="/photo_gallery/seattle26/DSC62681.jpg" /></a></li>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
</ul>

<!--  -->

<!--  -->
<p><!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 --></p>]]></content><author><name></name></author><category term="photography" /><summary type="html"><![CDATA[Upper Left America]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/photo_gallery/seattle26/DSC5239.jpg" /><media:content medium="image" url="/photo_gallery/seattle26/DSC5239.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Weight of Sampling</title><link href="/blog/ESSSMC/" rel="alternate" type="text/html" title="The Weight of Sampling" /><published>2026-02-12T00:00:00+00:00</published><updated>2026-02-12T00:00:00+00:00</updated><id>/blog/ESSSMC</id><content type="html" xml:base="/blog/ESSSMC/"><![CDATA[<script>
MathJax = {
  tex: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\\[','\\]']],
    processEscapes: true,
    tags: 'all'
  },
  options: {
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
  }
};
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async=""></script>

<h2 id="why-we-need-it">Why we need it</h2>

<p>Suppose you want to estimate $\mathbb{E}_p[f(X)]$ for some target distribution $p$. In a perfect world, you’d draw $N$ i.i.d. samples from $p$ and compute the sample mean. The variance of that estimator drops as $1/N$ — that’s what you learn in ML 101.</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_f = \frac{1}{N} \sum_{i=1}^{N} f(X_i) \quad \quad \text{where} \quad X_i \sim p \quad \quad \text{and} \quad \quad \mathbb{V}[\hat{\mu}_f] = \frac{1}{N} \sigma_f
\end{align*}
$$
</div>

<p>If we can draw i.i.d. samples from $p$, then there’s no real problem in computing the expectation.
But alas, we can’t always draw i.i.d. samples from $p$.</p>

<p>But sampling from $p$ is not always possible.
This can be for a variety of reasons, but one common reason is that the distribution $p$ is complex and high-dimensional and its normalization constant is not known.
Oftentimes, we only know $p$ up to a normalizing constant $p = \tilde{p} / Z$.
Without knowing $Z$, we can’t assign the correct probability to a point $x$ under $p(x)$.
For the same value of $\tilde{p}(x)$, the probability of $x$ under $p(x)$ is different if $Z$ is different.</p>

<p>The mathematical version of this predicament shows up everywhere.
Consider the workhorse of modern machine learning: Bayesian posterior inference.
You observe data $\mathcal{D}$ and want to reason about parameters $\theta$ under the posterior</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
p(\theta | \mathcal{D}) = \frac{p(\mathcal{D} | \theta)\, p(\theta)}{p(\mathcal{D})}, \quad \text{where} \quad p(\mathcal{D}) = \int p(\mathcal{D} | \theta)\, p(\theta)\, d\theta
\end{align*}
$$
</div>

<p>The numerator — likelihood times prior — you can evaluate pointwise, no problem.
But the denominator $p(\mathcal{D})$, the marginal likelihood, requires the probability of your entire dataset $\mathcal{D}$ under the model.</p>

<p>So either you somehow magically know the probability of every sample in your dataset $\mathcal{D}$ under the model, or you need to estimate it by defining every possible value of $\theta$ and evaluating the likelihood and prior for each of them.
Every possible value of $\theta$ for continuous parameters is an infinite number of values, so you’d need an infinite amount of computational resources to evaluate the likelihood and prior for each of them.</p>

<p>For anything beyond toy conjugate models, this integral is intractable: there is no closed-form solution and the dimensionality of $\theta$ makes brute-force numerical integration hopeless.
So you’re stuck with a <strong>distribution you can <em>evaluate</em> (up to a constant) but can’t <em>sample from</em> directly</strong>.
You know the shape of the landscape, but you have no way to generate points that are distributed according to it.</p>

<p>This is where sampling schemes come in.
But in practice, the samples these schemes produce are almost never i.i.d. from the target:</p>

<ul>
  <li><strong><a href="https://ludwigwinkler.github.io/blog/HMC/">MCMC</a></strong> produces samples that are serially correlated (each sample depends on the previous one).</li>
  <li><strong>Importance sampling</strong> draws from a proposal $q \neq p$, then corrects with weights — but those weights can be wildly uneven.</li>
  <li><strong>Particle filters</strong> resample with replacement, creating duplicate particles (see <a href="https://ludwigwinkler.github.io/blog/AISSMC/">Annealed Importance Sampling</a> for a related staged-sampling scheme).</li>
</ul>

<p>All of these schemes are ways to sample from $p$ even if you can’t sample from $p$ directly. In all these cases, your $N$ raw samples carry <em>less information</em> than $N$ truly independent samples from the OG $p$ would. You need a single number that answers: <strong>“how many i.i.d. samples is my collection of samples I proposed actually worth?”</strong> That number is the ESS.</p>

<p>Without it, you’re flying blind — you can’t meaningfully compare algorithms, diagnose poor mixing, set stopping criteria, or calibrate your confidence in Monte Carlo estimates.</p>

<h2 id="lets-get-the-equations-rolling">Let’s get the equations rolling</h2>

<h3 id="importance-sampling">Importance Sampling</h3>

<p>We want to compute:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mu = \mathbb{E}_p[f(X)] = \int f(x)\, p(x)\, dx
\end{align*}
$$
</div>

<p>We can’t sample from $p$ directly, so we draw $X_1, \dots, X_N \overset{\text{i.i.d.}}{\sim} q$ from a proposal distribution $q$, and use the importance sampling identity:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mu =\int \underbrace{\frac{q(x)}{q(x)}}_{=1} \ p(x) \ f(x) dx =  \int q(x) \frac{p(x)}{q(x)} q(x)\ f(x) dx = \mathbb{E}_q\!\left[w(X) \ f(X)\right]
\end{align*}
$$
</div>

<p>where $w(x) = p(x)/q(x)$ are the (unnormalized) importance weights.</p>

<p>Below you can see the importance weights $w(x) = p(x)/q(x)$ for a Gaussian mixture model from the cover.
If we sample from $q$ we have to take into consideration that we’re not sampling from $p$ directly.
The correction for sampling from $q$ but wanting to estimate the expectation under $p$ is given by the importance weights $w(x) = p(x)/q(x)$.
For all examples in the visualization below, $p(x) &lt; q(x)$, such that the importance weights will all be smaller than 1.
These weights $w(x) &lt; 1$, will thus downweight the contribution of the samples from $q$ in the expectation estimator,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_f = \frac{1}{N} \sum_{i=1}^{N} w(X_i)\, f(X_i)
\end{align*}
$$
</div>

<p>whereas parts where $p(x) &gt; q(x)$, the importance weights will be greater than 1, thus upweighting the contribution of the samples from $q$ in the expectation estimator.</p>

<p><img src="/blog/ESS/weights.png" alt="Importance Sampling" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" /></p>

<h4 id="deriving-the-self-normalized-estimator">Deriving the self-normalized estimator</h4>

<h5 id="the-problem-with-standard-importance-sampling">The problem with standard importance sampling</h5>

<p>If we know $p$ exactly (including its normalizing constant), life is simple. The <strong>standard IS estimator</strong> is:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_{\text{IS}} = \frac{1}{N} \sum_{i=1}^{N} w(X_i)\, f(X_i), \qquad w(x) = \frac{p(x)}{q(x)}
\end{align*}
$$
</div>

<p>This is unbiased:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}_q[\hat{\mu}_{\text{IS}}] = \mathbb{E}_q[w(X)f(X)] = \int f(x)\frac{p(x)}{q(x)}q(x)\,dx = \int f(x)\,p(x)\,dx = \mu
\end{align*}
$$
</div>

<p>But in practice — especially in Bayesian inference — we only know $p$ up to a normalizing constant $p = \tilde{p} / Z$. 
While we’re interested in estimating $w = p / q$, we’re left holding the bag with its unnormalized cousin where $Z$ is unknown,</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
w(x) = \frac{p(x)}{q(x)} = \frac{1}{Z}\frac{\tilde{p}(x)}{q(x)} = \frac{1}{Z} \tilde{w}(x)
\end{align*}
$$
</div>

<p>which makes us arrive again at the same spot we started from that we don’t know $Z$ in the first place.
But with unnormalized weights $\tilde{w}$, there is a trick we can pull to bootstrap ourselves out of this seemingly dead end.</p>

<p>The key observation is that the normalizing constant $Z$ is itself is an integral over the whole space $x$ of $\tilde{p}(x)$.
We can therefore use importance sampling to evaluate the integral as in</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
Z = \int \tilde{p}(x)\,dx =  \int \frac{q(x)}{q(x)} \tilde{p}(x) = \int \ q(x) \ \frac{\tilde{p}(x)}{q(x)}\,dx = \int q(x) \ \tilde{w}(x) dx = \mathbb{E}_q[\tilde{w}(X)]
\end{align*}
$$
</div>

<p>Given that we’ve generated the samples $x \sim q$ proportionally to the distribution $q$ by definition, the expectation reduces to a simple average over the samples $x$,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{Z} = \frac{1}{N}\sum_{i=1}^{N} \tilde{w}(X_i) \quad \quad ; \quad \quad X_i \sim q
\end{align*}
$$
</div>

<p>Going back to our original objective we want to identify</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_{\text{IS}} &amp;= \frac{1}{N} \sum_{i=1}^{N} w(X_i)\, f(X_i) \\
&amp;= \frac{1}{N} \sum_{i=1}^{N} \frac{p(X_i)}{q(X_i)}\, f(X_i) \\
&amp;=\frac{1}{N} \sum_{i=1}^{N} \frac{1}{Z}\frac{\tilde{p}(X_i)}{q(X_i)}\, f(X_i) \\
&amp;\approx \frac{1}{N}  \frac{1}{\hat{Z}} \sum_{i=1}^{N} \tilde{w}(X_i)\, f(X_i) \\
&amp;= \frac{1}{N}  \frac{1}{\frac{1}{N}\sum_{i=j}^{N} \tilde{w}(X_j)} \sum_{i=1}^{N} \tilde{w}(X_i)\, f(X_i) \\
&amp;= \frac{\sum_{j=i}^{N} \tilde{w}(X_i)\, f(X_i)}{\sum_{j=i}^{N} \tilde{w}(X_j)} \\
\end{align*}
$$
</div>

<p>Whereas the importance estimator \(\hat{\mu}_{\text{IS}}\) assumes access to the target distribution $p$ including its normalization constant $Z$, using the unnormalized weights \(\tilde{w}\) to estimate \(\hat{Z}\) turns this estimator it to the <em>self-normalized</em> estimator \(\hat{\mu}_{\text{SN}}\).</p>

<p>We can simplify the equation for $\hat{\mu}_{\text{SN}}$ even further by observing that the sum in the denominator is self contained (as in sum over $j$), so we can write</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_{\text{SN}} 
&amp;= \frac{\sum_{i=1}^{N} \tilde{w}(X_i)\, f(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)} \\
&amp;= \sum_{i=1}^{N} \frac{\tilde{w}(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)}\, f(X_i) \\
&amp;= \sum_{i=1}^{N} \bar{w}(X_i)\, f(X_i) \\
\end{align*}
$$
</div>

<p>where the normalized weights $\bar{w}$ are defined as</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\bar{w}_i = \frac{\tilde{w}_i}{\sum_{j=1}^{N} \tilde{w}_j}
\end{align*}
$$
</div>

<h4 id="how-much-does-our-estimator-actually-vary">How much does our estimator actually vary?</h4>

<p>The self-normalized estimator $\hat{\mu}_{\text{SN}}$ is a ratio of two random quantities.
This makes its variance analysis more subtle than the simple i.i.d. case.</p>

<p>The key question is: how much does our estimate vary from one set of samples to the next?
If we draw $N$ samples from $q$ repeatedly, how much will $\hat{\mu}_{\text{SN}}$ fluctuate around the true mean $\mu$?</p>

<p>Here’s a (somewhat) intuitive example: Let’s assume we want an estimate of the average height of people in Europe by sending $N$ random people a text message. Imagine you picking 100 random cell phone numbers and blasting off a text saying “Yo, how tall are you?”. These text message could arrive on a Greek island in the sun, in the eternal winter darkness of a Finnish village or in the land of the giants, Holland.</p>

<p>Problem is, you don’t know how tall nor how representative the person reading the text is for the average height of people in Europe.
Ideally, you want to representatively sample from all countries in Europe from all height groups: the small people in Holland, the tall and small people in Portugal and the average tall people in Greece.</p>

<p>The variance of your little survey depends on two factors:</p>
<ol>
  <li>The intrinsically different heights in Europe that exists regardless of who you pick. Every country has differently tall people.</li>
  <li>Who you send the text to in Europe from your 100 allowed text messages.</li>
</ol>

<p>Particularly the second source of variability is tricky in our case. 
Did you send the text message it by accident to all Dutch men measuring 2.14m? Then your average is like 2.05m. Hardly realistic.
Or maybe you send it to all Greek grandmas towering at a mighty 1.55m. Also hardly representative.</p>

<p>In more mathy terms we would say that the variance of $\hat{\mu}_{\text{SN}}$ depends on two factors:</p>
<ol>
  <li>The intrinsic variability of $f(X)$ under the target distribution $p$ captured by $\sigma_f^2 = \text{Var}_p[f(X)]$. (the natural distribution of heights in Europe)</li>
  <li>The mismatch between proposal $q$ and target $p$, reflected in the weight variability. ($q$ being the proposed people you send your height text message to)</li>
</ol>

<p>When $q$ is a poor approximation to $p$, a few samples will have very large weights while most have tiny weights.
For simplicites sake assume that $q$ is a uniform $q=1/100$.
The probability of picking the tiny Greek grandmas or the super tall male Dutchies is very small as their heights are at the far ends of the height distribution in Europe. The average height is around 1.75m, so picking a height of 1.55m or 2.14m is very unlikely.</p>

<p>It’s important to note that we’re self-normalizing the weights $\tilde{w}_i$ with all our samples, so we have \(\bar{w}_i = \tilde{w} / \sum_{j=1}^{100} \tilde{w}_j\).</p>

<p>If we ask 98 Greek grandmas and towering Dutchies and only 2 slightly less extreme samples, the contribution of those extreme 98 weights will be negligible.
Remember that in order to compute the average weight we’ll compute,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_{\text{SN}} 
&amp;= \sum_{i=1}^{N} \frac{\tilde{w}(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)}\, f(X_i) \\
&amp;= \overbrace{\sum_{i=1}^{98} \underbrace{\frac{\tilde{w}(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)}}_{\approx 0}\, f(X_i)}^{\text{Unrepresentative Greek Grandmas \&amp; Tall Dutchies}} + \overbrace{\sum_{i=1}^{2} \frac{\tilde{w}(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)}\, f(X_i)}^{\text{effective number of samples}=2} \\
\end{align*}
$$
</div>

<p>But lo and behold, due to $p$ being very small, the weights of the 98 unrepresentative samples have a negligible contribution to the overall value.
In effect, we’re trying to estimate the average European height with two samples. Not ideal when we had assumed that we used 100 samples.</p>

<p>This weight concentration in those two meaningful samples means we’re effectively using far fewer than $N$ samples, inflating the estimator variance.
The <strong>effective sample size</strong> (ESS) quantifies exactly how many “equivalent i.i.d. samples” our weighted samples represent. In our case it’s two, not a hundred.</p>

<p>But how do we quantify this effective sample size?</p>

<h3 id="the-variance-of-our-estimator">The Variance of our Estimator</h3>

<p>Let’s denote the number of effective samples as $N_{\text{eff}}$.</p>

<p>If we had $N$ i.i.d. samples from $p$, the variance of the sample mean would be:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[\mu] = \mathbb{V}\!\left[\frac{1}{N}\sum_{i=1}^{N} f(X_i)\right] = \frac{1}{N^2} \sum_{i=0}^{N} \mathbb{V}[f(X)] = \frac{\sigma_f^2}{N}
\end{align*}
$$
</div>

<p>where $\sigma_f^2 = \text{Var}_p[f(X)]$.</p>

<p>But alas, we don’t have access to samples from $p$ because if we had, I wouldn’t have to write this blog post explaining ESS.</p>

<p>What we do have is our estimator $\hat{\mu}_{\text{SN}}$,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_{\text{SN}} 
&amp;= \frac{\sum_{i=1}^{N} \tilde{w}(X_i) \ f(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)} \\
\end{align*}
$$
</div>

<p>The variance is easy to set up, but actually a bit tricky to compute analytically as we’re dealing with a ratio of random variables</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[\hat{\mu}_{\text{SN}}]
&amp;= \mathbb{V}\left[ \frac{\sum_{i=1}^{N} \tilde{w}(X_i) \ f(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)} \right]
\end{align*}
$$
</div>

<h2 id="the-delta-method-or-everybodys-favourite-tool-linearization-with-taylor-expansions">The Delta Method (or <em>“Everybody’s Favourite Tool: Linearization with Taylor Expansions”</em>)</h2>

<p>Imagine you have two random variables, say $X$ and $Y$, and you care about the ratio $g(X, Y) = X/Y$. You know the variances and covariance of $X$ and $Y$, but what’s the variance of the ratio? That’s awkward because the ratio is nonlinear — variances don’t just “pass through” nonlinear functions.</p>

<p>The delta method’s trick is simple. All we gonna do is <strong>locally linearize</strong> the function via a first-order Taylor expansion around the means, then compute the variance of that linear approximation. Since variance behaves nicely under linear transforms, you get a clean closed-form result (albeit an only locally valid result due to the linearization).</p>

<p>Let $g(X, Y) = X / Y$.</p>

<p>Let’s start with the one-dimensional case to build intuition. The Taylor expansion of a smooth function $g(X)$ around a point $\mu_X$ is:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
g(X) = \sum_{n=0}^{\infty} \frac{g^{(n)}(\mu_X)}{n!} \cdot (X - \mu_X)^n
\end{align*}
$$
</div>

<p>Now let’s extend this to two dimensions. The exact formula is actually bit complicated for the infinite sum but the practically interesting parts are</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
g(X, Y) &amp;= g(\mu_X, \mu_Y) \\
&amp;\quad + \frac{\partial g}{\partial X}\bigg|_{\mu} (X - \mu_X) + \frac{\partial g}{\partial Y}\bigg|_{\mu} (Y - \mu_Y) + \\
&amp;\quad + \frac{1}{2}\frac{\partial^2 g}{\partial X^2}\bigg|_{\mu} (X - \mu_X)^2 + \frac{\partial^2 g}{\partial X \partial Y}\bigg|_{\mu} (X - \mu_X)(Y - \mu_Y) + \frac{1}{2}\frac{\partial^2 g}{\partial Y^2}\bigg|_{\mu} (Y - \mu_Y)^2 \\
\end{align*}
$$
</div>

<p>Only using the first order yields the truncated version in the form of</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
g(X, Y) &amp;= g(\mu_X, \mu_Y) + \frac{\partial g}{\partial X}\bigg|_{\mu} (X - \mu_X) + \frac{\partial g}{\partial Y}\bigg|_{\mu} (Y - \mu_Y)
\end{align*}
$$
</div>

<p>For the delta method, we keep only the <strong>first-order</strong> term (linearization):</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
g(X, Y) \approx g(\mu_X) + g_X(\mu_X) \cdot (X - \mu_X) + g_Y(\mu_Y) \cdot (Y - \mu_Y)
\end{align*}
$$
</div>

<p>Taking the variance of both sides (and noting that $\mu_X , \mu_Y$ is a constant so $\mathbb{V}[\mu_X] = \mathbb{V}[\mu_Y] = 0$), we have</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[g(X,Y)] \approx [g_X(\mu_X)]^2 \cdot \mathbb{V}[X] + [g_Y(\mu_Y)]^2 \cdot \mathbb{V}[Y] + 2 \ g_X \cdot g_Y \cdot \mathbb{C}[X,Y]
\end{align*}
$$
</div>

<p>where the last term $\mathbb{C}[X, Y]$ is the covariance between $X$ and $Y$ as they might or might not be correlated.</p>

<p>The required partial derivatives are:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
g_X = \frac{\partial}{\partial X}\frac{X}{Y}\bigg|_{\mu} = \frac{1}{\mu_Y}, \qquad g_Y = \frac{\partial}{\partial Y}\frac{X}{Y}\bigg|_{\mu} = -\frac{\mu_X}{\mu_Y^2}
\end{align*}
$$
</div>

<!-- Now take the variance of the linear approximation:

$$\mathbb{V}\!\left[\frac{X}{Y}\right] \approx g_X^2 \,\mathbb{V}[X] + g_Y^2 \,\mathbb{V}[Y] + 2\, g_X\, g_Y \,\mathbb{C}[X, Y]$$ -->

<p>Substituting:</p>

<div style="overflow-x: auto;">
$$
\mathbb{V}\left[\frac{X}{Y}\right] 
\approx \frac{1}{\mu_Y^2}\left(\mathbb{V}[X] + \frac{\mu_X^2}{\mu_Y^2}\,\mathbb{V}[Y] - 2\frac{\mu_X}{\mu_Y}\,\mathbb{C}[X, Y]\right)
$$
</div>

<p>Naturally, this approximation degrades when $\mu_Y$ is close to zero since we’re effectively dividing by something close to zero or when the distributions have heavy tails.
In ML contexts (e.g., variance of a metric ratio in A/B testing), if our samples are large enough for CLT to kick in, this works extremely well.</p>

<p>For our case, we would have</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{\mu}_{\text{SN}}
= \frac{\sum_{i=1}^{N} \tilde{w}(X_i) \ f(X_i)}{\sum_{j=1}^{N} \tilde{w}(X_j)}
= \frac{\frac{1}{N}\sum_{i=1}^{N} \tilde{w}(X_i) \ f(X_i)}{\frac{1}{N}\sum_{j=1}^{N} \tilde{w}(X_j)} 
\stackrel{N \rightarrow \infty}{=} \frac{\mathbb{E}_q[ \tilde{w}(X_i) \ f(X_i)]}{\mathbb{E}_q[ \tilde{w}(X_j)]} 
= \frac{"X"}{"Y"}
\end{align*}
$$
</div>

<h3 id="linearizing-the-ratio-in-the-variance-estimator">Linearizing the Ratio in the Variance Estimator</h3>

<p>For the linearization to be applicable we need the expected values and the variances of the numerator and denominator in our self-normalizing importance sampler.</p>

<p>First up we need to compute the mean and the variance of numerator and denominator</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}_p[f] &amp;= \mu_f \\
\mathbb{E}_q[\tilde{w} \ f] &amp;= \ldots \text{nothing to simplify here} &amp; \quad \leftarrow \mu_X  \\
\mathbb{V}_q[\tilde{w} \ f] &amp;= \mathbb{E}[\tilde{w}^2 \ f^2] - \mathbb{E}[\tilde{w} \ f]^2 &amp; \quad \leftarrow \mathbb{V}[X] \\
\mathbb{E}_q[\tilde{w}] &amp;= \int q \ \tilde{w} \ dx = \int q \ \frac{\tilde{p}}{q} \ dx = \int \tilde{p} \ dx = Z &amp; \quad \leftarrow \mu_Y \\
\mathbb{V}_q[\tilde{w}] &amp;= \mathbb{E}_q[\tilde{w}^2] - \mathbb{E}_q[\tilde{w}]^2 
= \mathbb{E}_q[\tilde{w}^2] - Z^2 &amp; \quad \leftarrow \mathbb{V}[Y]\\
\frac{\mathbb{E}_q[\tilde{w} \ f]}{\mathbb{E}_q[\tilde{w}]} 
&amp;= \frac{\mathbb{E}_q[\tilde{w} \ f]}{Z} 
= \mathbb{E}_q\left[\frac{1}{Z}\tilde{w} \ f\right] \\
&amp;= \mathbb{E}_q\left[\frac{1}{Z}\frac{\tilde{p}}{q} \ f\right] 
= \int q \ \frac{p}{q} \ f \ dx \\
&amp;= \mathbb{E}_p[f] \\
&amp;= \mu_f &amp; \quad \leftarrow \frac{\mu_X}{\mu_Y}\\
\mathbb{C}[\tilde{w} \ f, \tilde{w}] 
&amp;= \mathbb{E}_q[\tilde{w} \ f \ \tilde{w}] - \mathbb{E}_q[f] \mathbb{E}_q[\tilde{w}] \\
&amp;= \mathbb{E}_q[\tilde{w}^2 \ f ] - \mu_f \ Z &amp;\quad \leftarrow \mathbb{C}[X, Y]\\
\end{align*}
$$
</div>

<p>All of the expectations and variances above are with respect to a random variable $X$ and it’s functions $\tilde{w}(X)$ or $f(X)$.
The expectation and variance operator are both linear for a sum of random variables, so we can generically write</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}\left[\frac{1}{N}\sum_i X_i'\right] &amp;= \frac{1}{N}\sum_i \mathbb{E}[X_i'] = \mathbb{E}\left[ X'_i\right] \\
\mathbb{V}\left[\frac{1}{N}\sum_i X_i'\right] &amp;= \frac{1}{N^2}\sum_i \mathbb{V}[X_i'] = \frac{1}{N}\mathbb{V}\left[ X'_i\right] \\
\mathbb{C}\left[\frac{1}{N}\sum_i W'_i X_i' \ , \ \frac{1}{N}\sum_i X_i' \right]
&amp;= \mathbb{E}\left[\frac{1}{N}\sum_i W'_i X_i' \ \frac{1}{N}\sum_j W'_j \right] - \mathbb{E}\left[\frac{1}{N}\sum_i X_i'\right] \mathbb{E}\left[\frac{1}{N}\sum_i W'_i X_i'\right] \\
&amp;= \frac{1}{N^2}\left( \sum_i \mathbb{E}[ W'_i X_i' \ W'_i] + \sum_{i \neq j} \mathbb{E}[ W'_i X_i' \ W'_j] \right) - \mathbb{E}[W'_i X_i'] \ \mathbb{E}[W'_i] \\
&amp;= \frac{1}{N^2}\left( N \ \mathbb{E}[ W'_i X_i' \ W'_i] + N(N-1) \ \mathbb{E}[ W'_i X_i'] \ \mathbb{E}[ W'_j] \right) - \mathbb{E}[W'_i X_i'] \ \mathbb{E}[W'_i] \\
&amp;= \frac{1}{N}\left( \mathbb{E}[ W'_i X_i' \ W'_i] + (N-1) \mathbb{E}[ W'_i X_i'] \ \mathbb{E}[ W'_i] \right) - \mathbb{E}[W'_i X_i'] \ \mathbb{E}[W'_i] \\
&amp;= \frac{1}{N}\ \mathbb{E}[ W'_i X_i' \ W'_i] + \left(1-\frac{1}{N}\right) \mathbb{E}[ W'_i X_i'] \ \mathbb{E}[ W'_i] - \mathbb{E}[W'_i X_i'] \ \mathbb{E}[W'_i] \\
&amp;= \frac{1}{N}\ \mathbb{E}[ W'_i X_i' \ W'_i] + \frac{1}{N}\mathbb{E}[ W'_i X_i'] \ \mathbb{E}[ W'_i]
\end{align*}
$$
</div>

<p>For the covariance calculation we’ve exploited the fact that over $N$ samples, we assume that only samples with the same index are correlated, whereas samples with different indices are independent.</p>

<p>Since the terms in $\mathbb{V}[X/Y]$ are all variances, we have to simply add a $1/N$ factor to each term, if we choose to work with the sum instead of the mean. In any case, since the $1/N$ factor can be inserted in both the numerator and denominator, it won’t change the final result anyway.</p>

<p>Putting all of these terms together we get</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}\left[\frac{\sum \tilde{w} \ f}{\sum \tilde{w}}\right]
&amp;= \mathbb{V}\left[\frac{\frac{1}{N}\sum \tilde{w} \ f}{\frac{1}{N}\sum \tilde{w}}\right] \\
&amp;\approx \frac{1}{E[\frac{1}{N}\Sigma \ \tilde{w}]^2} \left( \mathbb{V}\left[\frac{1}{N} \sum \tilde{w} \ f\right] 
+ \frac{\mathbb{E}[\frac{1}{N} \sum \tilde{w} \ f]^2}{\mathbb{E}[\frac{1}{N} \sum \tilde{w}]^2} \mathbb{V}\left[\frac{1}{N} \sum \tilde{w}\right] 
- 2 \frac{\mathbb{E}[\frac{1}{N} \sum \tilde{w} \ f]}{\mathbb{E}[\frac{1}{N} \sum \tilde{w}]} \mathbb{C}\left[\frac{1}{N} \sum \tilde{w}, \frac{1}{N} \sum \tilde{w} \ f\right] \right)\\
&amp;= \frac{1}{E[ \tilde{w}]^2} \frac{1}{N} \left( \mathbb{V}\left[ \tilde{w} \ f\right] 
+ \frac{\mathbb{E}[\tilde{w} \ f]^2}{\mathbb{E}[\tilde{w}]^2} \mathbb{V}\left[\tilde{w}\right] 
- 2 \frac{\mathbb{E}[\tilde{w} \ f]}{\mathbb{E}[\tilde{w}]} \mathbb{C}\left[\tilde{w},\tilde{w} \ f\right] \right)\\
&amp;= \overbrace{\frac{1}{Z^2}}^{\frac{1}{\mu_Y^2}}\frac{1}{N} \Big( \overbrace{\mathbb{E}[\tilde{w}^2 \ f^2] - \mathbb{E}[\tilde{w} \ f]^2}^{\mathbb{V}[X]} + \underbrace{\mathbb{E}_p[f]^2}_{\frac{\mu_X^2}{\mu_Y^2}} \big(\underbrace{\mathbb{E}_q[\tilde{w}^2] - Z^2}_{\mathbb{V}[Y]} \big) - 2 \ \underbrace{\mathbb{E}_p[f]}_{\frac{\mu_X}{\mu_Y}} \big(\underbrace{\mathbb{E}_q[\tilde{w}^2 \ f] -  \mathbb{E}_q[\tilde{w}] Z}_{\mathbb{C}[X, Y]} \big)\Big) \\
&amp;= \frac{1}{Z^2}\frac{1}{N} \Big( \mathbb{E}[\tilde{w}^2 \ f^2] - \underbrace{\frac{Z^2}{Z^2} \mathbb{E}[\tilde{w} \ f]^2}_{=Z^2 \ \mathbb{E}_p[f]} 
+ \mathbb{E}_p[f]^2 \big(\mathbb{E}_q[\tilde{w}^2] - Z^2\big) 
- 2 \ \mathbb{E}_p[f] \big(\mathbb{E}_q[\tilde{w}^2 \ f] - \underbrace{\frac{Z}{Z} \mathbb{E}_q[\tilde{w}] Z}_{=Z^2 \mathbb{E}_p[f]}\big)\Big) \\
&amp;= \frac{1}{Z^2}\frac{1}{N} \Big( \mathbb{E}[\tilde{w}^2 \ f^2] \ \cancel{- Z^2 \ \mathbb{E}_p[f]} 
+ \mathbb{E}_p[f]^2 \big(\mathbb{E}_q[\tilde{w}^2] \ \cancel{- Z^2} \big) 
- 2 \ \mathbb{E}_p[f] \big(\mathbb{E}_q[\tilde{w}^2 \ f] \cancel{- Z^2 \mathbb{E}_p[f]}\big)\Big) \\
&amp;= \frac{1}{Z^2}\frac{1}{N} \Big( \mathbb{E}[\tilde{w}^2 \ f^2] 
+ \mu_f^2 \ \mathbb{E}_q[\tilde{w}^2] 
- 2 \ \mu_f \ \mathbb{E}_q[\tilde{w}^2 \ f] \Big) \\
&amp;= \frac{1}{Z^2}\frac{1}{N} \Big( \mathbb{E}\left[\tilde{w}^2 \ f^2 + \mu_f^2 \ \tilde{w}^2 - 2 \ \mu_f \ \tilde{w}^2 \ f\right] \Big) \\
&amp;= \frac{1}{Z^2}\frac{1}{N} \Big( \mathbb{E}\left[\tilde{w}^2 \ \left( f^2 + \mu_f^2 - 2 \ \mu_f \ f\right)\right] \Big) \\
&amp;= \frac{1}{Z^2}\frac{1}{N} \Big( \mathbb{E}\left[\tilde{w}^2 \ \left( f - \mu_f\right)^2\right] \Big) \\
&amp;= \frac{1}{N} \frac{\mathbb{E}\left[\tilde{w}^2 \ \left( f - \mu_f\right)^2\right]}{\mathbb{E}[\tilde{w}]^2} \\
\end{align*}
$$
</div>

<p>The equation above is an approximation of the variance of the self-normalized estimator.
If we sample the samples from a proposal distribution $q$ that is different from the unnormalized target distribution $\tilde{p}$ resulting in the weights $\tilde{w}$.</p>

<p>But what if we wouldn’t sample from $q$ but from the true target distribution $p$ resulting in the weights $w$?
This implies that we can really generate $N_p$ samples from $p$ directly, and thus our samples will be i.i.d. from $p$, not suffering from the approximation error of $q$ to $p$.
Let’s calculate the variance of the self-normalized estimator if we were to sample from $p$ directly,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}_p\left[\frac{\sum \tilde{w} \ f}{\sum \tilde{w}}\right]
&amp;= \frac{1}{N_p} \frac{\mathbb{E}_p\left[\tilde{w}^2 \ \left( f - \mu_f\right)^2\right]}{\mathbb{E}_p[\tilde{w}]^2} \\
&amp;= \frac{1}{N_p} \frac{\mathbb{E}_p\left[\tilde{w}^2 \ \left( f - \mu_f\right)^2\right]}{\underbrace{\int p \frac{\tilde{p}}{p}\ dx^2}_{=Z^2}} \\
&amp;= \frac{1}{N_p} \mathbb{E}_p\left[\frac{1}{Z^2}\tilde{w}^2 \ \left( f - \mu_f\right)^2\right]\\
&amp;= \frac{1}{N_p} \mathbb{E}_p\left[\frac{1}{Z^2}\frac{\tilde{p}^2}{p^2} \ \left( f - \mu_f\right)^2\right]\\
&amp;= \frac{1}{N_p} \mathbb{E}_p\left[ \ \left( f - \mu_f\right)^2\right]\\
\end{align*}
$$
</div>

<p>And thus we can see that the variance is actually decreasing with the number of samples $N_p$ from $p$ directly as we would expect from a standard i.i.d. estimator.
If $N_p$ is large, we consequently decrease the inherent variance of the estimator by a factor of $N_p$.</p>

<p>But again, we can only sample from $q$, not from $p$.
But we can pose a clever question: How many samples $N_q$ from $q$ would we need to get the same variance as $N_p$ samples from $p$?
At the very beginning we said that our correlated samples from $q$ carry less information than $N_p$ truly independent samples would.
So the ESS metric asks the question how many samples $N_q$ from $q$ are worth $N_p$ perfect, i.i.d, super-duper-excellent samples from $p$?</p>

<p>In order to solidify this, we’ll set the two variance estimators equal to each other, one with $N_p$ samples and one with $N_q$ samples.
\(\begin{align*}
\mathbb{V}_p\left[\frac{\sum \tilde{w} \ f}{\sum \tilde{w}}\right]
&amp;= \mathbb{V}_q\left[\frac{\sum \tilde{w} \ f}{\sum \tilde{w}}\right] \\
\frac{1}{N_p} \mathbb{E}_p\left[ \ \left( f - \mu_f\right)^2\right] 
&amp;= \frac{1}{N_q} \frac{\mathbb{E}_q\left[ \tilde{w}^2 \ \left( f - \mu_f\right)^2\right]}{\mathbb{E}_q[\tilde{w}]^2} \\
\end{align*}\)</p>

<p>In order to arrive at the ESS formula, we’ll need to do a final approximation by assuming that the weights $\tilde{w}^2$ actually aren’t too correlated with $(f - \mu_f)^2$ which is in practice actually somewhat true,</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\frac{1}{N_p} \mathbb{E}_p\left[ \ \left( f - \mu_f\right)^2\right] 
&amp;\approx \frac{1}{N_q} \frac{\mathbb{E}_q\left[ \tilde{w}^2 \right] \ \mathbb{E}_p \left[ \left( f - \mu_f\right)^2\right]}{\mathbb{E}_q[\tilde{w}]^2} \\
\frac{1}{N_p} 
&amp;\approx \frac{1}{N_q} \frac{\mathbb{E}_q\left[ \tilde{w}^2 \right] }{\mathbb{E}_q[\tilde{w}]^2} \\
N_q 
&amp;\approx N_p \cdot \frac{\mathbb{E}_q[\tilde{w}]^2}{\mathbb{E}_q\left[ \tilde{w}^2 \right] } \\
\end{align*}
$$
</div>

<p>The samples $N_q$ are then the effective number of samples from $q$ that are worth $N_p$ samples from $p$.
The ratio $\frac{\mathbb{E}_q\left[ \tilde{w}^2 \right] }{\mathbb{E}_q[\tilde{w}]^2}$ can intuitively be interpreted as a discount factor.
You can quite literally think of the discount factor like a conversion rate between the number of samples from $q$ and the number of samples from $p$ that are worth the same.
This is really akin to conversion factors between currencies.
Take $N_q$ Zimbabwean dollars and try to convert them to $N_p$ US dollars. Since Zimbabwean dollars <a href="https://en.wikipedia.org/wiki/Hyperinflation_in_Zimbabwe">aren’t worth anything</a>, you got like 0.000001 Dollar per Zimbabwean dollar.
So you need an inordinate amount of Zimbabwean dollars to get a single $N_p=1$ US dollar.</p>

<p>We can also express the effective sampling size in terms of normalized weights $\tilde{w}$ instead of $\tilde{w}^2$.
Remember that</p>

<div style="overflow-x: auto;">
$$\begin{align*}
\frac{1}{N} \sum_{i=1}^{N} \tilde{w}_i &amp;= \mathbb{E}_q[\tilde{w}] = \int q \ \tilde{w} \ dx = \int q \ \frac{\tilde{p}}{q} \ dx = \int \tilde{p} \ dx = Z \\
&amp; \downarrow \\
\sum_{i=1}^{N} \tilde{w}_i &amp;= N \ Z
\end{align*}
$$
</div>

<div style="overflow-x: auto;">
$$
\begin{align*}
N_q 
&amp;\approx N \ \frac{\mathbb{E}_q[\tilde{w}]^2}{\mathbb{E}_q\left[ \tilde{w}^2 \right] } \\
&amp;\approx N \ \frac{(\frac{1}{N} \sum_i \tilde{w}_i)^2}{\frac{1}{N} \sum_i \tilde{w}_i^2} \\
&amp;\approx N^2 \ \frac{\frac{1}{N^2} (\sum_i \tilde{w}_i)^2}{\sum_i \tilde{w}_i^2} \\
&amp;= \frac{(\sum_i \tilde{w}_i)^2}{\sum_i \tilde{w}_i^2} \\
&amp;= \frac{1}{\sum_i \frac{1}{(\sum_j^N \tilde{w}_j)^2}  \tilde{w}_i^2}  &amp; \quad \quad \text{with} \ \bar{w}_i = \frac{\tilde{w}_i}{\sum_j^N \tilde{w}_j} \\
&amp;= \frac{1}{\sum_i \bar{w}_i^2} \\
\end{align*}
$$
</div>

<p>This gives us an easy way to calculate the ESS purely from the normalized weights $\tilde{w}_i$.</p>

<h3 id="mcmc">MCMC</h3>

<p>In the importance sampling case, the problem was that we couldn’t sample from $p$ and had to use a proposal $q$, leading to weights that could be wildly uneven.
MCMC flips the script entirely: we <em>are</em> sampling from $p$ (at least asymptotically), but our samples are <strong>serially correlated</strong> because each sample depends on the previous one.</p>

<p>Think of it this way: if importance sampling suffers from “some samples mattering way more than others”, MCMC suffers from “consecutive samples being almost the same thing”.
You’re walking through the mountainous landscape we described at the very beginning, but you can only take small steps from where you currently are.
If you’re standing on a peak, your next few hundred positions will all be near that same peak — you haven’t explored the valleys yet.
Your chain might eventually visit the entire landscape, but <em>locally</em>, your samples are redundant.</p>

<p>So the question is the same one we’ve been asking all along: <strong>how many i.i.d. samples is our correlated chain actually worth?</strong></p>

<p>Let’s set up the math. Denote the sample mean of our MCMC chain as</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\bar{f}_N = \frac{1}{N}\sum_{t=1}^{N} f(X_t)
\end{align*}
$$
</div>

<p>where $X_1, X_2, \ldots, X_N$ are the states of our Markov chain targeting $p$.</p>

<p>We want $\mathbb{V}[\bar{f}_N]$. Let’s expand this directly from the definition:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[\bar{f}_N] = \mathbb{V}\!\left[\frac{1}{N}\sum_{t=1}^N f(X_t)\right] = \frac{1}{N^2}\,\mathbb{V}\!\left[\sum_{t=1}^N f(X_t)\right]
\end{align*}
$$
</div>

<p>Now here’s where the correlation makes life interesting.
What we’ve done is run a MCMC chain which always takes the last value $X_{t-1}$ as the starting point for the next step $X_t$.
So for all intents and purposes, the samples $X_t$ are correlated.
The variance of a sum is <em>not</em> just the sum of the variances when the summands are correlated. We have to account for all the cross-terms:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}\!\left[\sum_{t=1}^N f(X_t)\right] = \sum_{t=1}^N \sum_{s=1}^N \mathbb{C}[f(X_t), f(X_s)] = \underbrace{\sum_{t=1}^N \mathbb{V}[f(X_t)]}_{\text{diagonal } (t=s)} + \underbrace{\sum_{t=1}^N \sum_{\substack{s=1 \\ s \neq t}}^N \mathbb{C}[f(X_t), f(X_s)]}_{\text{off-diagonal } (t \neq s)}
\end{align*}
$$
</div>

<p>This is a double sum — every pair of samples $(t, s)$ contributes a covariance term. The diagonal terms ($t = s$) give us the familiar sum of variances, while the off-diagonal terms ($t \neq s$) capture the correlations between different samples. For i.i.d. samples, all the off-diagonal terms vanish because $\mathbb{C}[f(X_t), f(X_s)] = 0$ for $t \neq s$, and we’d recover</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}\!\left[\frac{1}{N}\sum_{t=1}^N f(X_t)\right] = \frac{1}{N^2}\sum_{t=1}^N \mathbb{V}[f(X_t)] = \frac{N\sigma_f^2}{N^2} = \frac{\sigma_f^2}{N}
\end{align*}
$$
</div>

<p>But for MCMC, they don’t vanish. Consecutive samples are correlated, and that correlation bleeds into the variance.</p>

<p>The saving grace is <strong>stationarity</strong>: if you run the chain long enough after which the chain is said to have “converged”, the covariance between $f(X_t)$ and $f(X_s)$ depends only on the lag $k = |t - s|$, not on the absolute positions $t$ and $s$.
With stationarity, this allows us define the <strong>autocovariance function</strong>:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
c_k = \mathbb{C}[f(X_t), f(X_{t+k})] = \mathbb{E}[(f(X_t) - \mu_f)(f(X_{t+k}) - \mu_f)]
\end{align*}
$$
</div>

<p>Note that $c_0 = \sigma_f^2$ (the marginal variance of $f$ under $p$, the same $\sigma_f^2$ we’ve been carrying around since the beginning) and $c_k = c_{-k}$ by symmetry — the covariance doesn’t care about the direction of time.</p>

<p>In the double sum $\sum_{t=1}^N \sum_{s=1}^N$, we need to count how many pairs $(t, s)$ produce each lag $k = t - s$. This is a straightforward combinatorial exercise:</p>

<ul>
  <li>Lag $k = 0$: pairs $(1,1), (2,2), \ldots, (N,N)$ → exactly $N$ terms</li>
  <li>Lag $k = 1$: pairs $(2,1), (3,2), \ldots, (N, N-1)$ → exactly $N-1$ terms</li>
  <li>Lag $k = -1$: pairs $(1,2), (2,3), \ldots, (N-1, N)$ → exactly $N-1$ terms</li>
  <li>In general, lag $k$: exactly $N - |k|$ terms, for $k = -(N-1), \ldots, N-1$</li>
</ul>

<p>This allows us to simplify the equation as for any fixed $k=t-s$, there are $N - |k|$ pairs $(t, s)$ that produce this lag with a fixed value of $c_k$ for the stationary/converged chain.
So the double sum collapses to</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\sum_{t=1}^N \sum_{s=1}^N \mathbb{C}[f(X_t), f(X_s)] = \sum_{k=-(N-1)}^{N-1} (N - |k|)\,c_k
\end{align*}
$$
</div>

<p>Using the symmetry $c_k = c_{-k}$, we fold the negative lags into the positive ones:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\sum_{t=1}^N \sum_{s=1}^N \mathbb{C}[f(X_t), f(X_s)]= Nc_0 + 2\sum_{k=1}^{N-1}(N-k)\,c_k
\end{align*}
$$
</div>

<p>Plugging this back into our variance expression:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[\bar{f}_N] &amp;= \frac{1}{N^2}\left[Nc_0 + 2\sum_{k=1}^{N-1}(N-k)\,c_k\right] \\
&amp;= \frac{c_0}{N} + \frac{2}{N^2}\sum_{k=1}^{N-1}(N-k)\,c_k \\
&amp;= \frac{c_0}{N}\left[1 + 2\sum_{k=1}^{N-1}\underbrace{\left(1 - \frac{k}{N}\right)}_{\text{Bartlett weight}}\frac{c_k}{c_0}\right]
\end{align*}
$$
</div>

<p>Defining the <strong>autocorrelation function</strong> $\rho_k = c_k / c_0$ (the normalized version of the autocovariance), we arrive at</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[\bar{f}_N] = \frac{\sigma_f^2}{N}\left[1 + 2\sum_{k=1}^{N-1}\left(1 - \frac{k}{N}\right)\rho_k\right]
\end{align*}
$$
</div>

<p>For i.i.d. samples, $\rho_k = 0$ for all $k \geq 1$, and the bracket collapses to $1$, recovering the familiar $\sigma_f^2/N$ we know and love.</p>

<p>The Bartlett weights $(1 - k/N)$ are worth pausing on. They arise purely from counting: at lag $k$, there are only $N - k$ pairs of samples separated by exactly $k$ steps. The further apart two samples are in the chain, the fewer such pairs exist. For large $N$, these weights are approximately $1$ for any fixed lag — the edge effects wash out.</p>

<p>Thus for large $N$, the Bartlett weights $(1 - k/N) \to 1$ for any fixed $k$. If the autocorrelations decay fast enough that $\sum_{k=1}^\infty \|\rho_k\| &lt; \infty$ — which holds for any geometrically ergodic chain, i.e. pretty much every well-behaved MCMC sampler you’ll encounter in practice — we can take the limit:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}[\bar{f}_N] \approx \frac{\sigma_f^2}{N}\left[1 + 2\sum_{k=1}^{\infty}\rho_k\right] = \frac{\sigma_f^2}{N}\cdot\tau_f
\end{align*}
$$
</div>

<p>where we’ve defined the <strong>integrated autocorrelation time (IAT)</strong>:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\tau_f = 1 + 2\sum_{k=1}^{\infty} \rho_k
\end{align*}
$$
</div>

<p>The name “integrated” comes from viewing this as the discrete analogue of integrating the continuous autocorrelation function. It collapses the entire memory structure of the chain into a single number.</p>

<p><strong>What does $\tau_f$ tell us?</strong></p>

<ul>
  <li>$\tau_f = 1$: no autocorrelation at all, we’re back in i.i.d. land because all $\rho_k = 0$ for $k \geq 1$</li>
  <li>$\tau_f = 10$: each sample carries only $1/10$-th the information of an independent draw</li>
  <li>$\tau_f = 100$: the chain is mixing like molasses — you need 100 steps to get one effective independent sample</li>
</ul>

<p>One subtlety worth flagging: notice the subscript $f$ on $\tau_f$. Unlike the importance sampling ESS we derived above — which is purely a function of the weights and therefore the same no matter what $f$ you’re estimating — the MCMC IAT is <strong>function-dependent</strong>. A chain might mix quickly for the mean of $f$ but slowly for its variance, or zip along in one direction of parameter space while crawling in another. This is a fundamental difference between the two flavors of ESS.</p>

<p>We now play exactly the same game as in the importance sampling case. We ask: how many i.i.d. samples $N_{\text{eff}}$ from $p$ would give us the same variance as our $N$ correlated MCMC samples?</p>

<p>The i.i.d. baseline is, as before:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{V}_{\text{i.i.d.}} = \frac{\sigma_f^2}{N_{\text{eff}}}
\end{align*}
$$
</div>

<p>Setting this equal to the MCMC variance:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\frac{\sigma_f^2}{N_{\text{eff}}} = \frac{\sigma_f^2}{N}\cdot \tau_f
\end{align*}
$$
</div>

<p>The $\sigma_f^2$ cancels — though this is a bit misleading, since $\tau_f$ itself depends on $f$, so the ESS is still function-dependent unlike the IS case:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
N_{\text{eff}} = \frac{N}{\tau_f} = \frac{N}{1 + 2\sum_{k=1}^{\infty}\rho_k}
\end{align*}
$$
</div>

<p>The logic is beautifully parallel to what we did for importance sampling. There, the “discount factor” was the weight variability $\mathbb{E}_q[\tilde{w}^2] / \mathbb{E}_q[\tilde{w}]^2$. Here, the discount factor is the integrated autocorrelation time $\tau_f$. Both tell you how many of your $N$ samples you’re <em>actually</em> using.</p>

<p>Going back to our currency analogy: if importance sampling was converting Zimbabwean dollars to US dollars (with the exchange rate determined by weight concentration), then MCMC is like converting a time series of correlated price observations to independent ones (with the exchange rate determined by how sticky the prices are). Either way, you’re asking: what’s my real purchasing power?</p>

<h3 id="diffusion-models-as-sequential-monte-carlo">Diffusion Models as Sequential Monte Carlo</h3>

<p>We’ve now built up the entire ESS toolkit: for importance sampling, it measures weight concentration; for MCMC, it measures autocorrelation-induced information loss. These two stories converge in one of the most consequential generative modeling frameworks of the last few years — <strong>diffusion models</strong> — where the denoising process can be interpreted as a sequential Monte Carlo sampler.</p>

<p>But before we get to diffusion models, let’s set up the general SMC framework. The whole point of SMC is to do inference in <strong>state-space models</strong> (also known as hidden Markov models in the discrete case), where we have:</p>

<ul>
  <li>A sequence of <strong>hidden states</strong> $x_0, x_1, \ldots, x_T$ that we can’t observe directly</li>
  <li>A <strong>prior</strong> over the initial state: $p(x_0)$</li>
  <li>A <strong>transition kernel</strong> $p(x_t | x_{t-1})$ describing how the hidden state evolves over time</li>
  <li>An <strong>emission model</strong> (also called the observation model) $p(y_t | x_t)$ describing how the observed data $y_t$ is generated from the hidden state</li>
</ul>

<p>The joint distribution over the full trajectory and observations factorizes as</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
p(x_{0:T}, y_{1:T}) = p(x_0) \prod_{t=1}^{T} p(x_t | x_{t-1}) \cdot \prod_{t=1}^{T} p(y_t | x_t)
\end{align*}
$$
</div>

<p>The goal is to compute the <strong>filtering distribution</strong> $p(x_t | y_{1:t})$ — what do we believe about the current hidden state, given everything we’ve observed so far? — or, more ambitiously, the <strong>smoothing distribution</strong> $p(x_{0:T} | y_{1:T})$ over the entire trajectory.</p>

<p>The emission probability $p(y_t | x_t)$ is the likelihood of the observed data $y_t$ given the hidden state $x_t$. 
In effect, it works as a critic of the trajectory $x_{0:t}$ that was sampled so far. 
At each step (but also maybe only at other steps) it evaluates the probability $p(y_t | x_{0:t})$ of the observed data $y_t$ given the trajectory $x_{0:t}$.
If you did a lot of steps that moved your samples $x_t$ into some part of space where the data $y_t$ is very unlikely, your weight will be low, and thus that particular sample path $x_{0:t}$ will be highly discouraged.</p>

<p>The trouble is that the corresponding analytical posteriors are intractable in general. The exact recursion involves integrating over all possible previous states at every step, and this integral doesn’t have a closed form except in special cases (linear-Gaussian dynamics give you the Kalman filter; finite state spaces give you the forward-backward algorithm).</p>

<p><strong>Sequential Monte Carlo</strong> approximates these intractable distributions using a weighted set of <strong>particles</strong> — sample trajectories ${x_{0:t}^{(i)}, w_t^{(i)}}_{i=1}^N$ that represent an empirical approximation to the posterior. The algorithm proceeds sequentially:</p>

<ol>
  <li><strong>Propagate</strong>: Move each particle forward in time by sampling from the transition kernel $x_t^{(i)} \sim p(x_t | x_{t-1}^{(i)})$</li>
  <li><strong>Weight</strong>: Assign each particle an importance weight based on the emission model $\tilde{w}_t^{(i)} = p(y_t | x_t^{(i)})$</li>
  <li><strong>Normalize</strong>: Compute $\bar{w}_t^{(i)} = \tilde{w}_t^{(i)} / \sum_j \tilde{w}_t^{(j)}$</li>
  <li><strong>Evaluate</strong>: Compute the ESS $\hat{N}_{\text{eff}} = 1 / \sum_i (\bar{w}_t^{(i)})^2$</li>
  <li><strong>Resample</strong>: If the ESS drops below a threshold, resample particles according to the weights — duplicate the promising ones, discard the dead weight — and reset all weights to $1/N$</li>
</ol>

<p>Essentially, you keep sampling trajectories and you keep using the “critic” to evaluate them by checking what the probability is that the trajectory would have produced the data $y_t$ that you observed.
This information is encapsulated in the weights $w_t^{(i)}$ that you assign to each trajectory.
If the weights become too low, this implies that your trajectories have become more and more unlikely for the “critic” $p(y_t | x_{0:t})$.
What do you do? You look at your trajectories and you decide to keep only the ones that are still likely, and you discard the ones that are not.
The normalized weights $\bar{w}_t^{(i)}$ can then be used as the probabilities for a categorical distribution that you use to sample new trajectories.</p>

<p>Now — what does any of this have to do with diffusion models?</p>

<p>For diffusion models, we have the generative <a href="https://ludwigwinkler.github.io/blog/SimpleReverseSDE/"><strong>reverse-time SDE</strong></a>:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
dx_t = \left[f(x_t, t) - g^2(t)\, \nabla_{x_t} \log p_t(x_t)\right] dt + g(t)\, dW_t
\end{align*}
$$
</div>

<p>where $W_t$ is a reverse-time Wiener process and $\nabla_{x_t} \log p_t(x_t)$ is the <strong>score function</strong> — the gradient of the log-density at time $t$. This is the quantity a diffusion model learns: a neural network $s_\theta(x_t, t) \approx \nabla_{x_t} \log p_t(x_t)$.</p>

<p>To actually generate samples, we discretize the reverse SDE with an Euler-Maruyama step of size $\Delta t$:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
x_{t - \Delta t} = x_t + \left[f(x_t, t) - g^2(t)\, s_\theta(x_t, t)\right] \Delta t + g(t)\sqrt{\Delta t}\, \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)
\end{align*}
$$
</div>

<p>This discretization <em>is</em> our <strong>transition kernel</strong> — reading it off as a Gaussian:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
p_\theta(x_{t-\Delta t} | x_t) = \mathcal{N}\!\left(x_{t-\Delta t};\, \underbrace{x_t + [f(x_t, t) - g^2(t)\, s_\theta(x_t, t)]\,\Delta t}_{\text{mean}},\, \underbrace{g^2(t)\,\Delta t}_{\text{variance}}\, I\right)
\end{align*}
$$
</div>

<p>This is the transition kernel that enters the SMC framework. Unconditional generation is then straightforward: sample $x_T \sim \mathcal{N}(0, I)$, apply $p_\theta(x_{t-\Delta t} | x_t)$ for each step backwards, and out pops a sample $x_0 \approx p_{\text{data}}$. This is just a Markov chain — no weights, no resampling, no ESS to speak of.</p>

<h4 id="the-one-step-denoiser-tweedies-formula">The one-step denoiser: Tweedie’s formula</h4>

<p>Before we get to SMC, we need one more ingredient: the “critic” $p(y_t | x_t)$. At any noise level $t$, the diffusion model implicitly gives us a <strong>one-step denoiser</strong> — a direct estimate of what the clean data $x_0$ looks like, given only the current noisy state $x_t$.</p>

<p>This comes from <strong>Tweedie’s formula</strong>. Given the forward marginal $q(x_t | x_0) = \mathcal{N}(x_t;\, \alpha(t)\, x_0,\, \sigma^2(t)\, I)$, the posterior mean of $x_0$ given $x_t$ is:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{x}_0(x_t) = \mathbb{E}[x_0 | x_t] = \frac{1}{\alpha(t)}\left(x_t + \sigma^2(t)\, \nabla_{x_t} \log p_t(x_t)\right)
\end{align*}
$$
</div>

<p>Since the diffusion model learns the score $s_\theta(x_t, t) \approx \nabla_{x_t} \log p_t(x_t)$, we get $\hat{x}_0(x_t)$ essentially for free — it’s just a different way of reading off the network output.</p>

<p>Think of $\hat{x}_0(x_t)$ as the model’s best guess of the clean image, peeking through the noise at step $t$. Early in the denoising process (large $t$, lots of noise), this guess is blurry and uncertain. Late in the process (small $t$, little noise), it’s sharp and confident.</p>

<p>We’ll call this one-step denoiser prediction the “observation” that the current noisy state $x_t$ produces — and denote the observation model as $p(y_t | x_t)$, where $y_t$ encodes what we can infer about the data from $x_t$.</p>

<h4 id="conditional-generation-enter-the-state-space-model">Conditional generation: enter the state-space model</h4>

<p>Now suppose we don’t just want any sample from $p_{\text{data}}$ — we want a sample that satisfies some condition $y$. Maybe we want an image of a golden retriever, or we want to inpaint a missing region, or we want a molecule with certain properties.</p>

<p>What we want is to sample from the <strong>conditional</strong> reverse process:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
p(x_{0:T} | y) \propto p(x_T) \prod_{t=1}^{T} p_\theta(x_{t-1} | x_t) \cdot p(y | x_0)
\end{align*}
$$
</div>

<p>The problem? The likelihood $p(y | x_0)$ only tells us about the final clean sample $x_0$. We don’t get any signal about whether our denoising trajectory is heading in the right direction until the very end. By then, it’s too late — all our computational budget is spent.</p>

<p>This is where the one-step denoiser saves the day. Instead of waiting until $x_0$ to evaluate $p(y | x_0)$, we can get an <em>approximate</em> per-step likelihood using the Tweedie estimate:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
p(y_t \| x_t) \approx p(y \| \hat{x}_0(x_t))
\end{align*}
$$
</div>

<p>We’re peeking at the model’s current best guess of the clean output and asking: “does this look like it satisfies the condition?”</p>

<p>And now we have all the pieces to write down a <strong>state-space model</strong> — exactly the structure we introduced in the general SMC framework above:</p>

<ul>
  <li><strong>Hidden states</strong> $x_T, x_{T-\Delta t}, \ldots, x_0$ — the noisy latents at each denoising step</li>
  <li><strong>Prior</strong> $p(x_T) = \mathcal{N}(0, I)$ — pure noise at the start of the reverse process</li>
  <li><strong>Transition kernel</strong> $p_\theta(x_{t-\Delta t} \| x_t)$ — the Euler-Maruyama discretization of the reverse SDE</li>
  <li><strong>Emission model</strong> $p(y_t | x_t) \approx p(y | \hat{x}_0(x_t))$ — how consistent is the current state with the desired condition, as estimated through the one-step denoiser</li>
</ul>

<p>This is <em>exactly</em> the structure we need for a particle filter.</p>

<p>Combining everything we get the following algorithm:</p>

<p><strong>Initialize:</strong> Draw $N$ particles from the prior (pure noise):</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
x_T^{(i)} \sim \mathcal{N}(0, I), \quad i = 1, \ldots, N
\end{align*}
$$
</div>

<p><strong>For each denoising step $t = T, T-\Delta t, \ldots, \Delta t$:</strong></p>

<p><strong>1. Propagate</strong> each particle through the transition kernel (one reverse SDE step):</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
x_{t-\Delta t}^{(i)} \sim p_\theta(x_{t-\Delta t} | x_t^{(i)})
\end{align*}
$$
</div>

<p><strong>2. Compute the unnormalized importance weights</strong> via the observation model:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\tilde{w}_t^{(i)} = p(y_t | x_t^{(i)}) = p(y | \hat{x}_0(x_t^{(i)}))
\end{align*}
$$
</div>

<p>The weight of each particle reflects how well its one-step denoiser prediction $\hat{x}_0(x_t^{(i)})$ matches the desired condition $y$.</p>

<p><strong>3. Normalize the weights:</strong></p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\bar{w}_t^{(i)} = \frac{\tilde{w}_t^{(i)}}{\sum_{j=1}^N \tilde{w}_t^{(j)}}
\end{align*}
$$
</div>

<p>Look familiar? These are exactly the self-normalized weights $\bar{w}_i = \tilde{w}_i / \sum_j \tilde{w}_j$ we spent the first half of this post deriving.</p>

<p><strong>4. Compute the ESS</strong> using the formula we derived:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\hat{N}_{\text{eff}} = \frac{1}{\sum_{i=1}^{N} (\bar{w}_t^{(i)})^2}
\end{align*}
$$
</div>

<p><strong>5. Resample if necessary:</strong> if $\hat{N}_{\text{eff}}$ drops below some threshold (a common choice is $N/2$), resample the particles according to the weights — duplicate the high-weight particles and discard the low-weight ones. After resampling, reset all weights to $1/N$ and continue denoising from the refreshed particle cloud.</p>

<h4 id="why-ess-matters-here-the-weight-degeneracy-problem">Why ESS matters here: the weight degeneracy problem</h4>

<p>And here’s where our entire ESS derivation pays off.</p>

<p>Remember the Dutch giants and Greek grandmas? In the diffusion context, the particles are like our text message recipients. Each particle $x_t^{(i)}$ is a different “hypothesis” for what the clean image looks like. Some particles, by chance, will be heading toward images that satisfy the condition $y$ — samples that score highly under the observation model. Others will be heading toward something completely different: samples that bear no resemblance to the desired condition, effectively dead weight in the particle cloud.</p>

<p>The weights $\tilde{w}_t^{(i)} = p(y | \hat{x}_0(x_t^{(i)}))$ quantify this mismatch: particles whose one-step denoiser prediction aligns well with the condition $y$ get large weights, while those drifting toward irrelevant regions of the sample space get negligible weights. If only 2 out of 100 particles are heading in the right direction, the ESS will be approximately 2 — just like our Dutch-and-Greek height survey.</p>

<p>Without resampling, the particle cloud degenerates: a handful of particles carry all the weight, and you’re effectively running the denoising process with just those few trajectories. The estimator variance explodes, and the quality of your conditional samples plummets.</p>

<p>Resampling — triggered when the ESS drops below a threshold — is the remedy. It prunes the dead-weight particles and replicates the promising ones, refreshing the particle cloud. But the price is that resampled particles are no longer independent (they share common ancestors), introducing the autocorrelation that we analyzed in the MCMC section. So we’re back to our old friend: correlated samples carrying less information than independent ones.</p>

<p>This is the fundamental tension of SMC: <strong>resampling fights weight degeneracy but creates sample correlation</strong>. The ESS, in both its importance-sampling and MCMC flavors, is the diagnostic that navigates this tradeoff.</p>

<h4 id="deriving-the-incremental-weights">Deriving the incremental weights</h4>

<p>So far we have the transition probabilities $p_\theta(x_{t-\Delta t} | x_t)$ — the reverse SDE that propagates our particles from noise toward data. But transitions alone just tell us <em>how</em> particles move; they say nothing about <em>which</em> particles are worth keeping. For that, we need the emission probabilities $p(y_t | x_t)$.</p>

<p>The idea is simple: we define a function that takes in a particle $x_t$ and returns a scalar value evaluating how well that particle’s trajectory aligns with the desired observation $y$. In the diffusion setting, we use the one-step denoiser to peek ahead: $p(y_t | x_t) \approx p(y | \hat{x}_0(x_t))$. This is our critic — it scores each particle at time $t$ by asking “if I were to denoise this particle all the way to $x_0$ right now, how well would the result match the condition $y$?”.
In the literature, this is often called the “potential function” and denoted by the function $G_t(x_t)$.</p>

<p>The whole point of tracking importance weights is to keep score of which samples are good and which are bad, so that we can resample accordingly — duplicate the promising trajectories and prune the hopeless ones. And for that resampling decision to be well-informed, we need the most up-to-date emission probability at every time step $t$, not just at the very end.</p>

<p>We can achieve exactly this with a telescoping product. Define the <strong>per-step potential function</strong> $G_t(x_t)$:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
G_t(x_t) = \frac{p(\hat{x}_0 \| x_t)}{p(\hat{x}_0 \| x_{t+\Delta t})}
\end{align*}
$$
</div>

<p>This is the ratio of the current emission score to the previous one — how much our assessment of the particle has changed in a single denoising step. Remember, we’re going from $T \to 0$, so the previous step is $t + \Delta t$. If the particle is making progress toward satisfying the condition, $G_t &gt; 1$ and the weight increases. If it’s drifting away, $G_t &lt; 1$ and the weight decreases. The incremental unnormalized importance weight at step $t$ is then simply</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\tilde{w}_t^{(i)} \propto G_t(x_t^{(i)})
\end{align*}
$$
</div>

<p>Now watch what happens when we accumulate these incremental updates. The denominators and numerators of consecutive steps cancel each other:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\prod_{t=T-\Delta t}^{0} G_t(x_t) &amp;= \frac{p(\hat{x}_0 | x_{T-\Delta t})}{p(\hat{x}_0 | x_T)} \cdot \frac{p(\hat{x}_0 | x_{T-2\Delta t})}{p(\hat{x}_0 | x_{T-\Delta t})} \cdots \frac{p(\hat{x}_0 | x_0)}{p(\hat{x}_0 | x_{\Delta t})} \\
&amp;= \frac{p(\hat{x}_0 | x_0)}{p(\hat{x}_0 | x_T)} \\
&amp;\approx \frac{p(x_0)}{\underbrace{p(\hat{x}_0 | x_T)}_{\text{const (pure noise)}}} \\
&amp;\propto p(x_0)
\end{align*}
$$
</div>

<p>since $\hat{x}_0(x_0) = x_0$ (fully denoised) and $p(y | \hat{x}_0(x_T))$ is essentially constant — pure noise carries no information about $y$. The telescoping gives us the best of both worlds: at any point in time $t$, the accumulated weight reflects the most up-to-date emission probability $p(y | \hat{x}_0(x_t))$, while the recursive update via $G_t$ means we only ever need to compute a single ratio per step (instead of keeping track of the entire trajectory).</p>

<h4 id="the-big-picture">The big picture</h4>

<p><img src="/blog/ESS/SMC.png" alt="ESS SMC Diagram" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" /></p>

<p>Let’s step back and admire the view from the top of this particular mountain. What we’ve shown is that conditional diffusion generation is, at its core, a particle filter:</p>

<ul>
  <li>The <strong>prior</strong> $p(x_T) = \mathcal{N}(0, I)$ is where our particles start: pure isotropic noise.</li>
  <li>The <strong>transition kernel</strong> $p_\theta(x_{t-\Delta t} | x_t)$ — the discretized reverse SDE — propagates particles toward cleaner states.</li>
  <li>The <strong>emission model</strong> $p(\hat{x}_0 | x_t)$ evaluates each particle’s one-step denoiser prediction against the desired condition.</li>
  <li>The <strong>importance weights</strong> $\tilde{w}_t$ measure how well each particle’s denoised prediction matches $y$.</li>
  <li>The <strong>ESS</strong> — computed as $1/\sum_i \bar{w}_i^2$ — tells us how many particles are effectively contributing.</li>
  <li><strong>Resampling</strong> prunes bad hypotheses and replicates promising ones when the ESS signals weight degeneracy.</li>
</ul>

<p>Unconditional generation is the degenerate special case where $p(\hat{x}_0 | x_t) = \text{const}$ — there is no condition to satisfy, all weights are equal, the ESS equals $N$ at every step, and no resampling is ever triggered. You’re just running $N$ independent Markov chains in parallel. The moment you introduce any form of guidance — classifier guidance, classifier-free guidance, inpainting constraints, compositional objectives — you’ve entered SMC territory, and the ESS machinery we derived throughout this post becomes indispensable.</p>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Weight Watchers]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/blog/ESS/ess_cover.png" /><media:content medium="image" url="/blog/ESS/ess_cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Colombia</title><link href="/blog/Colombia25/" rel="alternate" type="text/html" title="Colombia" /><published>2026-01-05T00:00:00+00:00</published><updated>2026-01-05T00:00:00+00:00</updated><id>/blog/Colombia25</id><content type="html" xml:base="/blog/Colombia25/"><![CDATA[<!-- ## Berlin Over The Years -->

<style>
    .image-gallery {
        overflow: auto;
        margin-left: -1% !important;
    }

    .image-gallery li {
        float: left;
        float: top;
        display: block;
        margin: 0 0 1% 1%;
        width: 99%;
    }

    .image-gallery li a {
        text-align: top;
        text-decoration: none !important;
        color: #777;
    }

    .image-gallery li a span {
        display: block;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        padding: 3px 0;
    }

    .image-gallery li a img {
        width: 100%;
        height: 100%;
        display: flex;
        vertical-align: top;
    }
</style>

<ul class="image-gallery">
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3366.jpg"><img src="/photo_gallery/colombia25/DSC3366.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3464.jpg"><img src="/photo_gallery/colombia25/DSC3464.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3484.jpg"><img src="/photo_gallery/colombia25/DSC3484.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3696-Pano.jpg"><img src="/photo_gallery/colombia25/DSC3696-Pano.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3739.jpg"><img src="/photo_gallery/colombia25/DSC3739.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3775.jpg"><img src="/photo_gallery/colombia25/DSC3775.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3823.jpg"><img src="/photo_gallery/colombia25/DSC3823.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3914.jpg"><img src="/photo_gallery/colombia25/DSC3914.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC3985-2.jpg"><img src="/photo_gallery/colombia25/DSC3985-2.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC4170-2.jpg"><img src="/photo_gallery/colombia25/DSC4170-2.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC4209.jpg"><img src="/photo_gallery/colombia25/DSC4209.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC4404.jpg"><img src="/photo_gallery/colombia25/DSC4404.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/colombia25/DSC4423.jpg"><img src="/photo_gallery/colombia25/DSC4423.jpg" /></a></li>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
</ul>

<!--  -->

<!--  -->
<p><!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 --></p>]]></content><author><name></name></author><category term="photography" /><summary type="html"><![CDATA[The Caribbean, the Coffee Highlands, a Wedding]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/photo_gallery/colombia25/DSC3484.jpg" /><media:content medium="image" url="/photo_gallery/colombia25/DSC3484.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Amalfi to Lecce</title><link href="/blog/Italy25/" rel="alternate" type="text/html" title="Amalfi to Lecce" /><published>2025-10-01T00:00:00+00:00</published><updated>2025-10-01T00:00:00+00:00</updated><id>/blog/Italy25</id><content type="html" xml:base="/blog/Italy25/"><![CDATA[<!-- ## Berlin Over The Years -->

<style>
    .image-gallery {
        overflow: auto;
        margin-left: -1% !important;
    }

    .image-gallery li {
        float: left;
        float: top;
        display: block;
        margin: 0 0 1% 1%;
        width: 99%;
    }

    .image-gallery li a {
        text-align: top;
        text-decoration: none !important;
        color: #777;
    }

    .image-gallery li a span {
        display: block;
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        padding: 3px 0;
    }

    .image-gallery li a img {
        width: 100%;
        height: 100%;
        display: flex;
        vertical-align: top;
    }
</style>

<ul class="image-gallery">
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC2829.jpg"><img src="/photo_gallery/italy25/DSC2829.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC2871.jpg"><img src="/photo_gallery/italy25/DSC2871.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC2992-Pano.jpg"><img src="/photo_gallery/italy25/DSC2992-Pano.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3029.jpg"><img src="/photo_gallery/italy25/DSC3029.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3044.jpg"><img src="/photo_gallery/italy25/DSC3044.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3052.jpg"><img src="/photo_gallery/italy25/DSC3052.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3088-2.jpg"><img src="/photo_gallery/italy25/DSC3088-2.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3110.jpg"><img src="/photo_gallery/italy25/DSC3110.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3156.jpg"><img src="/photo_gallery/italy25/DSC3156.jpg" /></a></li>
    
    
    
    
    <li><a href="/photo_gallery/italy25/DSC3217-Enhanced-NR.jpg"><img src="/photo_gallery/italy25/DSC3217-Enhanced-NR.jpg" /></a></li>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
</ul>

<!--  -->

<!--  -->
<p><!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 -->
  <!-- 
 --></p>]]></content><author><name></name></author><category term="photography" /><summary type="html"><![CDATA[Driving around the boot]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/photo_gallery/italy25/DSC3110.jpg" /><media:content medium="image" url="/photo_gallery/italy25/DSC3110.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Feynman-Kac</title><link href="/blog/FeynmanKac/" rel="alternate" type="text/html" title="Feynman-Kac" /><published>2025-07-12T00:00:00+00:00</published><updated>2025-07-12T00:00:00+00:00</updated><id>/blog/FeynmanKac</id><content type="html" xml:base="/blog/FeynmanKac/"><![CDATA[<script>
MathJax = {
  tex: {
    inlineMath: [['$','$'], ['\\(','\\)']],
    displayMath: [['$$','$$'], ['\\[','\\]']],
    processEscapes: true,
    tags: 'all'
  },
  options: {
    skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
  }
};
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async=""></script>

<p>Recently there has been quite some interest in the idea of <em>Feynman-Kac Steering</em>.</p>

<p>For a given diffusion model, we can generate samples by iteratively removing noise, transforming a sample of pure noise into a sample of the target distribution.
At each step during the denoising process we can let the model estimate what the fully denoised sample would look like.
This can happen either through the score identity or is even more readily available if the model was trained on a denoising loss.
These fully denoised samples are then evaluated with a criterion.
This criterion is then “pulled back” into the denoising process to gauge whether the sample is going to be sufficiently good if fully denoised.
Feynman-Kac steering then implements a resampling step where all the samples are evaluated with the criterion in parallel and the batch is resampled from the noisy samples proportional to each samples fully denoised criterion.
This is essentially particle filtering with extra steps with stochastic PDE theory wrapped around it.</p>

<p>So let’s dissect the theory.</p>

<h3 id="ito-derivative">Ito Derivative</h3>

<p>At the core of diffusion model is a stochastic process $X_t$ that evolves according to a <a href="https://ludwigwinkler.github.io/blog/SDE/">stochastic differential equation (SDE)</a>.
The SDE in question is typically of the form:
\(dX_t = \mu(X_t, t) dt + \sigma(X_t, t) dW_t\)
where $\mu$ is the drift term, $\sigma$ is the diffusion term, and $W_t$ is a Wiener process (or Brownian motion).</p>

<p>What happens to a function $f(X_t, t)$ as it evolves over time? The <a href="https://ludwigwinkler.github.io/blog/ItosLemma/">Ito derivative</a> gives us a way to compute this by extending the classical Taylor expansion to the stochastic process realm:</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
f(X_{t+\Delta t}, t+ \Delta t) 
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} (t + \Delta t - t) + \frac{\partial f(X_t, t)}{\partial X_t} (X_{t+\Delta t} - X_t) \\ 
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2} (X_{t+\Delta t} - X_t)^2 + o(\Delta t)
\end{align*}
$$
</div>

<p>An important factor that we’ll encounter in a second again is that we won’t consider effects of higher order as A) they get divided by ever decreasing factors  and B) they are negligible for small $\Delta t^p$ where $p&gt;2$.</p>

<p>We then get the Ito derivative:</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
f(X_{t+\Delta t}, t+ \Delta t) 
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} \Delta t + \frac{\partial f(X_t, t)}{\partial X_t} (X_{t+\Delta t} - X_t) \\ 
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}(X_{t+\Delta t} - X_t)^2 + \cancel{o(\Delta t)} \\
% &amp;= f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} \Delta t + \frac{\partial f(X_t, t)}{\partial X_t} (X_{t+\Delta t} - X_t) \\
% &amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2} (X_{t+\Delta t} - X_t)^2
\end{align*}
$$  
</div>

<p>For an infinitesimally small time increment $\Delta t$, we assume that the difference becomes a continuous $dt$ and we can plug in our SDE:</p>

<div style="overflow-x: auto;">
$$\begin{align*}
f(X_{t+\Delta t}, t+ \Delta t)
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} dt + \frac{\partial f(X_t, t)}{\partial X_t} \overbrace{(X_{t+\Delta t} - X_t)}^{=dX_t} \\
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2} \overbrace{(X_{t+\Delta t} - X_t)^2}^{=dX_t^2} \\
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} dt + \frac{\partial f(X_t, t)}{\partial X_t} (\mu(X_t, t)dt + \sigma(X_t, t) dW_t) \\
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2} (\mu(X_t, t)dt + \sigma(X_t, t) dW_t)^2 \\
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} dt + \frac{\partial f(X_t, t)}{\partial X_t} (\mu(X_t, t)dt + \sigma(X_t, t) dW_t) \\
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2} (\mu(X_t, t)^2dt^2 + 2\mu(X_t, t)\sigma(x_t, t) dt dW_t + \sigma(X_t, t)^2 dW_t^2) \\
\end{align*}$$
</div>

<p>The next step to observe is that $dW_t = \epsilon \sqrt{dt}$ and that in the limit for a very small $dt$ (think of $dt=10^{-5}$), any effect of a term with $dt$ raised to any power larger than $1$ will diminish even faster (think of $dt^{1.5}=(10^{-5})^{1.5} = 10^{-7.5}$) and thus become negligible.
This allows us to drop any term where $dt^p$ where $p&gt;1$. Isn’t math convenient?</p>

<p>We thus get</p>
<div style="overflow-x: auto;">
$$\begin{align*}
f(X_{t+\Delta t}, t+ \Delta t)
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} dt + \frac{\partial f(X_t, t)}{\partial X_t} (\mu(X_t, t)dt + \sigma(X_t, t) dW_t) \\ 
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2} (\underbrace{\cancel{\mu(X_t, t)^2dt^2}}_{dt^2 \rightarrow 0} + \underbrace{\cancel{2\mu(X_t, t)\sigma(x_t, t) dt dW_t}}_{dt^{1.5} \rightarrow 0} + \sigma(X_t, t)^2 \underbrace{dW_t^2}_{=dt}) \\
=&amp; f(X_t, t) + \frac{\partial f(X_t, t)}{\partial t} \Delta t + \frac{\partial f(X_t, t)}{\partial X_t} (\mu(X_t, t)dt + \sigma(X_t, t) dW_t) \\
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2 dt
\end{align*}$$
</div>

<p>Rearranging a bit we have</p>
<div style="overflow-x: auto;">
$$\begin{align*}
f(X_{t+\Delta t}, t+ \Delta t)- f(X_t, t)
=&amp; \frac{\partial f(X_t, t)}{\partial t} dt + \frac{\partial f(X_t, t)}{\partial X_t} (\mu(X_t, t)dt + \sigma(X_t, t) dW_t) \\
&amp;+ \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2 dt
\end{align*}
$$
</div>

<p>So the infinitesimal change on the right hand side, properly denoted by $dt$ and $dW_t$, would equate the difference in the function $f(X_{t+\Delta t}, t+ \Delta t)$ and $f(X_t, t)$.
Given a step of $dt$ in time, the change in the function $f(X_t, t)$ is given by</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
df(X_t, t) =&amp;\left\{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2\right\} dt \\
&amp; + \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t
\end{align*}$$
</div>

<h3 id="backward-kolmogorov-equation">Backward Kolmogorov Equation</h3>

<p>For a derivation of the Kolmogorov backward equation via the Kramers-Moyal expansion, see <a href="https://ludwigwinkler.github.io/blog/Kramers/">Fokker, Planck &amp; Kolmogorov Revisited</a>; it is also the central object used in <a href="https://ludwigwinkler.github.io/blog/Doob/">Doob’s h-transform</a>.</p>

<p>Ito’s lemma gives us a way to compute the infinitesimal change in a function of a stochastic process.
Since our process is stochastic, for any realized $x_t$, if we run the stochastic process again, we will get a different $X_T$ where $T= t + \Delta t$.
All the stochasticity that we accrue over time $\Delta t$ will make the function $f(X_T, T)$ a random variable.</p>

<p>As a sort of cognitive crutch, you can think of $f(X_T)$ as your Temple Run gold coin counter.
You always start from the same starting point $x_t$ and you always do the same 5 moves in the time $\Delta t$.
If the coin positions stay fixed and you do the same five moves every time you run, you’ll always get the same score $f(x_T)$ at the end.
This is what the deterministic part models.
But now the stochastic part keeps on changing the gold coin positions.
Even for a fixed score $f(x_t)$ and deterministic moves/dynamics, you final gold coin score $f(x_T)$ will vary.
Sometimes you’ll get a lot of gold coins, sometimes you’ll get none.
So essentially, your gold coin score $f(X_T)$ now is a random variable.</p>

<p>The next question we have to ask to naturally arrive at the Kolmogorov backward equation is whether there is a function $f_T(x_T)$ that describes the expected value of $f_T(X_T)$ for the current state $x_t$ <em>given that the dynamics are stochastic!</em></p>

<p>So we’d be interested in the equation</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
f(x_t, t) 
&amp;= \mathbb{E} \left[ f_T(X_T) | X_t = x_t \right] \\
&amp;= \int \underbrace{p(X_T = x_T|x_t)}_{\text{Stochastic Process}} f_T(X_T) dx_T
\end{align*}
$$
</div>

<p>where the expectation how likely a state $X_T$ is given the current state $x_t$ is given by the stochastic process $p(X_T | x_t)$.
So $f_T(x_T)$ gives you an estimate of how much payout at the end you’ll get midway through your Temple Run.</p>

<div style="max-width: 100%; overflow-x: auto;">
  <img src="/blog/blogthumbnails/FeynmanKac.png" alt="Feynman-Kac Illustration" style="width: 100%; height: auto; display: block; margin: 0 auto;" />
</div>

<p><em>Figure: Visual intuition for Feynman-Kac. The stochastic process $X_t$ evolves under random noise, while the expected value of a terminal criterion $f_T(X_T)$ is “pulled back” to earlier times via the backward PDE. Particle trajectories (red) and expectation (blue) illustrate the connection between stochastic paths and deterministic backward evolution.</em></p>

<p>But the definition $f_T(x_T)$ is inherently forward looking as it relies on the solving the SDE forward in time.
Can we also reverse time in a way to compute earlier values of $f(x_t,t)$ starting from the terminal values $f_T(X_T)$?</p>

<p>It turns out that there is a partial differential equation hidden beneath all this stochasticity that allows us to compute the expected value of $f(x_t, t)$ backwards in time.</p>

<p>To show this we’ll start out with Ito’s lemma above but this time integrate it all the way from $t$ to $T$.</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
f_T(X_T) | X_t =&amp; f(X_t, t) + \int_t^T \left\{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2\right\} dt \\ 
&amp; + \int_t^T \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t
\end{align*}
$$
</div>

<p>(where I should have used the integration variable $s$ or $\tau$ instead of $t$ but I didn’t want to introduce unnecessary notational noise ;-) ).
More importantly, this equation tells us that if we start with some intermediate value $f(X_t, t)$ and integrate the infinitesimal changes in the function $f(X_t, t)$ from $t$ to $T$, we will end up with the final value of the function $f_T(X_T)$ which is our terminal payout.</p>

<p>And now comes the important step …</p>

<p>… drumroll 🥁 …</p>

<p>… we take the expectation. 👍</p>

<p>The important thing to note is that the expectation of a Wiener process, however he might be scaled over any time, is zero.</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}\left[ f_T(X_T) | X_t \right] =&amp; \mathbb{E} \left[ f(X_t, t) + \int_t^T\left\{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2\right\} dt \right] \\ 
&amp; + \underbrace{\cancel{\mathbb{E}\left[\sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t \right]}}_{=0}
\end{align*}
$$
</div>

<p>While $X_t$ is a random variable, its stochasticity arises completely from the input of the Wiener process $dW_t$.
If we eliminate the influence of the Wiener process with the expectation $\mathbb{E}$ at every step, we will essentially obtain a deterministic ODE.
With this intuition, the expectation essentially has no influence on the remaining terms and we can write</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\underbrace{\mathbb{E}\left[ f_T(x_T) | X_t \right]}_{=f(X_t, t)}
&amp;= f(X_t, t) + \int_t^T\left\{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2\right\} dt \\
f(X_t, t) &amp;= f(X_t, t) + \int_t^T\Bigg\{ \underbrace{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2}_{\stackrel{!}{=} 0}\Bigg\} dt
\end{align*}
$$
</div>

<p>The equation above can only hold if the term we’re integrating</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
0 &amp;\stackrel{!}{=} \int_t^T \left\{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2 \right\} dt
\end{align*}
$$
</div>

<p>An important observation is that the integral above is a definite integral over the arbitrary time interval $[t, T]$.
This means that the integrand must be zero for all $t$ in the interval $[t, T]$ as we could choose $t$ arbitrarily close to $T$ and the integral would still have to be zero.
We can go backward in time from the terminal time $T$ to $T-dt$ and the integral has to be zero, i.e. $\int_{T-dt}^T \ldots dt = 0$.
If that integral is zero, then the integral $\int_{T-2dt}^{T-dt} \ldots dt = 0$ also has to be zero in order for the whole integral $\int_{T-2dt}^T \ldots dt = 0$ to hold.
This means that the integrand must be zero for all $t$ in the interval $[t, T]$.
This gives us the backward Kolmogorov equation:</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
-\frac{\partial f(X_t, t)}{\partial t} &amp;= \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2
\end{align*}
$$
</div>

<p>with some terminal condition $f_T(X_T)$.
This result is quite remarkable as it allows us to compute the expected value of a function of a stochastic process at an earlier time $t$ given the terminal condition $f_T(X_T)$ with a PDE backward in time instead of solving a SDE forward in time.</p>

<p>While it looks deceptively similar to the Ito derivative, the backward Kolmogorov equation is a PDE that describes how the expected value of a function of a stochastic process evolves backward in time.</p>

<p>In practical terms, this means that if we have a function $f_T(X_T)$ at the terminal time $T$ with the stochastic dynamics of $dX_t$, we can compute the expected value of this function at an earlier time $t$ by solving the backward Kolmogorov equation.</p>

<p>For example, we could redefine the function $f_T(X_T)$ as the probability of $p(x_T)$ at the terminal time $T$.
We could then solve the Kolmogorov backward equation in the form of a PDE backwards in time to obtain the expected value of the probability at an earlier time $t$, which we would denote as $p(X_T | X_t)$.</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
- \frac{\partial p(X_T | X_t)}{\partial t} =&amp; \frac{\partial p(X_T|X_t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 p(X_T | X_t)}{\partial X_t^2}\sigma(X_t, t)^2
\end{align*}
$$
</div>

<p>This equation would evolve backward the probability density $p(X_T | X_t)$ from the terminal time $T$ to the earlier time $t$.
At some earlier point $t$, we could evaluate the how likely the state $X_T$ would be given the earlier state $X_t$.</p>

<h3 id="a-stochastic-sidenote">A Stochastic Sidenote</h3>

<p>For some function $f(x_t, t)$ and a terminal value $f_T(x_T)$, we derived the following identity above</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
f_T(X_T) | X_t =&amp; f(X_t, t) + \int_t^T \left\{\frac{\partial f(X_t, t)}{\partial t} + \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2\right\} dt \\ 
&amp; + \int_t^T \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t
\end{align*}
$$
</div>

<p>But now with our previously gained knowledge we can ascertain that the deterministic integrand is always zero, so we obtain the following equation</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
f_T(X_T) | X_t =&amp; f(X_t, t) + \int_t^T \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t
\end{align*}
$$
</div>

<p>which is A) a martingale and B) a random variable due to the Ito integral.
We can make this more precise by observing as before that the expectation is zero because $\mathbb{E}[dW_t] = 0$.
Also, we can quite easily compute the variance of this random variable by computing</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}\left[ \left( \int_t^T \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t \right)^2 \right]
&amp;= 
\mathbb{E}\left[ \left( \int_t^T \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t \right) 
\left( \int_t^T \sigma(X_{t'}, {t'}) \frac{\partial f(X_{t'}, {t'})}{\partial X_{t'}} dW_{t'} \right) \right].
\end{align*}
$$
</div>

<p>By definition a Wiener process $W_t$ has the property that $\mathbb{E}[dW_t dW_{t’}] = \delta(t-t’) dt$ which says that the product of a Wiener process at two different times is zero unless the two times are the same.
This means that we can rewrite the above equation as</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
\mathbb{E}\left[ \left( \int_t^T \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} dW_t \right)^2 \right]
&amp;= \int_t^    T \int_t^T \mathbb{E}\left[ \sigma(X_t, t) \frac{\partial f(X_t, t)}{\partial X_t} \sigma(X_{t'}, {t'}) \frac{\partial f(X_{t'}, {t'})}{\partial X_{t'}} dW_t dW_{t'} \right] \\
&amp;= \int_t^T \sigma(X_t, t)^2 \frac{\partial f(X_t, t)}{\partial X_t}^2 dt
\end{align*}
$$
</div>

<p>because all the “cross terms” where $t \neq t’$ vanish due to the property of the Wiener process. This is also known as Ito Isometry.
This gives us the mean and variance of the random variable $f_T(X_T) | X_t$:</p>

<div style="overflow-x: auto;">
$$\begin{align*}
\mathbb{E}[f_T(X_T) | X_t] &amp;= f(X_t, t) \\
\text{Var}[f_T(X_T) | X_t] &amp;= \int_t^T \sigma(X_t, t)^2 \frac{\partial f(X_t, t)}{\partial X_t}^2 dt \\
&amp;\downarrow \\
f_T(X_T) | X_t &amp;\sim \mathcal{N}\left(f(X_t, t), \int_t^T \sigma(X_t, t)^2 \frac{\partial f(X_t, t)}{\partial X_t}^2 dt\right)
\end{align*}$$
</div>

<h3 id="feynman-kac-equation">Feynman-Kac Equation</h3>

<p>Previously, we’ve observed how with a terminal condition at time $T$, we can compute the expected value of a function of a stochastic process at an earlier time $t$ by solving the backward Kolmogorov equation</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
-\frac{\partial f(X_t, t)}{\partial t} &amp;= \frac{\partial f(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 f(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2
\end{align*}
$$
</div>

<p>where the dynamics of a stochastic process $X_t$ are given by the SDE
\(dX_t = \mu(X_t, t) dt + \sigma(X_t, t) dW_t\)
and the terminal condition is given by $f_T(X_T)$.</p>

<p>In their famous equation, Mark Kac and Richard Feynman asked “Yo, what if we add more terms to the Kolmogorov Backward Equation?” (essentially … please don’t quote me on this).</p>

<p>In order to align with the normal notation of the Feynman-Kac equation, we will be using $u$ instead of $f$.
The FK equation then is posited as follows:</p>
<div style="overflow-x: auto;">
$$
\begin{align*}-\frac{\partial u(X_t, t)}{\partial t} &amp;= \frac{\partial u(X_t, t)}{\partial X_t} \mu(X_t, t) + \frac{1}{2} \frac{\partial^2 u(X_t, t)}{\partial X_t^2}\sigma(X_t, t)^2 \underbrace{+ c(X_t, t) u(X_t, t) + v(X_t, t)}_{\text{new FK terms}}
\end{align*}
$$
</div>

<p>where $c(X_t, t)$ and $v(X_t, t)$ are some functions that we can choose.</p>

<p>We can interpret $c(X_t, t)$ as a “potential” term that modifies the expected value of the function $u(X_t, t)$ at time $t$ by interacting with the function $u(X_t, t)$.
$v(X_t, t)$ can be interpreted as a “forcing” term that adds an additional contribution to the expected value of the function $u(X_t, t)$ at time $t$.</p>

<p>In order to make the notation a bit easier we’ll shorten the notation to</p>

<div style="overflow-x: auto;">
$$\begin{align*}
-\partial_t u &amp;= \partial_{x} u \ \mu + \frac{1}{2} \partial_{xx} u \ \sigma^2 + c \ u + v
\end{align*}
$$
</div>

<p>Solving this equation backwards in time from the terminal condition $u(X_T, T)$ gives us the expected value of the function $u(X_t, t)$ at an earlier time $t$ given the terminal condition $u(X_T, T)$.</p>

<p>So how do we solve this equation?
Solving PDEs is fiendishly hard and no easy task.</p>

<p>Usually, there are a couple of ‘ansatz’s that we can use to solve PDE’s.
For the FK equation we’ll chose the ansatz of defining a new function $y(x_t, t)$ as</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
y(X_s, s) &amp;= u(X_s, s) \ e^{\int_t^s c(x_r, r) dr} \\
&amp;\downarrow \text{Shorten the notation} \\
y_s &amp;= u_s \ e^{\int_t^s c_r dr}
\end{align*}
$$
</div>

<p>where $u_s$ is the function $u(X_s, s)$ and $c_r$ is the function $c(X_r, r)$.</p>

<p>Differentiating this function with respect to $s$ and the product rule gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
dy_s &amp;= du_s \ e^{\int_t^s c_r dr} + u_s \ c_s \ e^{\int_t^s c_r dr}
\end{align*}
$$
</div>

<p>where $du$ is an Ito derivative but $u_s \ d e^{\int_t^s c_r dr}$ is not.</p>

<p>Both $u(s, X_s)$ and $\exp\left( \int_t^s c(X_r), dr \right)$ depend on the random path $X$, but only $u(s, X_s)$ is a function of the semimartingale $X_s$ at a point—so Itô’s lemma applies.
$e^{\int_t^s c_r dr}$ is a functional of the entire path, not just $X_s$, and its exponent $\int_t^s c(X_r), dr$ is absolutely continuous (no Brownian term).
You assume the values $X_r$ to be deterministic and the functional $\int c_r dr$ is evaluated on an “already” realized path of $X_r$’s.
So the ordinary chain rule suffices:
\(d e^{\int_t^s c_r dr} = e^{\int_t^s c_r dr} c_r ds\)</p>

<p>In short: use Itô’s lemma for functions of $X_s$; use the chain rule for pathwise functionals without stochastic terms.</p>

<p>Evaluating the Ito derivative we get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
du &amp;= \left\{ \partial_t u + \partial_x u \ \mu + \frac{1}{2} \partial_{xx} u \ \sigma^2 \right\} dt + \sigma \ \partial_x u_t \ dW_t \\
\end{align*}
$$
</div>

<p>and we can compare that to our original FK equation</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
-\partial_t u &amp;= \partial_{x} u \ \mu + \frac{1}{2} \partial_{xx} u \ \sigma^2 + c \ u + v \\
- c \ u - v &amp;= \partial_t u +\partial_{x} u \ \mu + \frac{1}{2} \partial_{xx} u \ \sigma^2
\end{align*}
$$
</div>

<p>We can then proceed to plug our $-c \ u - v$ into our Ito derivative and get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
du &amp;= \left\{ -c \ u - v \right\} dt + \sigma \ \partial_x u_t \ dW_t \\
\end{align*}
$$
</div>

<p>Now we take the $du$ and plug that back into our definition of $dy$ and get</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
dy_s &amp;= du_s \ e^{\int_t^s c_r dr} + u_s \ c_s \ e^{\int_t^s c_r dr} \\
&amp;= \left( \left\{ \cancel{-c_s \ u_s} - v_s \right\} ds + \sigma \ \partial_x u_s \ dW_s \right) \ e^{\int_t^s c_r dr} \
\cancel{ + u_s \ c_s \ e^{\int_t^s c_r dr}} \\
&amp;= \left(- v_s \ ds + \sigma \ \partial_x u_s \  dW_s \right) e^{\int_t^s c_r dr}
\end{align*}
$$
</div>
<p>Integrating both sides from $s=t$ to $s=T$ gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}y_T - y_t &amp;= \int_t^T \left(- v_s \ ds + \sigma \ \partial_x u_s \  dW_s \right) e^{\int_t^s c_r dr} \ ds\\
&amp;= -\int_t^T v_s e^{\int_t^s c_r dr} ds + \int_t^T \sigma \ \partial_x u_s \ e^{\int_t^s c_r dr} \ dW_s
\end{align*}
$$
</div>
<p>and evaluating the left hand side gives us</p>
<div style="overflow-x: auto;">
$$
\begin{align*}
y_T - y_t &amp;= u_T \ e^{\int_t^T c_r dr} - u_t \ \underbrace{e^{\int_t^t c_r dr}}_{=e^0=1} \\
&amp;= u_T \ e^{\int_t^T c_r dr} - u_t
\end{align*}
$$
</div>

<p>where integrating any quantity a zero amount $\int_t^t \ldots dt$ is always equal to zero as nothing gets added to the integral.</p>

<p>We then ultimately have</p>

<div style="overflow-x: auto;">
$$\begin{align*}
u_T \ e^{\int_t^T c_r dr} - u_t &amp;= \int_t^T -v_s e^{\int_t^s c_r dr} ds + \int_t^T \sigma \ \partial_x u_s \ e^{\int_t^s c_r dr} \ dW_s
\end{align*}
$$
</div>

<p>Now, we’re going to pull off our favourite, slick stochastic process move and take the expectation of both sides which eliminates the martingale $\int dW_s$ term on the right hand side,</p>

<div style="overflow-x: auto;">
$$\begin{align*}
u_t &amp;= \mathbb{E}\left[ u_T \ e^{\int_t^T c_r dr} +  \int_t^T v_s e^{\int_t^s c_r dr} ds \right]
\end{align*}
$$
</div>

<p>If we now set $c_r=0$ and $v_s =0$, we indeed recover OG Kolmogorov backward equation $u_t = \mathbb{E}[u_T]$.</p>

<h3 id="feynman-kac-steering">Feynman-Kac Steering</h3>

<p>Diffusion models are able to produce an estimate of a fully denoised sample $\hat{x}_0$ from a diffusive sample $x_t$ by either leveraging the score identity</p>

<div style="overflow-x: auto;">
$$
\begin{align*}
\nabla_{x_t} \log p_t(x_t|x_0) &amp;=  -\frac{(x_t - \alpha_t x_0)}{\sigma_t^2} \quad ; \quad x_t = \alpha_t x_0 + \sigma_t \epsilon\\ 
&amp;\downarrow \\
\hat{x}_0 &amp;= x_t + \frac{\sigma_t^2 \ \nabla_{x_t} \log p_t(x_t|x_0)}{\alpha_t} \\
&amp;= \frac{x_t - \sigma_t \ \epsilon_\theta}{\alpha_t}
\end{align*}
$$
</div>

<p>or by using a denoising loss which directly aims at estimating the fully denoised sample $x_0$ from a noisy sample $x_t$.</p>

<p>In both cases, we can use the denoised sample $\hat{x}_0$ as an estimate of the fully denoised sample $x_0$.
This denoised sample is then evaluated with a criterion $c(\hat{x}_0)$ that tells us how good the sample is.</p>

<p>The Feynman-Kac/Kolmogorov Backward equation give us a mathematical tool to “pull back” $c(\hat{x}_0)$ to a more diffused sample $x_t$.
In essence it allows us to estimate a diffused version of the criterion $c(x_t)$ at an earlier time step $t$.
We use the Feynman-Kac framework to estimate the expected value of the criterion at an earlier time step $t$.
Theoretically, we could try to evaluate the full expectation $\mathbb{E}[c(x_t)] = \int c(x_t) p(x_t) dx_t$ but this is computationally expensive and not feasible in practice.
We can instead filter or resample the batch, preferentially retaining samples with higher expected criterion values and discarding those predicted to perform poorly.
FK steering then allows us to resample the batch of samples $x_t$ proportional to the expected value of the criterion $c(x_t)$.
Since we’re dealing with a stochastic process, even samples in the mini batch which were resampled from the same noisy sample $x_t$ will have different expected values of the criterion $c(x_t)$ further down the reverse diffusion process.</p>]]></content><author><name></name></author><category term="blog" /><summary type="html"><![CDATA[Is it Katz, Kak, Kaz, Katsch? Anyway, it's nice math.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="/blog/blogthumbnails/FeynmanKac.png" /><media:content medium="image" url="/blog/blogthumbnails/FeynmanKac.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>