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/