How to Calculate Fire Weather Indices in Python

On this page

Problem statement

Fire behaviour follows the weather: hot, dry air dries fuels, wind drives flames and carries embers, and the humidity that returns at night usually slows a fire down until morning. Fire weather indices combine these into one number that can be mapped, ranked and compared with what a fire actually did. The simplest widely used one, the Fosberg Fire Weather Index (FFWI), needs only temperature, relative humidity and wind speed. Hourly values of all three are available for any place on Earth from reanalysis, without an account.

This page computes the FFWI at the Park Fire's origin for three weeks around its start on 24 July 2024 from Open-Meteo's ERA5 archive. It looks at how humid the nights got, and compares hourly fire weather with the fire's radiative power measured by GOES-18.

Quick answer

Fetch hourly temperature (Β°F), humidity (%) and wind (mph), compute the equilibrium moisture content, damp it, and scale by wind:

def ffwi(t_f, rh, wind_mph):
    m = np.where(rh < 10, 0.03229 + 0.281073 * rh - 0.000578 * rh * t_f,
        np.where(rh < 50, 2.22749 + 0.160107 * rh - 0.01478 * t_f,
                 21.0606 + 0.005565 * rh ** 2 - 0.00035 * rh * t_f - 0.483199 * rh))   # equilibrium moisture content, %
    eta = 1 - 2 * (m / 30) + 1.5 * (m / 30) ** 2 - 0.5 * (m / 30) ** 3                  # moisture damping
    return eta * np.sqrt(1 + wind_mph ** 2) / 0.3002

wx["ffwi"] = ffwi(wx.temperature_2m, wx.relative_humidity_2m, wx.wind_speed_10m)
daily = wx.resample("D").agg({"temperature_2m": "max", "relative_humidity_2m": "min", "wind_gusts_10m": "max", "ffwi": "max"})
daily.index = daily.index.date
print(daily.loc[pd.Timestamp("2024-07-21").date():pd.Timestamp("2024-07-28").date()].round(1).to_string())
            temperature_2m  relative_humidity_2m  wind_gusts_10m  ffwi
2024-07-21           101.3                    17            19.0  21.7
2024-07-22           106.3                    15            18.8  21.2
2024-07-23           107.5                    15            22.6  28.0
2024-07-24           103.6                    15            40.3  36.9
2024-07-25           102.0                    11            25.7  33.0
2024-07-26           101.6                    10            26.4  31.5
2024-07-27            85.5                    24            26.8  28.9
2024-07-28            91.1                    18            22.6  25.6
Two stacked time series from noon on 24 July to 18:00 on 26 July 2024 at the Park Fire origin: the Fosberg Fire Weather Index peaks above 35 on the afternoon of 24 July and around 30 to 33 on the next two days while humidity stays between 10 and 30 percent; below, GOES-18 fire radiative power climbs to 45 gigawatts in the first night, peaks at 58 gigawatts on the afternoon of 25 July, falls to near zero on the morning of 26 July and rises again to about 50 gigawatts that afternoon. Nights are shaded. Two stacked time series from noon on 24 July to 18:00 on 26 July 2024 at the Park Fire origin: the Fosberg Fire Weather Index peaks above 35 on the afternoon of 24 July and around 30 to 33 on the next two days while humidity stays between 10 and 30 percent; below, GOES-18 fire radiative power climbs to 45 gigawatts in the first night, peaks at 58 gigawatts on the afternoon of 25 July, falls to near zero on the morning of 26 July and rises again to about 50 gigawatts that afternoon. Nights are shaded.
Fire weather and the fire's own power, hour by hour. The fire kept burning hard through its first two nights, when humidity stayed below 30%.

Context, overnight recovery, what the fire did

1. Put the day in context

A high index means little without knowing what is normal for the place. Rank the days of the three-week window:

top = daily.loc[:pd.Timestamp("2024-08-04").date()].sort_values("ffwi", ascending=False).head(4)
print(top[["ffwi", "wind_gusts_10m"]].round(1).to_string())
print(f"window median of daily maximum FFWI: {daily.ffwi.loc[:pd.Timestamp('2024-08-04').date()].median():.1f}")
            ffwi  wind_gusts_10m
2024-07-24  36.9            40.3
2024-07-19  33.1            26.6
2024-07-25  33.0            25.7
2024-07-26  31.5            26.4
window median of daily maximum FFWI: 25.0

The fire's first day ranks first, with the strongest gusts of the three weeks. Its next two days follow close behind.

2. Check the overnight recovery

Humidity normally rises at night, fuels take up moisture and fires lie down. Poor recovery, a night that stays dry and windy, lets a fire keep running. Take each night from 22:00 to 08:00, labelled by the evening it starts:

night = wx[(wx.index.hour >= 22) | (wx.index.hour <= 8)]
nights = night.groupby((night.index - pd.Timedelta(hours=9)).date).agg(
    rh_max=("relative_humidity_2m", "max"), wind_mean=("wind_speed_10m", "mean"))
nights = nights.loc[pd.Timestamp("2024-07-15").date():pd.Timestamp("2024-08-04").date()]
print(nights.loc[pd.Timestamp("2024-07-21").date():pd.Timestamp("2024-07-27").date()].round(1).to_string())
print(f"driest nights in the window: {', '.join(f'{d:%d %b} ({r}%)' for d, r in nights.rh_max.nsmallest(3).items())}")
print(f"windiest nights: {', '.join(f'{d:%d %b} ({v:.1f} mph)' for d, v in nights.wind_mean.nlargest(3).items())}")
            rh_max  wind_mean
2024-07-21      55        2.8
2024-07-22      37        3.9
2024-07-23      43        7.6
2024-07-24      29       10.3
2024-07-25      27        9.0
2024-07-26      62        9.7
2024-07-27      70        3.4
driest nights in the window: 25 Jul (27%), 24 Jul (29%), 01 Aug (29%)
windiest nights: 24 Jul (10.3 mph), 26 Jul (9.7 mph), 25 Jul (9.0 mph)

The fire's first three nights were the three windiest of the window. On the first two, humidity never rose above 29%; on the third it reached 62%.

Bar chart of the highest overnight relative humidity for each night from 15 July to 4 August 2024 at the Park Fire origin, with mean overnight wind as a line: the nights of 24 and 25 July, in red, peaked at only 29 and 27 percent humidity with the strongest winds of the period, 10.3 and 9.0 miles per hour; on 26 July humidity recovered to 62 percent; most other nights reached 35 to 70 percent with winds of 2 to 6 miles per hour. Bar chart of the highest overnight relative humidity for each night from 15 July to 4 August 2024 at the Park Fire origin, with mean overnight wind as a line: the nights of 24 and 25 July, in red, peaked at only 29 and 27 percent humidity with the strongest winds of the period, 10.3 and 9.0 miles per hour; on 26 July humidity recovered to 62 percent; most other nights reached 35 to 70 percent with winds of 2 to 6 miles per hour.
The fire's first nights were dry and windy, the conditions that let a fire keep running after dark.

3. Compare with what the fire did

GOES-18 fire radiative power (FRP), summed over the fire every 15 minutes and averaged by hour, measures how hard the fire burned. Rank correlations show which weather variable tracked it most closely over its first two days:

both = wx.join(FRP.rename("frp"), how="inner")
corr = both[["relative_humidity_2m", "vapour_pressure_deficit", "ffwi", "wind_speed_10m"]].corrwith(both.frp, method="spearman")
print(f"{len(both)} hours; Spearman correlation with hourly FRP:")
print(corr.round(2).to_string())
50 hours; Spearman correlation with hourly FRP:
relative_humidity_2m      -0.63
vapour_pressure_deficit    0.43
ffwi                       0.29
wind_speed_10m             0.18

Humidity tracks the fire's power better than the combined index. FFWI gives wind a large weight, and on these days the fire's intensity followed the daily cycle of heat and dryness more closely than the wind. A growing fire also burns harder simply because it is bigger, which no weather index can see.

A surface hot-dry-windy proxy

The data used on this page

Hourly ERA5 reanalysis from the Open-Meteo historical weather API for the grid cell nearest the Park Fire's origin (39.80Β° N, 121.79Β° W), 14 July to 5 August 2024, in local time; and GOES-18 ABI fire detections (ABI-L2-FDCC) from NOAA's archive on AWS, one scan every 15 minutes from 21:00 UTC on 24 July to 23:45 UTC on 26 July.

import re
from pathlib import Path

import numpy as np
import pandas as pd
import requests
import xarray as xr
from pyproj import Proj

r = requests.get("https://archive-api.open-meteo.com/v1/archive", params=dict(
    latitude=39.80, longitude=-121.79, start_date="2024-07-14", end_date="2024-08-05",
    hourly="temperature_2m,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,wind_direction_10m,vapour_pressure_deficit",
    timezone="America/Los_Angeles", temperature_unit="fahrenheit", wind_speed_unit="mph"), timeout=60).json()
wx = pd.DataFrame(r["hourly"])
wx.index = pd.to_datetime(wx.pop("time")).dt.tz_localize("America/Los_Angeles")
Path("data/fdc").mkdir(parents=True, exist_ok=True)
B = "https://noaa-goes18.s3.amazonaws.com/"
FILES = []
for day in (206, 207, 208):
    for h in range(21 if day == 206 else 0, 24):
        text = requests.get(B, params={"list-type": 2, "prefix": f"ABI-L2-FDCC/2024/{day}/{h:02d}/"}, timeout=60).text
        for k in re.findall(r"<Key>([^<]+)</Key>", text):
            if int(re.search(r"_s\d{9}(\d{2})", k).group(1)) % 15 < 5:
                f = Path("data/fdc", k.rsplit("/", 1)[1])
                if not f.exists():
                    f.write_bytes(requests.get(B + k, timeout=120).content)
                FILES.append(f)
with xr.open_dataset(FILES[0], engine="h5netcdf") as ds:
    p = ds.goes_imager_projection
    meanings = dict(zip(ds.Mask.attrs["flag_values"].tolist(), ds.Mask.attrs["flag_meanings"].split()))
    proj = Proj(proj="geos", h=p.perspective_point_height, lon_0=p.longitude_of_projection_origin, sweep=p.sweep_angle_axis,
                a=p.semi_major_axis, b=p.semi_minor_axis)
    x, y = ds.x.values * p.perspective_point_height, ds.y.values * p.perspective_point_height
codes = [c for c, n in meanings.items() if n.endswith("fire_pixel") and "no_fire" not in n]
bx, by = proj([-122.3, -121.3, -122.3, -121.3], [39.6, 39.6, 40.5, 40.5])
IX, IY = (x >= min(bx)) & (x <= max(bx)), (y >= min(by)) & (y <= max(by))
scans = {}
for f in FILES:
    with xr.open_dataset(f, engine="h5netcdf") as ds:
        fire = np.isin(ds.Mask.values[np.ix_(IY, IX)], codes)
        t = pd.Timestamp(ds.attrs["time_coverage_start"].rstrip("Z"), tz="UTC").tz_convert("America/Los_Angeles")
        scans[t] = float(np.nansum(np.where(fire, ds.Power.values[np.ix_(IY, IX)], 0)))
FRP = pd.Series(scans).sort_index().resample("h").mean().loc["2024-07-24 15:00":]
print(f"{len(wx)} hours of weather at {r['latitude']:.2f}, {r['longitude']:.2f} ({r['elevation']:.0f} m); "
      f"{len(FILES)} GOES scans, FRP peak {FRP.max() / 1000:.1f} GW")
552 hours of weather at 39.82, -121.84 (232 m); 204 GOES scans, FRP peak 57.6 GW

Example 1: a surface hot-dry-windy proxy

The Hot-Dry-Windy Index multiplies vapour pressure deficit by wind speed, both taken as the maximum in the lowest 500 m of the atmosphere. With only surface values, the same product is a proxy:

wx["hdw_proxy"] = wx.vapour_pressure_deficit * 10 * wx.wind_speed_10m * 0.44704      # hPa x m/s
d = wx.hdw_proxy.resample("D").max().loc[:"2024-08-04"]
print(f"highest days: {', '.join(f'{t:%d %b} {v:.0f}' for t, v in d.nlargest(3).items())}; window median {d.median():.0f}")
highest days: 24 Jul 361, 23 Jul 321, 19 Jul 313; window median 205

Fire weather indices are compact physics

Fire weather indices are compact physics. The FFWI turns temperature and humidity into the moisture content that fine dead fuels such as grass and needles would reach in equilibrium, converts that into a damping factor between 0 and 1, and multiplies by a wind term. It answers one question: how readily would fine fuels ignite and carry fire at this moment? It knows nothing about the fuels actually present, the terrain or the fire's own size.

How the Fosberg Fire Weather Index is computed from temperature, humidity and wind. inside the Fosberg Fire Weather Indextemperature +humidityhourly, Β°F and %equilibriummoisturefine dead fuel, %damping Ξ·0 wet … 1 bone dryΓ— √(1 + windΒ²)/ 0.3002wind in mphFFWI36.9 peak on 24 JulNo fuel load, no terrain, no memory of yesterday: it rates the air, not the fire spatialworkflow.io
The Fosberg index in four steps.

Reanalysis such as ERA5 gives hourly weather for any grid cell, which makes indices like this easy to compute anywhere and for any past date. Its cells are about 25 km across, though, and it smooths local winds in canyons and on ridges, exactly where fires make their runs. For operations, weather stations such as RAWS and high-resolution forecasts are the standard. For mapping and for comparing days and seasons, reanalysis is consistent and free.

Units, local time and gusts

Fire weather index problems and fixes. fire weather problemsSymptomFixindex far off the usual rangeFFWI needs Β°F, % and mphweather 7 hours off the firecheck local vs UTC timestampsreanalysis calm, fire running25 km cells smooth terrain windsindex high, fire smallit rates conditions, not fuel orsizeERA5 via Open-Meteo at the Park Fire origin spatialworkflow.io
Fire weather problems and fixes.
  • Units: the FFWI equations use Β°F, % and mph. Ask Open-Meteo for those units, or convert before calling the function.
  • Local time: Open-Meteo returns local clock times when timezone is set, so tz_localize with the same zone is correct here. For UTC data use tz_convert.
  • Gusts vs mean wind: the FFWI uses sustained wind; gusts are worth reporting separately.
  • Other indices: the Canadian Fire Weather Index system, the US National Fire Danger Rating System and the Australian Forest Fire Danger Index carry fuel-moisture memory from day to day; the FFWI does not.
  • Grid cell: the nearest ERA5 cell may sit at a different elevation from the fire; Open-Meteo reports the cell's elevation (232 m here).

FAQ

What is the Fosberg Fire Weather Index?

An index from temperature, relative humidity and wind speed that estimates how readily fine fuels would ignite and spread fire, on a scale of about 0 to 100.

Where can I get historical weather for fire analysis?

Open-Meteo's archive API serves ERA5 reanalysis hourly for any location without a key. Weather stations such as RAWS give observations at fire-prone sites.

Why does overnight humidity matter for wildfires?

Fine fuels take up moisture when humidity rises at night, which slows fires. A dry, windy night lets a fire keep spreading.

Does a high fire weather index mean a fire will be large?

No. It describes conditions; a fire needs an ignition and fuel, and its size also depends on terrain and suppression.