OS-026 · Agents & cellular automata

Forest fire model

Cells of a grid are empty, hold a tree or are burning. Trees grow slowly, lightning is rare, and fire jumps from tree to neighboring tree. The forest never settles: it thickens until fires can cross it, burns back, and regrows, producing fires from a single tree to a large part of the grid.

Interactive model

OS-026 · forest-fire · seeded
treeburningrecently burned
fires per unit size
tree densitysecond-half mean
Parameters
The forest starts empty; averages use the second half
Same seed, same run — here, in simulation.js and in simulation.py.
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

Example run: default parameters, seed 42.

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:

  1. A burning tree becomes an empty cell.
  2. A tree catches fire if at least one of its four nearest neighbors (up, down, left, right — the von Neumann neighborhood) is burning.
  3. A tree with no burning neighbor catches fire with probability f (lightning).
  4. 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

mean fire size ≈ (p/f) · (1 − ρ) / ρ

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.

Output of forest-fire with p=0.005 f=0.000025 size=100 steps=6000 seed=42
Mean tree density (second half)0.385
Tree density at the end0.423
Fires started by lightning563
Mean fire size (trees)318.9
Balance estimate (p/f)(1−ρ)/ρ319.1
Largest fire (trees)5,101
Largest fire ÷ number of cells51.0%
p / f200

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.

JavaScript — simulation.js
// 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++;
}
Python — simulation.py
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"] += 1

Run the full model from a terminal:

Shell
node simulation.js --p 0.005 --f 0.000025 --seed 42
python simulation.py --p 0.005 --f 0.000025 --seed 42

Download the model

forest-fire.zip6 files · 12.8 KBDownload .zip
Files in forest-fire.zip
FileContentsSize
README.mdHow to run it, parameters, example result2.6 KB
model.jsonParameters with ranges and defaults, outputs, the seed-42 example2.4 KB
simulation.jsThe model used on this page plus a command-line runner (Node 18+)9.0 KB
simulation.pyPython 3.9+ port, standard library only7.4 KB
example.csvResult table from the example run7.1 KB
LICENSE.txtMIT License1.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

  1. 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