Fourier's Fire

real fire
equations
u/∂t + (u·∇)u = −∇p + βTŷ0.50buoyancy+ ε(N×ω)
T/∂t + (u·∇)T = −kT0.025cooling
T(x, bottom) = embers(x)0.55ember density · hottest word
tell the fire what it is doing, the model reads it

loading a 23 MB sentence model into your browser…

or set the fire by hand
embersinferno
how it works, in seven questions

How does a sentence become a fire?

Sentence → 384 numbers → one number on the embers→inferno line → the fire. Remove a word: its heat.

the steps
  • The model turns any sentence into 384 numbers; similar meanings land close together. At load it does this for 16 "roaring" and 16 "dying" sentences, and the line between their two averages is the scale, −1 to +1.
  • Your sentence is projected onto that line. One number. It sets buoyancy, cooling and ember density.
  • Each word is removed in turn and the sentence re-embedded; the drop is the word's heat on the bottom row. No word lists, no rules.

What is the model allowed to touch?

Under the two fires, each term glows with the value the model has just set: orange for buoyancy, blue for cooling, yellow for the embers. The tiny numbers are those values.

u/∂t + (u·∇)u = −∇p + βTŷ + ε(N×ω)
T/∂t + (u·∇)T = −kT
T(x, bottom) = embers(x)
∇·u = 0
what the model actually controls
  • One number, s: the projection, clipped to ±1.5. From it, by fixed linear maps I chose by hand: ember density p = 0.55 + 0.3s, buoyancy β = 0.5 (1 + 0.45s), cooling k = 0.025 (1 − 0.3s).
  • And one number per word, its heat on the bottom row: 0.6 + 0.25s + 1.8 × the word's share, clipped to 0.15 … 1.
  • Nothing else changes. The slider sets the same s by hand.
the equations, the way a textbook writes them
  • Name. The incompressible Navier–Stokes equations with the Boussinesq approximation for buoyancy, coupled to an advection equation for temperature. Two dimensions.
  • Momentum.u/∂t + (u·∇)u = −∇p/ρ + ν∇²u + gβT(TT0) ŷ. Newton's second law for a parcel of air: acceleration equals pressure pushing it, viscosity dragging it, and buoyancy lifting it if it is warmer than its surroundings.
  • Continuity. ∇·u = 0. Air neither piles up nor thins out. This is what the pressure enforces.
  • Heat.T/∂t + (u·∇)T = α∇²Tk(TT0). Temperature is carried along by the air, spreads by conduction, and is lost to the cold surroundings. The −k(T − T₀) term is Newton cooling, a linear loss; real radiation goes as T⁴ and is not in here.
  • Symbols. u velocity (m/s; here cells per frame). p pressure. ρ density. ν kinematic viscosity. g gravity, 9.81 m/s². βT thermal expansion coefficient of air, about 1/300 per kelvin. TT0 temperature above ambient. α thermal diffusivity. k a loss rate, per second.
  • What the page lumps together. The single β on the page is g βT, in grid units. ρ is folded into p. ν and α are not set at all: the grid's own numerical diffusion plays both roles, and a vorticity-confinement term ε(N × ω) puts back the small swirls that diffusion eats (Fedkiw, Stam & Jensen, 2001).
  • Boussinesq. Density is treated as constant everywhere except in the buoyancy term. That is valid when temperature differences are small compared with absolute temperature. A flame is hundreds of kelvin above ambient, so this is well beyond Boussinesq's assumptions, not at their edge. It still looks right because the puffing instability of a buoyant plume is robust enough not to care. Read it as a model of a hot plume over a heat source, not of combustion.
  • What is not in it. No chemistry: the fire is a prescribed source of heat on the bottom row. No radiation transport beyond the −kT loss. No third dimension. No soot, so no smoke.
  • Boundary conditions. Bottom: T = embers(x), the words; no flow through the floor. Sides: no flow through the walls. Top: open, air leaves freely.
  • The 1990s trick, in the same language.T/∂t = D ∂²T/∂x² − vT/∂ykT. Fourier's heat equation with a constant vertical wind v put in by hand. There is no u to solve for, so nothing can puff.
  • Numerics. 320×180 cells, one step per frame, semi-Lagrangian advection (Stam 1999), 16 Jacobi sweeps for the pressure. Unconditionally stable; the price is extra numerical diffusion, which is what ε compensates.

What does the model actually see?

all-MiniLM-L6-v2, 22.7 M parameters, 23 MB, in your tab. It cannot answer, only place. This is what it sees:

your sentence, 384 numbers
the embers→inferno direction, 384 numbers
one number: how far along the line
each word, removed in turn
what these numbers are, and what the model is
  • Top strip: the 384 numbers for your sentence, orange positive, blue negative. Second strip: the direction, the difference between the "roaring" average and the "embers" average, same 384 slots.
  • "How far along the line" is the dot product of the two, scaled so the two training averages sit at ±1. Each word: the sentence without it, re-embedded, and the drop.
  • The model is a small BERT-style encoder: 6 transformer layers, 384 hidden units, 12 attention heads. Distilled from Microsoft's MiniLM, fine-tuned by the sentence-transformers team on about a billion sentence pairs so that paraphrases land close together.
  • It reads word pieces, runs six layers of attention, and averages the last layer over the tokens. No decoder. Runs as an 8-bit ONNX export on WebAssembly; nothing you type leaves the tab.

What does "embers→inferno" mean to the model?

Try "not roaring anymore" above: it reads as roaring. The model has word sense and no theory of "not". 441 words placed on the line by the model, coldest and hottest thirty; the rest below. On 40 held-out sentences the sign is right 30 times, chance 20; the eval, with controls. What the direction is and is not: the README.

waiting for the model…
all 441 words, coldest to hottest

Why does a real fire flicker?

The brightness of each fire over the last ten seconds. A real fire pulses, a few times a second. The equations pulse too, because hot air is light. The trick only flickers: noise, no rhythm.

real fire
equations
the 90s trick, same rule as FIRE.COM
why this is the whole point
  • Real fires puff at about 1.5/√d Hz for a fire d metres across (Cetegen & Ahmed, 1993): the rising column of hot air pinches off into blobs. The equations have that column, so they get the rhythm for free.
  • The trick has no column. Its updraft is a constant typed in by hand, so its brightness is the noise of the random embers and nothing else. This strip is the difference between the two fires in one line.
  • The simulated pulse has its own time scale (a frame, not a second), so the count will not match the real fire's. The claim is that there is a rhythm, not that it is the same rhythm.

What did the 1990s do in 153 bytes?

153 bytes. Fourier's heat equation with the updraft typed in as a constant. It cannot puff.

The model reaches this one too, the only way it can: by rewriting two bytes, 0x53 ember density and 0x7C heat lost per row, and restarting DOSBox. That is the trick's whole state space. No buoyancy to set, so no puffing to get.

tell this fire too, the same model reads it

153 bytes of 8086, assembled with GNU as, two of them written by the page. ESC quits. Or open the shipped file in its own tab.

    

the inner loop
  • Every pixel becomes the average of the ones below it, minus a loss, written one row up. The two side neighbours are the diffusion, reading below and writing above is the updraft, the subtraction is the loss.
  • Readable version, 70 instructions with comments, 153 bytes. Not golfed; the 1996 compo did it under 256 with tricks this file does not use. The whole file, with the palette and the build line.
# the equation, one cell at a time, for every cell from row 1 down to row 197.
# T(x, y-1)  <-  ( T(x-1,y) + T(x+1,y) + T(x,y+1) + T(x,y+2) ) / 4  -  loss
#   the two side neighbours   = diffusion, D d2T/dx2
#   reading below, writing above = the updraft, v dT/dy, at one row per frame
#   the subtraction           = the loss, kT, here a constant so it fits in one instruction
        mov     di, 320
cell:   xor     ax, ax
        mov     al, es:[di-1]
        mov     bl, es:[di+1]
        xor     bh, bh
        add     ax, bx
        mov     bl, es:[di+320]
        add     ax, bx
        mov     bl, es:[di+640]
        add     ax, bx
        shr     ax, 2                   # the average
        sub     al, 1                   # the loss. This byte is the flame height: about (mean source)/loss rows.
        jnc     6f
        xor     al, al                  # temperature does not go below zero
6:      mov     es:[di-320], al         # written one row UP: that is the updraft
        inc     di
        cmp     di, 198*320
        jb      cell

What line did they leave out?

Ninety lines. One of them says hot air is light. It puffs.

the lines that matter
  • One line adds buoyancy. Then the air must stay incompressible, so a pressure solve. Then the heat rides the air. That is the whole difference between the trick and a fire.
// buoyancy: hot air is light. This single line is what the 1990s trick does not have.
for (let i = 0; i < N; i++) v[i] -= K.beta * T[i];
// advect velocity by itself
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const i = y * W + x; u2[i] = sample(u, x - u[i], y - v[i]); v2[i] = sample(v, x - u[i], y - v[i]); }
// make it incompressible: solve ∇²p = ∇·u, subtract ∇p
for (let y = 1; y < H - 1; y++) for (let x = 1; x < W - 1; x++) { const i = y * W + x; p[i] = (p[i - 1] + p[i + 1] + p[i - W] + p[i + W] - div[i]) * 0.25; }
for (let y = 1; y < H - 1; y++) for (let x = 1; x < W - 1; x++) { const i = y * W + x; u[i] = u2[i] - 0.5 * (p[i + 1] - p[i - 1]); v[i] = v2[i] - 0.5 * (p[i + W] - p[i - W]); }
// carry the heat with the air, and let it cool
const i = y * W + x; const t = sample(T, x - u[i], y - v[i]) * (1 - K.k);
  • v[i] -= K.beta * T[i] is buoyancy. y grows downward in the array, so "up" is −v.
  • Advection is semi-Lagrangian: each cell asks "where did my air come from" and samples there. Unconditionally stable.
  • The pressure solve makes the velocity divergence-free; without it the plumes would pile up and stall.
  • Heat is advected with the air and multiplied by (1 − k) each step. Source: fluid.js in the repo.