Here is a tiny chemical system with just two reactions: molecules of X are created at a steady rate, and each existing molecule can decay. Together they pull the count toward an equilibrium — but never smoothly.
<p class="hint">A tiny chemical system: molecules of <b>X</b> are <b>created</b> at a steady rate, and each one can <b>decay</b>. The Gillespie algorithm draws two random numbers per step — <i>how long</i> until the next reaction, and <i>which</i> one fires — so the count jumps in discrete, random steps. Press <b>Run</b> for a fresh trajectory.</p>
<div class="readout">
<span>X = <b id="count">0</b></span>
<span>t = <b id="time">0.00</b></span>
<span>events: <b id="events">0</b></span>
</div>
<canvas id="plot" width="520" height="240"></canvas>
<div class="btns">
<button id="run" type="button">Run</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.readout { display: flex; gap: 1.2rem; font: 600 15px ui-monospace, monospace; color: #1d3557; margin: .2rem 0 .5rem; }
.readout b { color: #e63946; }
canvas { width: 100%; max-width: 520px; height: auto; background: #f5f8fb; border: 1px solid #cdd9e3; border-radius: 8px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
// Gillespie Stochastic Simulation Algorithm (Gillespie, 1976/1977).
// System: 0 -> X (creation, rate k1) and X -> 0 (decay, rate k2 per molecule).
// Propensities: a1 = k1 (constant), a2 = k2 * X. a0 = a1 + a2.
// Per step: tau = (1/a0) * ln(1/r1) is the exponential waiting time;
// r2 picks which reaction fires, weighted by a1/a0 vs a2/a0.
// The deterministic steady state is X* = k1/k2; the stochastic count fluctuates around it.
const k1 = 10; // creation rate (molecules per unit time)
const k2 = 1; // decay rate per molecule
const T_MAX = 12; // total simulated time
const cv = document.getElementById('plot');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height, PAD = 28, YMAX = 24;
let timer = null;
function axes(){
ctx.clearRect(0,0,W,H);
// steady-state reference line X* = k1/k2
const ystar = H - PAD - (k1/k2)/YMAX * (H - 2*PAD);
ctx.strokeStyle = '#bcd';
ctx.setLineDash([5,4]); ctx.beginPath(); ctx.moveTo(PAD, ystar); ctx.lineTo(W-4, ystar); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#7a8aa0'; ctx.font = '11px system-ui';
ctx.fillText('X* = '+(k1/k2), PAD+4, ystar-4);
ctx.strokeStyle = '#cdd9e3'; ctx.beginPath();
ctx.moveTo(PAD, PAD); ctx.lineTo(PAD, H-PAD); ctx.lineTo(W-4, H-PAD); ctx.stroke();
}
const px = t => PAD + (t / T_MAX) * (W - PAD - 4);
const py = x => H - PAD - Math.min(x, YMAX) / YMAX * (H - 2*PAD);
function run(){
if (timer) return;
axes();
let X = 0, t = 0, events = 0;
const traj = [[0,0]];
// Run the full SSA up front, storing the step-trajectory.
while (t < T_MAX) {
const a1 = k1, a2 = k2 * X, a0 = a1 + a2;
if (a0 <= 0) break;
const r1 = Math.random() || 1e-12;
t += (1 / a0) * Math.log(1 / r1); // exponential waiting time
if (t > T_MAX) break;
const r2 = Math.random();
if (r2 < a1 / a0) X += 1; else X -= 1; // creation vs decay
events++;
traj.push([t, X]);
}
traj.push([T_MAX, X]);
// Animate drawing the step function.
let i = 1;
document.getElementById('run').disabled = true;
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2; ctx.beginPath();
ctx.moveTo(px(traj[0][0]), py(traj[0][1]));
timer = setInterval(() => {
const reps = Math.max(1, Math.floor(traj.length / 60));
for (let r = 0; r < reps && i < traj.length; r++, i++) {
const [pt, ] = traj[i-1], [ct, cx] = traj[i];
ctx.lineTo(px(ct), py(traj[i-1][1])); // horizontal hold
ctx.lineTo(px(ct), py(cx)); // vertical jump
document.getElementById('count').textContent = cx;
document.getElementById('time').textContent = ct.toFixed(2);
document.getElementById('events').textContent = Math.min(i, events);
}
ctx.stroke();
if (i >= traj.length) { clearInterval(timer); timer = null; document.getElementById('run').disabled = false; }
}, 30);
}
function reset(){ if (timer){ clearInterval(timer); timer = null; } document.getElementById('run').disabled = false;
axes(); document.getElementById('count').textContent='0'; document.getElementById('time').textContent='0.00'; document.getElementById('events').textContent='0'; }
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', reset);
axes();
<p class="hint">A tiny chemical system: molecules of <b>X</b> are <b>created</b> at a steady rate, and each one can <b>decay</b>. The Gillespie algorithm draws two random numbers per step — <i>how long</i> until the next reaction, and <i>which</i> one fires — so the count jumps in discrete, random steps. Press <b>Run</b> for a fresh trajectory.</p>
<div class="readout">
<span>X = <b id="count">0</b></span>
<span>t = <b id="time">0.00</b></span>
<span>events: <b id="events">0</b></span>
</div>
<canvas id="plot" width="520" height="240"></canvas>
<div class="btns">
<button id="run" type="button">Run</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.readout { display: flex; gap: 1.2rem; font: 600 15px ui-monospace, monospace; color: #1d3557; margin: .2rem 0 .5rem; }
.readout b { color: #e63946; }
canvas { width: 100%; max-width: 520px; height: auto; background: #f5f8fb; border: 1px solid #cdd9e3; border-radius: 8px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
// Gillespie Stochastic Simulation Algorithm (Gillespie, 1976/1977).
// System: 0 -> X (creation, rate k1) and X -> 0 (decay, rate k2 per molecule).
// Propensities: a1 = k1 (constant), a2 = k2 * X. a0 = a1 + a2.
// Per step: tau = (1/a0) * ln(1/r1) is the exponential waiting time;
// r2 picks which reaction fires, weighted by a1/a0 vs a2/a0.
// The deterministic steady state is X* = k1/k2; the stochastic count fluctuates around it.
const k1 = 10; // creation rate (molecules per unit time)
const k2 = 1; // decay rate per molecule
const T_MAX = 12; // total simulated time
const cv = document.getElementById('plot');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height, PAD = 28, YMAX = 24;
let timer = null;
function axes(){
ctx.clearRect(0,0,W,H);
// steady-state reference line X* = k1/k2
const ystar = H - PAD - (k1/k2)/YMAX * (H - 2*PAD);
ctx.strokeStyle = '#bcd';
ctx.setLineDash([5,4]); ctx.beginPath(); ctx.moveTo(PAD, ystar); ctx.lineTo(W-4, ystar); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#7a8aa0'; ctx.font = '11px system-ui';
ctx.fillText('X* = '+(k1/k2), PAD+4, ystar-4);
ctx.strokeStyle = '#cdd9e3'; ctx.beginPath();
ctx.moveTo(PAD, PAD); ctx.lineTo(PAD, H-PAD); ctx.lineTo(W-4, H-PAD); ctx.stroke();
}
const px = t => PAD + (t / T_MAX) * (W - PAD - 4);
const py = x => H - PAD - Math.min(x, YMAX) / YMAX * (H - 2*PAD);
function run(){
if (timer) return;
axes();
let X = 0, t = 0, events = 0;
const traj = [[0,0]];
// Run the full SSA up front, storing the step-trajectory.
while (t < T_MAX) {
const a1 = k1, a2 = k2 * X, a0 = a1 + a2;
if (a0 <= 0) break;
const r1 = Math.random() || 1e-12;
t += (1 / a0) * Math.log(1 / r1); // exponential waiting time
if (t > T_MAX) break;
const r2 = Math.random();
if (r2 < a1 / a0) X += 1; else X -= 1; // creation vs decay
events++;
traj.push([t, X]);
}
traj.push([T_MAX, X]);
// Animate drawing the step function.
let i = 1;
document.getElementById('run').disabled = true;
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2; ctx.beginPath();
ctx.moveTo(px(traj[0][0]), py(traj[0][1]));
timer = setInterval(() => {
const reps = Math.max(1, Math.floor(traj.length / 60));
for (let r = 0; r < reps && i < traj.length; r++, i++) {
const [pt, ] = traj[i-1], [ct, cx] = traj[i];
ctx.lineTo(px(ct), py(traj[i-1][1])); // horizontal hold
ctx.lineTo(px(ct), py(cx)); // vertical jump
document.getElementById('count').textContent = cx;
document.getElementById('time').textContent = ct.toFixed(2);
document.getElementById('events').textContent = Math.min(i, events);
}
ctx.stroke();
if (i >= traj.length) { clearInterval(timer); timer = null; document.getElementById('run').disabled = false; }
}, 30);
}
function reset(){ if (timer){ clearInterval(timer); timer = null; } document.getElementById('run').disabled = false;
axes(); document.getElementById('count').textContent='0'; document.getElementById('time').textContent='0.00'; document.getElementById('events').textContent='0'; }
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', reset);
axes();
Press Run and watch the count climb and fall in discrete jumps, each at a random moment. The more X there is, the faster decay competes with creation, so the line wobbles around a balance point instead of resting on it. Press Run again: same rules, a brand-new trajectory. That irreducible scatter is exactly what the smooth differential equation throws away — and exactly what matters inside a cell with only a handful of molecules.
Comments
Loading comments...