Fire#
The Fire tools provide per-cell raster functions for burn severity mapping, fire behavior modeling, and drought indexing.
dNBR: Differenced Normalized Burn Ratio (pre minus post NBR).
RdNBR: Relative dNBR, normalized by pre-fire vegetation density.
Burn Severity Classification: USGS 7-class severity from dNBR.
Fireline Intensity: Byram’s fireline intensity (kW/m).
Flame Length: Flame length from intensity (m).
Rate of Spread: Simplified Rothermel with Anderson 13 fuel models (m/min).
KBDI: Keetch-Byram Drought Index, single time-step update.
Importing packages#
[1]:
import numpy as np
import xarray as xr
from datashader.transfer_functions import shade, stack, Images
from xrspatial.fire import (
dnbr, rdnbr, burn_severity_class,
fireline_intensity, flame_length,
rate_of_spread, kbdi,
)
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[1], line 4
1 import numpy as np
2 import xarray as xr
3
----> 4 from datashader.transfer_functions import shade, stack, Images
5
6 from xrspatial.fire import (
7 dnbr, rdnbr, burn_severity_class,
ModuleNotFoundError: No module named 'datashader'
Generate synthetic data#
We create a 200x200 landscape with a simulated burn scar. Pre-fire NBR is higher where vegetation is denser; after the fire, NBR drops inside an elliptical burn perimeter.
In a real workflow you would compute NBR from satellite imagery using xrspatial.multispectral.nbr.
[2]:
H, W = 200, 200
rng = np.random.default_rng(42)
ys = np.linspace(H - 1, 0, H)
xs = np.linspace(0, W - 1, W)
def make_da(data, name):
return xr.DataArray(data.astype(np.float32), dims=['y', 'x'],
coords={'y': ys, 'x': xs}, name=name)
yy, xx = np.meshgrid(np.linspace(0, 1, H), np.linspace(0, 1, W), indexing='ij')
veg = 0.3 + 0.3 * np.sin(2 * np.pi * yy) * np.cos(np.pi * xx)
pre_nbr = np.clip(veg + rng.normal(0, 0.03, (H, W)), 0.05, 0.85)
dist = np.sqrt(((yy - 0.5) / 0.25) ** 2 + ((xx - 0.5) / 0.35) ** 2)
burn_mask = dist < 1.0
burn_intensity = np.clip(1.0 - dist, 0, 1)
post_nbr = pre_nbr.copy()
post_nbr[burn_mask] -= burn_intensity[burn_mask] * (0.3 + rng.uniform(0, 0.3, burn_mask.sum()))
post_nbr = np.clip(post_nbr, -0.5, 0.85)
pre_nbr_agg = make_da(pre_nbr, 'pre_nbr')
post_nbr_agg = make_da(post_nbr, 'post_nbr')
pre_img = shade(pre_nbr_agg, cmap=['brown', 'yellow', 'green'], how='linear')
pre_img.name = 'Pre-fire NBR'
post_img = shade(post_nbr_agg, cmap=['brown', 'yellow', 'green'], how='linear')
post_img.name = 'Post-fire NBR'
imgs = Images(pre_img, post_img)
imgs.num_cols = 2
imgs
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[2], line 26
22
23 pre_nbr_agg = make_da(pre_nbr, 'pre_nbr')
24 post_nbr_agg = make_da(post_nbr, 'post_nbr')
25
---> 26 pre_img = shade(pre_nbr_agg, cmap=['brown', 'yellow', 'green'], how='linear')
27 pre_img.name = 'Pre-fire NBR'
28 post_img = shade(post_nbr_agg, cmap=['brown', 'yellow', 'green'], how='linear')
29 post_img.name = 'Post-fire NBR'
NameError: name 'shade' is not defined
dNBR#
The differenced Normalized Burn Ratio is pre_NBR - post_NBR. Positive values mean vegetation loss; negative values mean regrowth. USGS and BAER teams use dNBR as input to the severity classification thresholds.
[3]:
dnbr_agg = dnbr(pre_nbr_agg, post_nbr_agg)
print(f"dNBR range: {float(dnbr_agg.min()):.3f} to {float(dnbr_agg.max()):.3f}")
shade(dnbr_agg, cmap=['green', 'lightyellow', 'orange', 'red', 'darkred'], how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 1
----> 1 dnbr_agg = dnbr(pre_nbr_agg, post_nbr_agg)
2
3 print(f"dNBR range: {float(dnbr_agg.min()):.3f} to {float(dnbr_agg.max()):.3f}")
4 shade(dnbr_agg, cmap=['green', 'lightyellow', 'orange', 'red', 'darkred'], how='linear')
NameError: name 'dnbr' is not defined
RdNBR#
Relative dNBR normalizes severity by pre-fire vegetation density: dNBR / sqrt(abs(pre_NBR / 1000)). This lets you compare burn severity across vegetation types. Pixels where pre-fire NBR is near zero are set to NaN.
[4]:
rdnbr_agg = rdnbr(dnbr_agg, pre_nbr_agg)
print(f"RdNBR range: {float(np.nanmin(rdnbr_agg.data)):.3f} to "
f"{float(np.nanmax(rdnbr_agg.data)):.3f}")
shade(rdnbr_agg, cmap=['green', 'lightyellow', 'orange', 'red', 'darkred'], how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[4], line 1
----> 1 rdnbr_agg = rdnbr(dnbr_agg, pre_nbr_agg)
2
3 print(f"RdNBR range: {float(np.nanmin(rdnbr_agg.data)):.3f} to "
4 f"{float(np.nanmax(rdnbr_agg.data)):.3f}")
NameError: name 'rdnbr' is not defined
Burn Severity Classification#
burn_severity_class bins dNBR into the standard USGS 7-class scheme (int8 output, 0 = nodata). This function accepts Datasets via @supports_dataset.
Class |
Label |
dNBR range |
|---|---|---|
1 |
Enhanced regrowth (high) |
< -0.251 |
2 |
Enhanced regrowth (low) |
-0.251 to -0.101 |
3 |
Unburned |
-0.101 to 0.099 |
4 |
Low severity |
0.099 to 0.269 |
5 |
Moderate-low severity |
0.269 to 0.439 |
6 |
Moderate-high severity |
0.439 to 0.659 |
7 |
High severity |
>= 0.659 |
[5]:
severity = burn_severity_class(dnbr_agg)
severity_float = severity.astype(np.float32)
severity_float.values = np.where(severity_float.values == 0, np.nan, severity_float.values)
shade(severity_float,
cmap=['darkgreen', 'green', 'lightgreen', 'yellow', 'orange', 'red', 'darkred'],
how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[5], line 1
----> 1 severity = burn_severity_class(dnbr_agg)
2
3 severity_float = severity.astype(np.float32)
4 severity_float.values = np.where(severity_float.values == 0, np.nan, severity_float.values)
NameError: name 'burn_severity_class' is not defined
Fireline Intensity#
Byram’s fireline intensity: I = H * w * R where H is heat content (kJ/kg), w is fuel consumed (kg/m²), and R is spread rate (m/s). Output is kW/m. Fires below ~350 kW/m can be attacked by hand crews; above ~4,000 kW/m they typically need indirect attack or aerial resources.
[6]:
fuel = make_da((veg * 3.0 + rng.uniform(0, 0.5, (H, W))).astype(np.float32), 'fuel')
spread = make_da((0.02 + 0.03 * rng.uniform(0, 1, (H, W))).astype(np.float32), 'spread')
intensity_agg = fireline_intensity(fuel, spread, heat_content=18000)
print(f"Intensity range: {float(intensity_agg.min()):.1f} to {float(intensity_agg.max()):.1f} kW/m")
shade(intensity_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[6], line 4
1 fuel = make_da((veg * 3.0 + rng.uniform(0, 0.5, (H, W))).astype(np.float32), 'fuel')
2 spread = make_da((0.02 + 0.03 * rng.uniform(0, 1, (H, W))).astype(np.float32), 'spread')
3
----> 4 intensity_agg = fireline_intensity(fuel, spread, heat_content=18000)
5
6 print(f"Intensity range: {float(intensity_agg.min()):.1f} to {float(intensity_agg.max()):.1f} kW/m")
7 shade(intensity_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')
NameError: name 'fireline_intensity' is not defined
Flame Length#
Flame length from fireline intensity: L = 0.0775 * I^0.46. Zero or negative intensity gives zero flame length. Accepts Datasets via @supports_dataset.
[7]:
fl_agg = flame_length(intensity_agg)
print(f"Flame length range: {float(fl_agg.min()):.2f} to {float(fl_agg.max()):.2f} m")
shade(fl_agg, cmap=['lightyellow', 'orange', 'red'], how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 1
----> 1 fl_agg = flame_length(intensity_agg)
2
3 print(f"Flame length range: {float(fl_agg.min()):.2f} to {float(fl_agg.max()):.2f} m")
4 shade(fl_agg, cmap=['lightyellow', 'orange', 'red'], how='linear')
NameError: name 'flame_length' is not defined
Rate of Spread#
rate_of_spread uses a simplified Rothermel (1972) model with the Anderson 13 fuel model table. Inputs are slope (degrees), mid-flame wind speed (km/h), and dead fuel moisture (fraction 0-1). The fuel_model parameter (1-13) selects fuel bed properties.
Below, slope increases from bottom to top and wind increases from left to right, so spread rate is highest in the top-right corner.
[8]:
slope_agg = make_da((5.0 + 20.0 * yy).astype(np.float32), 'slope')
wind_agg = make_da((5.0 + 15.0 * xx).astype(np.float32), 'wind')
moisture_agg = make_da(np.full((H, W), 0.06, dtype=np.float32), 'moisture')
ros_agg = rate_of_spread(slope_agg, wind_agg, moisture_agg, fuel_model=1)
print(f"Rate of spread: {float(ros_agg.min()):.2f} to {float(ros_agg.max()):.2f} m/min")
shade(ros_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[8], line 5
1 slope_agg = make_da((5.0 + 20.0 * yy).astype(np.float32), 'slope')
2 wind_agg = make_da((5.0 + 15.0 * xx).astype(np.float32), 'wind')
3 moisture_agg = make_da(np.full((H, W), 0.06, dtype=np.float32), 'moisture')
4
----> 5 ros_agg = rate_of_spread(slope_agg, wind_agg, moisture_agg, fuel_model=1)
6
7 print(f"Rate of spread: {float(ros_agg.min()):.2f} to {float(ros_agg.max()):.2f} m/min")
8 shade(ros_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')
NameError: name 'rate_of_spread' is not defined
Comparing fuel models with the same inputs:
[9]:
for fm, name in [(1, 'Short grass'), (3, 'Tall grass'), (4, 'Chaparral'), (8, 'Timber litter')]:
r = rate_of_spread(slope_agg, wind_agg, moisture_agg, fuel_model=fm)
print(f" Model {fm:2d} ({name:15s}): {float(r.min()):8.2f} to {float(r.max()):8.2f} m/min")
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[9], line 2
1 for fm, name in [(1, 'Short grass'), (3, 'Tall grass'), (4, 'Chaparral'), (8, 'Timber litter')]:
----> 2 r = rate_of_spread(slope_agg, wind_agg, moisture_agg, fuel_model=fm)
3 print(f" Model {fm:2d} ({name:15s}): {float(r.min()):8.2f} to {float(r.max()):8.2f} m/min")
NameError: name 'rate_of_spread' is not defined
KBDI#
The Keetch-Byram Drought Index tracks cumulative soil moisture deficit (0-800 mm). It gets updated daily from max temperature (Celsius) and precipitation (mm). annual_precip is a scalar for mean annual rainfall.
Below we start from KBDI = 300 (moderate drought), run 30 hot dry days, drop 40 mm of rain, then continue.
[10]:
current = make_da(np.full((H, W), 300.0, dtype=np.float32), 'kbdi')
hot = make_da(np.full((H, W), 35.0, dtype=np.float32), 'temp')
no_rain = make_da(np.zeros((H, W), dtype=np.float32), 'precip')
history = [float(current.mean())]
for _ in range(30):
current = kbdi(current, hot, no_rain, annual_precip=1200.0)
history.append(float(current.mean()))
rain = make_da(np.full((H, W), 40.0, dtype=np.float32), 'precip')
current = kbdi(current, hot, rain, annual_precip=1200.0)
history.append(float(current.mean()))
for _ in range(5):
current = kbdi(current, hot, no_rain, annual_precip=1200.0)
history.append(float(current.mean()))
print(f"Day 0: {history[0]:.1f}")
print(f"Day 30: {history[30]:.1f} (pre-rain)")
print(f"Day 31: {history[31]:.1f} (post-rain)")
print(f"Day 36: {history[-1]:.1f} (5 days after rain)")
shade(current, cmap=['green', 'yellow', 'orange', 'red'], how='linear')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[10], line 7
3 no_rain = make_da(np.zeros((H, W), dtype=np.float32), 'precip')
4
5 history = [float(current.mean())]
6 for _ in range(30):
----> 7 current = kbdi(current, hot, no_rain, annual_precip=1200.0)
8 history.append(float(current.mean()))
9
10 rain = make_da(np.full((H, W), 40.0, dtype=np.float32), 'precip')
NameError: name 'kbdi' is not defined
References#
Key, C.H. and Benson, N.C. (2006). Landscape Assessment. In: FIREMON, USDA Forest Service Gen. Tech. Rep. RMRS-GTR-164-CD.
Rothermel, R.C. (1972). A mathematical model for predicting fire spread in wildland fuels. USDA Forest Service Res. Pap. INT-115.
Anderson, H.E. (1982). Aids to determining fuel models for estimating fire behavior. USDA Forest Service Gen. Tech. Rep. INT-122.
Keetch, J.J. and Byram, G.M. (1968). A drought index for forest fire control. USDA Forest Service Res. Pap. SE-38.
USGS Burn Severity Portal: https://burnseverity.cr.usgs.gov/