Growth, lightning and fire
The run starts from bare ground. Trees fill in at random, and for a while nothing burns much because the forest is too sparse for fire to travel. As density rises, clusters of touching trees grow and merge; a lightning strike in a large cluster now burns all of it. The density curve turns into a sawtooth — slow growth, sudden loss — and then fluctuates around a steady level with no further drift.
- Fires come in every size. Most strikes burn a handful of trees; a few burn a large part of the grid. The log-log plot shows how the number of fires falls off with size.
- The ratio p/f sets the scale. Rarer lightning (larger p/f) lets more trees accumulate between fires, so fires are fewer and larger. The lab's balance readout makes this exact on average.
- The forest keeps a memory. A large fire leaves a scar that regrows as a young, sparse patch; the next fires tend to stop at its edge. Watch the pale ash patches turn green.
The Drossel–Schwabl rules
Barbara Drossel and Franz Schwabl added lightning to an earlier forest-fire automaton in 1992. On an L × L grid with wrapped edges, every cell updates at once from the previous step:
- A burning tree becomes an empty cell.
- A tree catches fire if at least one of its four nearest neighbors (up, down, left, right — the von Neumann neighborhood) is burning.
- A tree with no burning neighbor catches fire with probability f (lightning).
- An empty cell grows a tree with probability p.
Fire therefore advances one cell per step, and diagonal neighbors do not ignite each other directly. Each lightning strike starts a new fire; a tree ignited by a burning neighbor is credited to that neighbor's fire, so a fire's size is the number of trees it burned. The code draws only the random events it needs: the gap between successive successes of independent trials with probability q is geometric, so it jumps from one growth or lightning event to the next instead of drawing a number for every cell.
Averaged over the second half of the run, the forest is in a statistical steady state, so trees grow as fast as they burn. Trees grow at rate p(1 − ρ)N per step and burn at fρN × (mean fire size), where ρ is the tree density and N = L². Setting the two equal gives
The lab reports both sides, which is a check on the simulation as much as a result.
Fire sizes and self-organized criticality
Drossel and Schwabl argued that in the limit of rare lightning (f/p → 0), with fires burning out much faster than trees regrow, the model is self-organized critical: without any parameter being tuned, the fire-size distribution approaches a power law up to a cutoff that grows as p/f grows. That made it a standard example alongside sandpile models.
Two cautions keep the claim honest. First, a finite run only approaches that limit: a large fire needs up to about L steps to burn through the grid, and in L steps a fraction 1 − (1 − p)L of the empty cells regrow — about 39% at the defaults — so growth and burning are only partly separated. Raise p and fire fronts can circulate through regrowing forest indefinitely; a single fire can then burn more trees than the grid has cells, which is why the largest-fire share can exceed 100%. Second, later and much larger simulations found that the scaling is not a clean power law, and whether the model is truly critical has been debated. The log-log plot here shows a broad distribution over several decades; it is not a measurement of a critical exponent.
The model is also not a wildfire model: real fire spread depends on wind, slope, fuel moisture and suppression, none of which appear here.
Example result
With p = 0.005, f = 0.000025 (p/f = 200) on a 100 × 100 grid and seed 42, lightning started 563 fires in 6,000 steps. The mean fire burned 318.9 trees, against 319.1 from the balance equation; the largest burned 5,101 trees, 51.0% of the cells. Over the second half of the run the tree density averaged 0.385.
| Mean tree density (second half) | 0.385 |
|---|---|
| Tree density at the end | 0.423 |
| Fires started by lightning | 563 |
| Mean fire size (trees) | 318.9 |
| Balance estimate (p/f)(1−ρ)/ρ | 319.1 |
| Largest fire (trees) | 5,101 |
| Largest fire ÷ number of cells | 51.0% |
| p / f | 200 |
Implementation
The page runs simulation.js from the package below; the Python file is a line-by-line port with the same random-number generator, so a given seed produces the same result in both languages. The excerpts show the core of each.
// One synchronous step of the Drossel–Schwabl forest-fire model on an L×L grid
// (edges wrap; neighbors are the 4 cells up, left, right and down):
// burning tree → empty
// tree → burning if a neighbor is burning, or if struck by lightning (prob. f)
// empty → tree with probability p
export function step(st, p, rng) {
const { L, grid, fireOf, sizes } = st;
const N = L * L, next = [];
for (const c of st.burning) { // fire spreads to neighbor trees
const x = c % L, y = (c - x) / L;
for (const n of [((y + L - 1) % L) * L + x, y * L + (x + L - 1) % L, y * L + (x + 1) % L, ((y + 1) % L) * L + x]) {
if (grid[n] === TREE) { grid[n] = IGNITE; fireOf[n] = fireOf[c]; sizes[fireOf[c]]++; next.push(n); }
}
}
events(N, p.f, rng, (c) => { // lightning starts a new fire
if (grid[c] === TREE) { grid[c] = IGNITE; fireOf[c] = sizes.length; sizes.push(1); next.push(c); }
});
events(N, p.p, rng, (c) => { // growth on empty cells
if (grid[c] === EMPTY) { grid[c] = TREE; st.trees++; }
});
for (const c of st.burning) grid[c] = EMPTY;
for (const c of next) grid[c] = FIRE;
st.trees -= next.length;
st.burning = next;
st.step++;
}def step(st, p, rng):
"""Drossel-Schwabl step on a wrapped L x L grid with 4 neighbors (up, left, right, down):
burning -> empty; tree -> burning if a neighbor burns or lightning strikes (prob f);
empty -> tree with probability p. All cells update from the same previous state."""
L, grid, fire_of, sizes = st["L"], st["grid"], st["fire_of"], st["sizes"]
nxt = []
for c in st["burning"]:
x, y = c % L, c // L
for n in (((y + L - 1) % L) * L + x, y * L + (x + L - 1) % L, y * L + (x + 1) % L, ((y + 1) % L) * L + x):
if grid[n] == TREE:
grid[n] = IGNITE
fire_of[n] = fire_of[c]
sizes[fire_of[c]] += 1
nxt.append(n)
for c in events(L * L, p["f"], rng):
if grid[c] == TREE:
grid[c] = IGNITE
fire_of[c] = len(sizes)
sizes.append(1)
nxt.append(c)
for c in events(L * L, p["p"], rng):
if grid[c] == EMPTY:
grid[c] = TREE
st["trees"] += 1
for c in st["burning"]:
grid[c] = EMPTY
for c in nxt:
grid[c] = FIRE
st["trees"] -= len(nxt)
st["burning"] = nxt
st["step"] += 1Run the full model from a terminal:
node simulation.js --p 0.005 --f 0.000025 --seed 42
python simulation.py --p 0.005 --f 0.000025 --seed 42Download the model
| File | Contents | Size |
|---|---|---|
README.md | How to run it, parameters, example result | 2.6 KB |
model.json | Parameters with ranges and defaults, outputs, the seed-42 example | 2.4 KB |
simulation.js | The model used on this page plus a command-line runner (Node 18+) | 9.0 KB |
simulation.py | Python 3.9+ port, standard library only | 7.4 KB |
example.csv | Result table from the example run | 7.1 KB |
LICENSE.txt | MIT License | 1.0 KB |
Assumptions & limitations
- Square lattice, four neighbors, wrapped edges. Fire cannot jump diagonally or across gaps; there are no boundaries, wind or terrain.
- Synchronous steps; one step to burn. Every tree burns for exactly one step, so the ratio of growth time to burning time is set by p.
- Independent, memoryless events. Growth and lightning are independent coin flips per cell per step.
- Starts from bare ground. The first part of the run is a transient; averages use the second half only.
- Fire identity is a bookkeeping choice. When two fires reach the same tree in the same step, it is credited to whichever burning neighbor was processed first.
References
- Drossel, B. & Schwabl, F. (1992). Self-organized critical forest-fire model. Physical Review Letters, 69(11), 1629–1632. doi.org/10.1103/PhysRevLett.69.1629