Modeling Rate of Spread with Rothermel in Python

This page gives a minimal, correct Python implementation of the Rothermel (1972) surface rate-of-spread equation for a single fuel model, with the wind and slope factors, returning rate of spread in metres per minute. It is the focused, copy-pasteable core of the broader fire behavior and rate-of-spread modeling guide, which sits in the Fire Risk & Fuel Assessment section. Start here when you want one function you can trust before scaling it across a raster; the parent guide covers the gridded, multi-fuel version.

The whole model reduces to one ratio — heat supplied to unburned fuel over heat needed to ignite it — scaled up by wind and slope. Everything below is the arithmetic to compute each term for one homogeneous fuel bed.

Getting a single fuel model exactly right is the foundation for everything downstream: the same function, broadcast over a raster of fuel-model codes and slopes, becomes the landscape rate-of-spread surface described in the parent guide, and its output feeds Byram fireline intensity and flame length. So it pays to implement it once, test it against a known benchmark, and only then vectorize. The version here deliberately handles one dead-fuel size class; that keeps every term visible, which is what you want when you are learning the model or debugging a discrepancy with BehavePlus.

When to use a single-fuel implementation

A one-fuel-model function is the right tool for calibration, teaching, and unit tests; reach for a package or the gridded version when you have spatial or multi-fuel-class data.

Situation Single-fuel function (this page) Gridded / package
Understanding or unit-testing the equations Yes Obscures the math
Calibrating against a BehavePlus benchmark Yes Overkill
Landscape raster of many fuel models No Yes — see the parent guide
Operational wind-limit and crown logic Add manually Use an established engine

Minimal reproducible example

The function works entirely in imperial units internally — the units the Rothermel coefficients were fit in — and converts the result to metric at the end. It implements the standard equations for a single dead-fuel class (e.g. Scott & Burgan GR1/GR2 grass, using the fine dead-fuel load).

import numpy as np

PARTICLE_DENSITY = 32.0      # lb/ft^3
TOTAL_MINERAL = 0.0555       # S_T
EFFECTIVE_MINERAL = 0.010    # S_e
HEAT_CONTENT = 8000.0        # Btu/lb (h)


def rothermel_ros(w0, sigma, depth, mf, mx, wind_mid_mph, slope_frac):
    """Rothermel surface rate of spread for one fuel model.

    w0            oven-dry fuel load           (lb/ft^2)
    sigma         surface-area-to-volume ratio (1/ft)
    depth         fuel bed depth               (ft)
    mf            fuel moisture                (fraction, e.g. 0.08)
    mx            moisture of extinction       (fraction, e.g. 0.15)
    wind_mid_mph  midflame wind speed          (mph)
    slope_frac    slope as rise/run            (tan of slope angle)

    Returns rate of spread in metres per minute.
    """
    # --- fuel-bed geometry ---
    bulk_density = w0 / depth                       # rho_b
    packing = bulk_density / PARTICLE_DENSITY       # beta
    packing_opt = 3.348 * sigma ** -0.8189          # beta_op
    net_load = w0 / (1.0 + TOTAL_MINERAL)           # w_n

    # --- damping coefficients ---
    rm = min(mf / mx, 1.0)                           # capped moisture ratio
    eta_moisture = 1.0 - 2.59 * rm + 5.11 * rm ** 2 - 3.52 * rm ** 3
    eta_mineral = min(0.174 * EFFECTIVE_MINERAL ** -0.19, 1.0)

    # --- reaction intensity I_R (Btu/ft^2/min) ---
    a = 133.0 * sigma ** -0.7913
    gamma_max = sigma ** 1.5 / (495.0 + 0.0594 * sigma ** 1.5)
    gamma = (gamma_max * (packing / packing_opt) ** a
             * np.exp(a * (1.0 - packing / packing_opt)))
    reaction_intensity = gamma * net_load * HEAT_CONTENT * eta_moisture * eta_mineral

    # --- propagating flux ratio xi ---
    xi = (np.exp((0.792 + 0.681 * sigma ** 0.5) * (packing + 0.1))
          / (192.0 + 0.2595 * sigma))

    # --- wind factor phi_w (wind in ft/min) ---
    wind_ft_min = wind_mid_mph * 88.0
    c = 7.47 * np.exp(-0.133 * sigma ** 0.55)
    b = 0.02526 * sigma ** 0.54
    e = 0.715 * np.exp(-3.59e-4 * sigma)
    phi_w = c * wind_ft_min ** b * (packing / packing_opt) ** -e

    # --- slope factor phi_s ---
    phi_s = 5.275 * packing ** -0.3 * slope_frac ** 2

    # --- heat sink ---
    epsilon = np.exp(-138.0 / sigma)                # effective heating number
    q_ig = 250.0 + 1116.0 * mf                      # heat of preignition (Btu/lb)
    heat_sink = bulk_density * epsilon * q_ig

    ros_ft_min = reaction_intensity * xi * (1.0 + phi_w + phi_s) / heat_sink
    return ros_ft_min * 0.3048                       # ft/min -> m/min


if __name__ == "__main__":
    # Scott & Burgan GR2 (low-load dry grass), 8% moisture, 5 mph midflame, 20% slope.
    ros = rothermel_ros(w0=0.0046, sigma=2000.0, depth=1.0,
                        mf=0.08, mx=0.15, wind_mid_mph=5.0, slope_frac=0.20)
    print(f"Rate of spread: {ros:.2f} m/min")
The Rothermel spread equation as a block diagram A numerator block combines reaction intensity with the propagating flux ratio and then multiplies by one plus the wind factor plus the slope factor. A denominator block combines oven-dry bulk density, the effective heating number and the heat of preignition. The quotient of the two is the rate of spread. Each block notes which input it is most sensitive to. Iᵣ · ξ · (1 + Φḯ + Φₛ) reaction intensity × propagating flux ratio × wind and slope factors ρᵇ · ε · Qᵢᵗ bulk density × effective heating number × heat of preignition = R Everything that makes fire faster is in the numerator; everything that resists it is below the line. Moisture enters through the heat of preignition, which is why its effect is so strongly non-linear.

How the terms combine

Reading the function top to bottom traces the physics. The fuel-bed geometry block converts the fuel model into a packing ratio (how tightly the fuel is stacked) and compares it to the optimum packing — fuel that is too sparse or too dense burns slower than fuel at the optimum. The damping block penalizes reaction intensity for moisture and mineral content: as fuel moisture approaches the moisture of extinction , the cubic damping polynomial drives the moisture coefficient toward zero. The reaction-intensity block multiplies the optimum reaction velocity , the net (mineral-free) load, the heat content, and the two damping coefficients into , the heat release per unit area.

The propagating flux ratio is the fraction of that heat that actually reaches the unburned fuel ahead of the front — it depends on the fuel-bed geometry, not the weather. The wind and slope factors are pure multipliers on the no-wind, no-slope spread: grows with midflame wind, and with the square of the slope. Finally the heat sink — bulk density times effective heating number times heat of preignition — is the denominator, the energy the fire must spend to bring the next parcel of fuel to ignition. Divide supply by sink, scale by , and convert feet-per-minute to metres-per-minute. Every line of the function maps to one of those physical quantities, which is exactly why a single-fuel implementation is the best place to build intuition.

Parameter reference

Every argument below changes the result; the fuel-bed geometry terms come from the fuel model, the moisture and wind terms from the day’s conditions.

Argument Symbol Type Typical value Ecological rationale
w0 float (lb/ft²) 0.002–0.05 Oven-dry load of the fine dead-fuel size class; more fuel, more reaction heat
sigma float (1/ft) 1500–3500 Surface-area-to-volume ratio; fine fuels (high σ) ignite and spread faster
depth float (ft) 0.2–3.0 Fuel-bed depth; sets bulk density and packing ratio
mf float (frac) 0.03–0.30 Dead fuel moisture; damps reaction intensity toward extinction
mx float (frac) 0.12–0.40 Moisture of extinction; above it the fuel will not carry fire
wind_mid_mph float (mph) 0–20 Midflame wind, not 10 m wind; drives
slope_frac float 0–1.5 Slope as rise/run; enters as its square
Spread rate by fuel model class under identical weather Horizontal bars show modelled rate of spread for six fuel model classes under one identical weather scenario. Short grass spreads fastest at about sixty metres per minute, tall grass slightly less, shrub models around thirty, timber understory around twelve, timber litter around four, and slash around eight. A note records that the fuel model choice changes the answer more than any weather input. short grass tall grass shrub timber understory slash timber litter 60 m/min 53 30 12 8 4 Identical weather; only the fuel model differs. Assigning the wrong model matters more than any plausible error in the wind speed.

Expected output and verification

For the GR2 example — dry grass, 8% moisture, 5 mph midflame wind, 20% slope — the function returns a rate of spread of a few metres per minute, consistent with BehavePlus for that fuel model. The most useful checks are monotonicity and the extinction boundary:

base = rothermel_ros(0.0046, 2000.0, 1.0, 0.08, 0.15, 0.0, 0.0)
windy = rothermel_ros(0.0046, 2000.0, 1.0, 0.08, 0.15, 10.0, 0.0)
steep = rothermel_ros(0.0046, 2000.0, 1.0, 0.08, 0.15, 0.0, 0.50)

assert base > 0.0, "dry grass must spread with no wind"
assert windy > base, "more wind must increase spread"
assert steep > base, "steeper slope must increase spread"

# At/above the moisture of extinction, spread collapses to ~zero.
dead = rothermel_ros(0.0046, 2000.0, 1.0, 0.15, 0.15, 5.0, 0.0)
assert dead < base, "fuel at extinction moisture barely spreads"

When mf equals mx, the capped moisture ratio drives the damping coefficient — and thus reaction intensity — to its floor, so rate of spread should nearly vanish. That behavior is the cleanest single test that your damping term is coded correctly.

One spread rate, three very different edges An elliptical fire perimeter is drawn around an ignition point set back from the centre. The head, at the downwind end, advances fastest. The flanks advance at roughly a fifth of that rate. The backing edge, upwind, advances slowest of all. Arrows of proportional length mark each. A note records that a single modelled rate describes only the head, so applying it around the whole perimeter over-predicts area by a large factor. ignition head — 100% of the modelled rate flank — about 20% flank — about 20% back — about 8% Length to breadth grows with wind; a circular assumption over-predicts area several-fold.

Common pitfalls

  • Unit inconsistency. The coefficients are imperial. Pass fuel load in lb/ft², SAV in 1/ft, depth in ft, and wind in mph (converted to ft/min inside). Feeding SI values gives a plausible-looking but wrong number with no error — convert at the boundary only.
  • Uncapped moisture damping. If mf > mx and you do not cap mf / mx at 1.0, the damping polynomial goes negative and reaction intensity turns negative. Always min(mf / mx, 1.0).
  • 10 m wind in place of midflame wind. Passing the open 10 m or 20 ft wind instead of the midflame wind overpredicts spread badly. Apply a wind-adjustment factor first.
  • No wind limit at extreme wind. Rothermel’s grows without bound; at very high winds it overpredicts. For extreme-wind scenarios cap the effective wind at the reaction-intensity-based limit rather than trusting the raw factor.

Frequently Asked Questions

What units does this Rothermel function expect?

Imperial, because that is what the model’s coefficients were fit in: fuel load in lb/ft², surface-area-to-volume ratio in 1/ft, fuel-bed depth in ft, moistures as fractions, and midflame wind in mph (converted to ft/min internally). The output is converted to metres per minute at the end. Do not pass SI values — the arithmetic will not error, it will just be wrong.

Which fuel load do I use for a multi-size-class fuel model?

For this single-class function, use the fine dead-fuel load (the 1-hour class) with its surface-area-to-volume ratio, since fine fuels dominate surface spread. A full treatment weights multiple dead and live size classes by their SAV and load — that characteristic-value weighting is what the gridded parent guide and operational engines implement.