Computing the Fire Weather Index (FWI) in Python
The Canadian Forest Fire Weather Index (FWI) System turns four daily noon observations — temperature, relative humidity, wind speed, and 24-hour rainfall — into six numeric codes and indices that summarize fuel dryness and expected fire intensity. This page implements those equations in Python so you can produce the fire-weather layer that feeds the wildfire risk modeling workflow, itself part of the Fire Risk & Fuel Assessment section. The FWI System (Van Wagner 1987) is a set of recursive daily calculations: three moisture codes track fuel layers of increasing depth, and three indices combine them with wind into rate-of-spread and intensity proxies.
When to compute FWI yourself
The cffdrs R package and several web services already publish FWI, so implement it directly only when you need control over the inputs or want it inside a Python raster pipeline.
| Situation | Use a service / package | Implement in Python |
|---|---|---|
| Single station, historical archive | Yes — fast and validated | Overkill |
| Gridded FWI over a raster domain | Awkward to wire up | Yes — vectorize over pixels |
| Custom overwintering / start-up rules | Limited control | Yes — you own the recursion |
| Reproducible research artifact | External dependency | Yes — pin the equations |
For a one-off station series, a maintained package is the right call. For a spatial layer aligned to your fuel and terrain grids, a numpy implementation that runs per pixel is cleaner.
The six components
Three moisture codes and three fire-behavior indices form a dependency chain. FFMC, DMC, and DC each carry over from the previous day; the indices are computed fresh each day from the codes plus wind.
- FFMC — Fine Fuel Moisture Code: litter and cured fine fuels (~1 cm), responds in hours. Drives ignition and initial spread.
- DMC — Duff Moisture Code: loosely compacted organic layers (~5–10 cm), responds over days.
- DC — Drought Code: deep compact organic matter (~10–20 cm), responds over weeks to months; the seasonal drought memory.
- ISI — Initial Spread Index: combines FFMC with wind to estimate expected rate of spread.
- BUI — Buildup Index: combines DMC and DC into total fuel available for combustion.
- FWI — Fire Weather Index: combines ISI and BUI into a single fire-intensity number.
Minimal reproducible example
The functions below implement the standard FWI equations for one day, taking yesterday’s codes and today’s noon weather. They are the exact recursions from the Canadian system; wind is in km/h, rain in mm, temperature in °C, humidity in %.
import numpy as np
def fine_fuel_moisture_code(temp, rh, wind, rain, ffmc_prev):
"""FFMC from noon weather and yesterday's FFMC."""
mo = 147.2 * (101.0 - ffmc_prev) / (59.5 + ffmc_prev) # prior moisture
if rain > 0.5:
rf = rain - 0.5
if mo <= 150.0:
mo += (42.5 * rf * np.exp(-100.0 / (251.0 - mo))
* (1.0 - np.exp(-6.93 / rf)))
else:
mo += (42.5 * rf * np.exp(-100.0 / (251.0 - mo))
* (1.0 - np.exp(-6.93 / rf))
+ 0.0015 * (mo - 150.0) ** 2 * np.sqrt(rf))
mo = min(mo, 250.0)
# Equilibrium moisture contents for drying (Ed) and wetting (Ew).
ed = (0.942 * rh ** 0.679 + 11.0 * np.exp((rh - 100.0) / 10.0)
+ 0.18 * (21.1 - temp) * (1.0 - np.exp(-0.115 * rh)))
ew = (0.618 * rh ** 0.753 + 10.0 * np.exp((rh - 100.0) / 10.0)
+ 0.18 * (21.1 - temp) * (1.0 - np.exp(-0.115 * rh)))
if mo > ed: # drying
ko = (0.424 * (1.0 - (rh / 100.0) ** 1.7)
+ 0.0694 * np.sqrt(wind) * (1.0 - (rh / 100.0) ** 8))
kd = ko * 0.581 * np.exp(0.0365 * temp)
m = ed + (mo - ed) * 10.0 ** (-kd)
elif mo < ew: # wetting
kl = (0.424 * (1.0 - ((100.0 - rh) / 100.0) ** 1.7)
+ 0.0694 * np.sqrt(wind) * (1.0 - ((100.0 - rh) / 100.0) ** 8))
kw = kl * 0.581 * np.exp(0.0365 * temp)
m = ew - (ew - mo) * 10.0 ** (-kw)
else:
m = mo
return 59.5 * (250.0 - m) / (147.2 + m)
def duff_moisture_code(temp, rh, rain, dmc_prev, day_length):
"""DMC from noon weather, yesterday's DMC, and month day-length factor."""
t = max(temp, -1.1)
if rain > 1.5:
re = 0.92 * rain - 1.27
mo = 20.0 + np.exp(5.6348 - dmc_prev / 43.43)
if dmc_prev <= 33.0:
b = 100.0 / (0.5 + 0.3 * dmc_prev)
elif dmc_prev <= 65.0:
b = 14.0 - 1.3 * np.log(dmc_prev)
else:
b = 6.2 * np.log(dmc_prev) - 17.2
mr = mo + 1000.0 * re / (48.77 + b * re)
pr = 244.72 - 43.43 * np.log(mr - 20.0)
dmc_prev = max(pr, 0.0)
k = 1.894 * (t + 1.1) * (100.0 - rh) * day_length * 1e-6
return dmc_prev + 100.0 * k
def drought_code(temp, rain, dc_prev, day_length_factor):
"""DC from noon temperature, rain, yesterday's DC, and month factor."""
t = max(temp, -2.8)
if rain > 2.8:
rd = 0.83 * rain - 1.27
qo = 800.0 * np.exp(-dc_prev / 400.0)
qr = qo + 3.937 * rd
dr = 400.0 * np.log(800.0 / qr)
dc_prev = max(dr, 0.0)
v = 0.36 * (t + 2.8) + day_length_factor
v = max(v, 0.0)
return dc_prev + 0.5 * v
def initial_spread_index(ffmc, wind):
"""ISI from FFMC and wind speed (km/h)."""
m = 147.2 * (101.0 - ffmc) / (59.5 + ffmc)
ff = 91.9 * np.exp(-0.1386 * m) * (1.0 + m ** 5.31 / 4.93e7)
return 0.208 * np.exp(0.05039 * wind) * ff
def buildup_index(dmc, dc):
"""BUI from DMC and DC."""
if dmc == 0.0 and dc == 0.0:
return 0.0
if dmc <= 0.4 * dc:
return 0.8 * dmc * dc / (dmc + 0.4 * dc)
return dmc - (1.0 - 0.8 * dc / (dmc + 0.4 * dc)) * (0.92 + (0.0114 * dmc) ** 1.7)
def fire_weather_index(isi, bui):
"""FWI from ISI and BUI."""
if bui <= 80.0:
fd = 0.626 * bui ** 0.809 + 2.0
else:
fd = 1000.0 / (25.0 + 108.64 * np.exp(-0.023 * bui))
b = 0.1 * isi * fd
if b > 1.0:
return np.exp(2.72 * (0.434 * np.log(b)) ** 0.647)
return b
Day length is not a free parameter — it is a fixed monthly lookup from Van Wagner (1987). Drive the recursion from a pandas frame of daily observations:
import pandas as pd
# Effective day lengths (DMC) and day-length factors (DC) by month, mid-latitudes.
DMC_DAY_LENGTH = [6.5, 7.5, 9.0, 12.8, 13.9, 13.9, 12.4, 10.9, 9.4, 8.0, 7.0, 6.0]
DC_DAY_FACTOR = [-1.6, -1.6, -1.6, 0.9, 3.8, 5.8, 6.4, 5.0, 2.4, 0.4, -1.6, -1.6]
df = pd.read_csv("noon_weather.csv", parse_dates=["date"]) # temp, rh, wind, rain
ffmc, dmc, dc = 85.0, 6.0, 15.0 # standard start-up values
rows = []
for _, r in df.iterrows():
mi = r["date"].month - 1
ffmc = fine_fuel_moisture_code(r.temp, r.rh, r.wind, r.rain, ffmc)
dmc = duff_moisture_code(r.temp, r.rh, r.rain, dmc, DMC_DAY_LENGTH[mi])
dc = drought_code(r.temp, r.rain, dc, DC_DAY_FACTOR[mi])
isi = initial_spread_index(ffmc, r.wind)
bui = buildup_index(dmc, dc)
fwi = fire_weather_index(isi, bui)
rows.append((r["date"], ffmc, dmc, dc, isi, bui, fwi))
out = pd.DataFrame(rows, columns=["date", "FFMC", "DMC", "DC", "ISI", "BUI", "FWI"])
Parameter reference
Inputs are daily values measured at local noon standard time (roughly the time of peak burning), and the six outputs are open-ended indices — higher means drier or more intense.
| Symbol | Meaning | Unit | Typical range |
|---|---|---|---|
temp |
Noon dry-bulb temperature | °C | -20 to 45 |
rh |
Noon relative humidity | % | 0–100 |
wind |
Noon 10 m wind speed | km/h | 0–60 |
rain |
24-hour accumulated rainfall | mm | 0–100+ |
| FFMC | Fine Fuel Moisture Code | index | 0–99 (fire risk > ~85) |
| DMC | Duff Moisture Code | index | 0–150+ |
| DC | Drought Code | index | 0–1000+ |
| ISI | Initial Spread Index | index | 0–50+ |
| BUI | Buildup Index | index | 0–200+ |
| FWI | Fire Weather Index | index | 0–100+ (extreme > ~30) |
Standard fair-weather start-up values are FFMC 85, DMC 6, DC 15, applied on the first day of the fire season after snowmelt.
Expected output and verification
Van Wagner and Pickett (1985) publish a worked example you can assert against. For the canonical test day — 17 °C, 42% RH, 25 km/h wind, 0 mm rain, starting from FFMC 85, DMC 6, DC 15 — the system yields FFMC ≈ 87.7, DMC ≈ 8.5, DC ≈ 19.0, and an FWI in the mid-teens. Codify a tolerance check:
ffmc = fine_fuel_moisture_code(17.0, 42.0, 25.0, 0.0, 85.0)
assert abs(ffmc - 87.7) < 0.5, f"FFMC off: {ffmc:.2f}"
assert 0.0 <= buildup_index(8.5, 19.0) < 200.0
If your FFMC drifts far from the reference, the usual cause is a units slip — feeding wind in m/s, or rainfall in inches — rather than a coded equation error.
Common pitfalls
- Overwintering the Drought Code. DC carries drought memory across the dormant season. Restarting it at 15 every spring ignores a dry winter; apply an overwintering DC adjustment from autumn moisture and winter precipitation instead of a blind reset.
- Using non-noon observations. The equations are calibrated to local noon standard time (≈1200 LST), the daily peak-burning proxy. Feeding morning or midnight data biases FFMC and ISI low; resample to noon or apply a diurnal correction.
- Ignoring latitude in day length. The month lookups above are tuned to mid-latitudes. Near the equator or the poles, day-length effects on DMC and DC change; use latitude-adjusted day-length tables for those regions.
- Feeding wrong units. Wind must be km/h and rain mm. A m/s wind or inch rainfall passes silently through the arithmetic and produces plausible-but-wrong indices — the failure has no exception, only a biased number.
Related
- Wildfire Risk Modeling in Python — the parent workflow that consumes this fire-weather layer
- Fire Risk & Fuel Assessment — the section this task belongs to
- Fire Behavior and Rate-of-Spread Modeling — where ISI-style wind effects reappear in the physical spread model
- Fuel Load Mapping from LiDAR — the fuel side of the risk equation
Frequently Asked Questions
Do I need yesterday's values to compute today's FWI?
Yes for the three moisture codes. FFMC, DMC, and DC are recursive — each day’s value depends on the previous day’s code plus today’s weather. On the first day of the season you seed them with standard start-up values (FFMC 85, DMC 6, DC 15). The three indices (ISI, BUI, FWI) are computed fresh each day from the current codes and wind, with no carry-over of their own.
What weather inputs does the FWI System require?
Four daily observations taken at local noon standard time: dry-bulb temperature in °C, relative humidity in %, 10 m wind speed in km/h, and 24-hour accumulated rainfall in mm. Getting the units right — especially km/h wind and mm rain — matters, because a units mismatch produces plausible but wrong indices with no error raised.