Computing Flame Length and Fireline Intensity in Python
Rate of spread tells you how fast a fire moves; fireline intensity tells you what it can do when it arrives, and flame length is the form that fire crews actually plan against. The conversion is two short equations, and almost every mistake in it is a unit error that produces a number three orders of magnitude wrong while still looking plausible. This guide gets it right, inside Fire Behavior and Rate of Spread Modeling, part of Fire Risk & Fuel Assessment.
When to use each quantity
| Quantity | Units | Answers | Audience |
|---|---|---|---|
| Rate of spread | m/min | How fast the head moves | Modelling, evacuation timing |
| Fireline intensity | kW/m | Energy released per metre of fireline per second | Physical comparison, crown-fire initiation |
| Flame length | m | What the flames look like | Suppression tactics, safety |
| Heat per unit area | kJ/m² | Total energy released as the front passes | Soil heating, ecological effect |
Intensity and flame length carry the same information — one is a monotone function of the other — but they serve different audiences. Suppression doctrine is written in flame length; crown-fire initiation thresholds are written in intensity. Compute both and report the one your reader uses.
Byram’s equations and their units
Fireline intensity is the product of the fuel’s heat yield, the mass of fuel consumed per unit area, and the rate of spread:
with the low heat of combustion in kJ/kg (about 18,600 for most woody fuels), the available fuel load in kg/m², and the rate of spread in m/s. The result is kW/m.
Flame length follows from intensity by Byram’s empirical relation:
with in kW/m and in metres.
The unit traps are all in and . Fuel load is nearly always tabulated in tonnes per hectare and must be converted to kg/m² by dividing by 10; rate of spread is nearly always reported in metres per minute and must be divided by 60. Get either wrong and the intensity is out by a factor of 10 or 60 — enough to move a fire from “hand crews can hold it” to “nothing will stop it”, and not obviously wrong on a map.
Minimal reproducible example
import numpy as np
HEAT_YIELD_KJ_KG = 18_600.0 # low heat of combustion, typical woody fuel
def fireline_intensity(fuel_load_t_ha, ros_m_min, heat_kj_kg=HEAT_YIELD_KJ_KG):
"""Byram's fireline intensity in kW/m from field-convention inputs."""
w_kg_m2 = np.asarray(fuel_load_t_ha, dtype="float64") / 10.0 # t/ha -> kg/m²
r_m_s = np.asarray(ros_m_min, dtype="float64") / 60.0 # m/min -> m/s
return heat_kj_kg * w_kg_m2 * r_m_s # kJ/kg·kg/m²·m/s = kW/m
def flame_length(intensity_kw_m):
"""Byram's flame length in metres from fireline intensity in kW/m."""
i = np.asarray(intensity_kw_m, dtype="float64")
return np.where(i > 0, 0.0775 * np.power(np.maximum(i, 1e-9), 0.46), 0.0)
def heat_per_unit_area(fuel_load_t_ha, heat_kj_kg=HEAT_YIELD_KJ_KG):
"""Total energy released per square metre as the front passes, in kJ/m²."""
return heat_kj_kg * (np.asarray(fuel_load_t_ha, dtype="float64") / 10.0)
The comment on the return line of fireline_intensity is not decoration — writing the unit algebra out is the cheapest defence against the error this function exists to avoid.
Only the fuel that actually burns in the flaming front counts as . Large woody debris and duff burn behind the front and contribute to heat per unit area and to soil heating, but not to the intensity that drives flame length. Using total fuel load rather than available fine fuel systematically over-predicts, often by a factor of two or more in a stand with heavy downed wood.
Interpretation thresholds
Flame length maps onto suppression doctrine through a set of widely used breakpoints. They are the reason the conversion is worth doing at all.
| Flame length | Intensity | Interpretation |
|---|---|---|
| < 1.2 m | < 350 kW/m | Hand crews with hand tools can generally hold the line |
| 1.2–2.4 m | 350–1,700 kW/m | Too intense for direct attack at the head; equipment or aircraft needed |
| 2.4–3.4 m | 1,700–3,500 kW/m | Torching, crowning and spotting likely; control efforts at the head are unlikely to succeed |
| > 3.4 m | > 3,500 kW/m | Crown fire; suppression at the head is ineffective |
Parameter reference
| Parameter | Type | Typical | Range | Rationale |
|---|---|---|---|---|
heat_kj_kg |
float | 18,600 | 16,000–22,000 | Low heat of combustion; varies modestly with species and resin content |
| available fuel | t/ha | 2–20 | 0.5–40 | Only fine fuel consumed in the flaming front, not total load |
| rate of spread | m/min | 1–60 | 0.2–150 | Head-fire spread; flanking and backing rates are much lower |
| Byram coefficient | float | 0.0775 | — | Empirical; leave it unless replacing the whole relation |
| Byram exponent | float | 0.46 | — | Empirical; changing it invalidates the threshold table |
| moisture of extinction | % | 25–40 | — | Applied upstream in the spread model, not here |
Expected output and verification
The verification that matters is dimensional. Two anchors and a monotonicity check catch essentially every implementation error:
import numpy as np
# Anchor 1: a light grass fire — 3 t/ha at 10 m/min.
i1 = fireline_intensity(3.0, 10.0)
assert 800 < i1 < 1000, f"grass-fire intensity out of range: {i1:.0f} kW/m"
assert 1.5 < flame_length(i1) < 2.2, "grass-fire flame length out of range"
# Anchor 2: a heavy slash fire — 30 t/ha at 40 m/min.
i2 = fireline_intensity(30.0, 40.0)
assert i2 > 30_000, f"slash-fire intensity implausibly low: {i2:.0f} kW/m"
assert flame_length(i2) > 8.0, "slash-fire flame length implausibly short"
# Monotonicity: both inputs must increase intensity, and intensity must increase length.
loads = np.linspace(1, 30, 50)
assert np.all(np.diff(fireline_intensity(loads, 10.0)) > 0)
assert np.all(np.diff(flame_length(fireline_intensity(loads, 10.0))) > 0)
# The refusal test: a unit slip must be detectable, not silently absorbed.
wrong = HEAT_YIELD_KJ_KG * 3.0 * 10.0 # t/ha and m/min used raw
assert wrong / i1 > 100, "a raw-units calculation should be wildly off, and is"
The last assertion is the one worth keeping. It feeds the machinery the exact mistake the unit conversions exist to prevent and confirms the result is obviously wrong rather than plausibly wrong — which is the difference between an error someone catches and an error that ships.
Applying it across a raster is straightforward once the units are right:
import rasterio
def intensity_raster(fuel_path: str, ros_path: str, out_path: str,
heat_kj_kg: float = HEAT_YIELD_KJ_KG):
"""Write fireline intensity and flame length as a two-band GeoTIFF."""
with rasterio.open(fuel_path) as f, rasterio.open(ros_path) as r:
assert f.transform == r.transform and f.crs == r.crs, "grids must align"
fuel = f.read(1, masked=True).astype("float64")
ros = r.read(1, masked=True).astype("float64")
profile = f.profile
intensity = fireline_intensity(fuel.filled(np.nan), ros.filled(np.nan), heat_kj_kg)
length = flame_length(intensity)
profile.update(count=2, dtype="float32", nodata=np.nan,
compress="deflate", predictor=2, tiled=True)
with rasterio.open(out_path, "w", **profile) as dst:
dst.write(intensity.astype("float32"), 1)
dst.write(length.astype("float32"), 2)
dst.set_band_description(1, "fireline_intensity_kW_per_m")
dst.set_band_description(2, "flame_length_m")
return out_path
Setting band descriptions costs one line and prevents the downstream question that always follows a two-band raster.
Common pitfalls
- Total fuel load instead of available fine fuel. Heavy downed wood burns behind the front and inflates intensity if included.
- Rate of spread in the wrong units. m/min into an equation expecting m/s is a factor of 60, and the resulting map still looks like a fire.
- Applying head-fire spread everywhere. Flanks and the backing edge spread far more slowly, so a single spread raster over-predicts intensity around most of the perimeter.
- Reading flame length as flame height. They differ on slopes and in wind, where the flame leans; the suppression thresholds are written in flame length.
- Assuming intensity keeps rising with flame length. The exponent is 0.46, so a fire at 20,000 kW/m has roughly twice the flame length of one at 3,500 — not six times.
Frequently Asked Questions
Should I use Byram’s flame length or a crown-fire specific relation?
Byram’s relation is for surface fire and is what the suppression thresholds are built on. Once a fire is crowning, flame length is governed by canopy fuel rather than surface fuel, and a surface-fire relation under-predicts. Use Byram for the surface case and switch to a crown-fire model past the initiation threshold rather than extrapolating.
Where does heat per unit area fit?
It is the ecological quantity. Intensity governs suppression and crown-fire initiation; heat per unit area governs soil heating, seed bank survival and duff consumption. A slow fire in heavy fuel can have low intensity and very high heat per unit area — the classic long-residence smouldering burn that kills roots without ever looking dramatic.
Can I invert flame length to get intensity?
Yes, and it is often useful when the only record is an observer’s flame-length estimate: . Treat the result as an order-of-magnitude figure, because the relation is empirical and observed flame lengths carry large uncertainty.
Related
- Fire Behavior and Rate of Spread Modeling — produces the spread rate this consumes
- Modeling Rate of Spread with Rothermel in Python — the spread model in detail
- Fuel Load Mapping from LiDAR — produces the fuel load term
- Deriving Canopy Base Height from LiDAR Vertical Profiles — the other half of crown-fire initiation
Up: Fire Behavior and Rate of Spread Modeling · Fire Risk & Fuel Assessment