Skip to content

Commit 52c6136

Browse files
committed
Add fire module to Sphinx docs
- API reference page (docs/source/reference/fire.rst) with autosummary directives for all 7 public functions - User guide notebook (docs/source/user_guide/fire.ipynb) with synthetic data examples for burn severity, fire behavior, and KBDI - Updated reference and user guide toctrees
1 parent f812eb0 commit 52c6136

File tree

4 files changed

+369
-0
lines changed

4 files changed

+369
-0
lines changed

docs/source/reference/fire.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.. _reference.fire:
2+
3+
****
4+
Fire
5+
****
6+
7+
Burn Severity
8+
=============
9+
.. autosummary::
10+
:toctree: _autosummary
11+
12+
xrspatial.fire.dnbr
13+
xrspatial.fire.rdnbr
14+
xrspatial.fire.burn_severity_class
15+
16+
Fire Behavior
17+
=============
18+
.. autosummary::
19+
:toctree: _autosummary
20+
21+
xrspatial.fire.fireline_intensity
22+
xrspatial.fire.flame_length
23+
xrspatial.fire.rate_of_spread
24+
25+
Fire Danger
26+
===========
27+
.. autosummary::
28+
:toctree: _autosummary
29+
30+
xrspatial.fire.kbdi

docs/source/reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Reference
88
:maxdepth: 2
99

1010
classification
11+
fire
1112
focal
1213
multispectral
1314
pathfinding

docs/source/user_guide/fire.ipynb

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"## Fire"
8+
]
9+
},
10+
{
11+
"cell_type": "markdown",
12+
"metadata": {},
13+
"source": [
14+
"The Fire tools provide per-cell raster functions for burn severity mapping, fire behavior modeling, and drought indexing.\n",
15+
"\n",
16+
"- [dNBR](#dNBR): Differenced Normalized Burn Ratio (pre minus post NBR).\n",
17+
"- [RdNBR](#RdNBR): Relative dNBR, normalized by pre-fire vegetation density.\n",
18+
"- [Burn Severity Classification](#Burn-Severity-Classification): USGS 7-class severity from dNBR.\n",
19+
"- [Fireline Intensity](#Fireline-Intensity): Byram's fireline intensity (kW/m).\n",
20+
"- [Flame Length](#Flame-Length): Flame length from intensity (m).\n",
21+
"- [Rate of Spread](#Rate-of-Spread): Simplified Rothermel with Anderson 13 fuel models (m/min).\n",
22+
"- [KBDI](#KBDI): Keetch-Byram Drought Index, single time-step update."
23+
]
24+
},
25+
{
26+
"cell_type": "markdown",
27+
"metadata": {},
28+
"source": [
29+
"### Importing packages"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"import numpy as np\n",
39+
"import xarray as xr\n",
40+
"\n",
41+
"from datashader.transfer_functions import shade, stack, Images\n",
42+
"\n",
43+
"from xrspatial.fire import (\n",
44+
" dnbr, rdnbr, burn_severity_class,\n",
45+
" fireline_intensity, flame_length,\n",
46+
" rate_of_spread, kbdi,\n",
47+
")"
48+
]
49+
},
50+
{
51+
"cell_type": "markdown",
52+
"metadata": {},
53+
"source": [
54+
"### Generate synthetic data\n",
55+
"\n",
56+
"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.\n",
57+
"\n",
58+
"In a real workflow you would compute NBR from satellite imagery using `xrspatial.multispectral.nbr`."
59+
]
60+
},
61+
{
62+
"cell_type": "code",
63+
"execution_count": null,
64+
"metadata": {},
65+
"outputs": [],
66+
"source": [
67+
"H, W = 200, 200\n",
68+
"rng = np.random.default_rng(42)\n",
69+
"\n",
70+
"ys = np.linspace(H - 1, 0, H)\n",
71+
"xs = np.linspace(0, W - 1, W)\n",
72+
"\n",
73+
"def make_da(data, name):\n",
74+
" return xr.DataArray(data.astype(np.float32), dims=['y', 'x'],\n",
75+
" coords={'y': ys, 'x': xs}, name=name)\n",
76+
"\n",
77+
"yy, xx = np.meshgrid(np.linspace(0, 1, H), np.linspace(0, 1, W), indexing='ij')\n",
78+
"veg = 0.3 + 0.3 * np.sin(2 * np.pi * yy) * np.cos(np.pi * xx)\n",
79+
"pre_nbr = np.clip(veg + rng.normal(0, 0.03, (H, W)), 0.05, 0.85)\n",
80+
"\n",
81+
"dist = np.sqrt(((yy - 0.5) / 0.25) ** 2 + ((xx - 0.5) / 0.35) ** 2)\n",
82+
"burn_mask = dist < 1.0\n",
83+
"burn_intensity = np.clip(1.0 - dist, 0, 1)\n",
84+
"\n",
85+
"post_nbr = pre_nbr.copy()\n",
86+
"post_nbr[burn_mask] -= burn_intensity[burn_mask] * (0.3 + rng.uniform(0, 0.3, burn_mask.sum()))\n",
87+
"post_nbr = np.clip(post_nbr, -0.5, 0.85)\n",
88+
"\n",
89+
"pre_nbr_agg = make_da(pre_nbr, 'pre_nbr')\n",
90+
"post_nbr_agg = make_da(post_nbr, 'post_nbr')\n",
91+
"\n",
92+
"pre_img = shade(pre_nbr_agg, cmap=['brown', 'yellow', 'green'], how='linear')\n",
93+
"pre_img.name = 'Pre-fire NBR'\n",
94+
"post_img = shade(post_nbr_agg, cmap=['brown', 'yellow', 'green'], how='linear')\n",
95+
"post_img.name = 'Post-fire NBR'\n",
96+
"imgs = Images(pre_img, post_img)\n",
97+
"imgs.num_cols = 2\n",
98+
"imgs"
99+
]
100+
},
101+
{
102+
"cell_type": "markdown",
103+
"metadata": {},
104+
"source": [
105+
"### dNBR\n",
106+
"\n",
107+
"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."
108+
]
109+
},
110+
{
111+
"cell_type": "code",
112+
"execution_count": null,
113+
"metadata": {},
114+
"outputs": [],
115+
"source": [
116+
"dnbr_agg = dnbr(pre_nbr_agg, post_nbr_agg)\n",
117+
"\n",
118+
"print(f\"dNBR range: {float(dnbr_agg.min()):.3f} to {float(dnbr_agg.max()):.3f}\")\n",
119+
"shade(dnbr_agg, cmap=['green', 'lightyellow', 'orange', 'red', 'darkred'], how='linear')"
120+
]
121+
},
122+
{
123+
"cell_type": "markdown",
124+
"metadata": {},
125+
"source": [
126+
"### RdNBR\n",
127+
"\n",
128+
"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."
129+
]
130+
},
131+
{
132+
"cell_type": "code",
133+
"execution_count": null,
134+
"metadata": {},
135+
"outputs": [],
136+
"source": [
137+
"rdnbr_agg = rdnbr(dnbr_agg, pre_nbr_agg)\n",
138+
"\n",
139+
"print(f\"RdNBR range: {float(np.nanmin(rdnbr_agg.data)):.3f} to \"\n",
140+
" f\"{float(np.nanmax(rdnbr_agg.data)):.3f}\")\n",
141+
"shade(rdnbr_agg, cmap=['green', 'lightyellow', 'orange', 'red', 'darkred'], how='linear')"
142+
]
143+
},
144+
{
145+
"cell_type": "markdown",
146+
"metadata": {},
147+
"source": [
148+
"### Burn Severity Classification\n",
149+
"\n",
150+
"`burn_severity_class` bins dNBR into the standard USGS 7-class scheme (int8 output, 0 = nodata). This function accepts Datasets via `@supports_dataset`.\n",
151+
"\n",
152+
"| Class | Label | dNBR range |\n",
153+
"|-------|-------|------------|\n",
154+
"| 1 | Enhanced regrowth (high) | < -0.251 |\n",
155+
"| 2 | Enhanced regrowth (low) | -0.251 to -0.101 |\n",
156+
"| 3 | Unburned | -0.101 to 0.099 |\n",
157+
"| 4 | Low severity | 0.099 to 0.269 |\n",
158+
"| 5 | Moderate-low severity | 0.269 to 0.439 |\n",
159+
"| 6 | Moderate-high severity | 0.439 to 0.659 |\n",
160+
"| 7 | High severity | >= 0.659 |"
161+
]
162+
},
163+
{
164+
"cell_type": "code",
165+
"execution_count": null,
166+
"metadata": {},
167+
"outputs": [],
168+
"source": [
169+
"severity = burn_severity_class(dnbr_agg)\n",
170+
"\n",
171+
"severity_float = severity.astype(np.float32)\n",
172+
"severity_float.values = np.where(severity_float.values == 0, np.nan, severity_float.values)\n",
173+
"shade(severity_float,\n",
174+
" cmap=['darkgreen', 'green', 'lightgreen', 'yellow', 'orange', 'red', 'darkred'],\n",
175+
" how='linear')"
176+
]
177+
},
178+
{
179+
"cell_type": "markdown",
180+
"metadata": {},
181+
"source": [
182+
"### Fireline Intensity\n",
183+
"\n",
184+
"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."
185+
]
186+
},
187+
{
188+
"cell_type": "code",
189+
"execution_count": null,
190+
"metadata": {},
191+
"outputs": [],
192+
"source": [
193+
"fuel = make_da((veg * 3.0 + rng.uniform(0, 0.5, (H, W))).astype(np.float32), 'fuel')\n",
194+
"spread = make_da((0.02 + 0.03 * rng.uniform(0, 1, (H, W))).astype(np.float32), 'spread')\n",
195+
"\n",
196+
"intensity_agg = fireline_intensity(fuel, spread, heat_content=18000)\n",
197+
"\n",
198+
"print(f\"Intensity range: {float(intensity_agg.min()):.1f} to {float(intensity_agg.max()):.1f} kW/m\")\n",
199+
"shade(intensity_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')"
200+
]
201+
},
202+
{
203+
"cell_type": "markdown",
204+
"metadata": {},
205+
"source": [
206+
"### Flame Length\n",
207+
"\n",
208+
"Flame length from fireline intensity: `L = 0.0775 * I^0.46`. Zero or negative intensity gives zero flame length. Accepts Datasets via `@supports_dataset`."
209+
]
210+
},
211+
{
212+
"cell_type": "code",
213+
"execution_count": null,
214+
"metadata": {},
215+
"outputs": [],
216+
"source": [
217+
"fl_agg = flame_length(intensity_agg)\n",
218+
"\n",
219+
"print(f\"Flame length range: {float(fl_agg.min()):.2f} to {float(fl_agg.max()):.2f} m\")\n",
220+
"shade(fl_agg, cmap=['lightyellow', 'orange', 'red'], how='linear')"
221+
]
222+
},
223+
{
224+
"cell_type": "markdown",
225+
"metadata": {},
226+
"source": [
227+
"### Rate of Spread\n",
228+
"\n",
229+
"`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.\n",
230+
"\n",
231+
"Below, slope increases from bottom to top and wind increases from left to right, so spread rate is highest in the top-right corner."
232+
]
233+
},
234+
{
235+
"cell_type": "code",
236+
"execution_count": null,
237+
"metadata": {},
238+
"outputs": [],
239+
"source": [
240+
"slope_agg = make_da((5.0 + 20.0 * yy).astype(np.float32), 'slope')\n",
241+
"wind_agg = make_da((5.0 + 15.0 * xx).astype(np.float32), 'wind')\n",
242+
"moisture_agg = make_da(np.full((H, W), 0.06, dtype=np.float32), 'moisture')\n",
243+
"\n",
244+
"ros_agg = rate_of_spread(slope_agg, wind_agg, moisture_agg, fuel_model=1)\n",
245+
"\n",
246+
"print(f\"Rate of spread: {float(ros_agg.min()):.2f} to {float(ros_agg.max()):.2f} m/min\")\n",
247+
"shade(ros_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')"
248+
]
249+
},
250+
{
251+
"cell_type": "markdown",
252+
"metadata": {},
253+
"source": [
254+
"Comparing fuel models with the same inputs:"
255+
]
256+
},
257+
{
258+
"cell_type": "code",
259+
"execution_count": null,
260+
"metadata": {},
261+
"outputs": [],
262+
"source": [
263+
"for fm, name in [(1, 'Short grass'), (3, 'Tall grass'), (4, 'Chaparral'), (8, 'Timber litter')]:\n",
264+
" r = rate_of_spread(slope_agg, wind_agg, moisture_agg, fuel_model=fm)\n",
265+
" print(f\" Model {fm:2d} ({name:15s}): {float(r.min()):8.2f} to {float(r.max()):8.2f} m/min\")"
266+
]
267+
},
268+
{
269+
"cell_type": "markdown",
270+
"metadata": {},
271+
"source": [
272+
"### KBDI\n",
273+
"\n",
274+
"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.\n",
275+
"\n",
276+
"Below we start from KBDI = 300 (moderate drought), run 30 hot dry days, drop 40 mm of rain, then continue."
277+
]
278+
},
279+
{
280+
"cell_type": "code",
281+
"execution_count": null,
282+
"metadata": {},
283+
"outputs": [],
284+
"source": [
285+
"current = make_da(np.full((H, W), 300.0, dtype=np.float32), 'kbdi')\n",
286+
"hot = make_da(np.full((H, W), 35.0, dtype=np.float32), 'temp')\n",
287+
"no_rain = make_da(np.zeros((H, W), dtype=np.float32), 'precip')\n",
288+
"\n",
289+
"history = [float(current.mean())]\n",
290+
"for _ in range(30):\n",
291+
" current = kbdi(current, hot, no_rain, annual_precip=1200.0)\n",
292+
" history.append(float(current.mean()))\n",
293+
"\n",
294+
"rain = make_da(np.full((H, W), 40.0, dtype=np.float32), 'precip')\n",
295+
"current = kbdi(current, hot, rain, annual_precip=1200.0)\n",
296+
"history.append(float(current.mean()))\n",
297+
"\n",
298+
"for _ in range(5):\n",
299+
" current = kbdi(current, hot, no_rain, annual_precip=1200.0)\n",
300+
" history.append(float(current.mean()))\n",
301+
"\n",
302+
"print(f\"Day 0: {history[0]:.1f}\")\n",
303+
"print(f\"Day 30: {history[30]:.1f} (pre-rain)\")\n",
304+
"print(f\"Day 31: {history[31]:.1f} (post-rain)\")\n",
305+
"print(f\"Day 36: {history[-1]:.1f} (5 days after rain)\")\n",
306+
"\n",
307+
"shade(current, cmap=['green', 'yellow', 'orange', 'red'], how='linear')"
308+
]
309+
},
310+
{
311+
"cell_type": "markdown",
312+
"metadata": {},
313+
"source": [
314+
"### References\n",
315+
"\n",
316+
"- Key, C.H. and Benson, N.C. (2006). Landscape Assessment. In: *FIREMON*, USDA Forest Service Gen. Tech. Rep. RMRS-GTR-164-CD.\n",
317+
"- Rothermel, R.C. (1972). A mathematical model for predicting fire spread in wildland fuels. USDA Forest Service Res. Pap. INT-115.\n",
318+
"- Anderson, H.E. (1982). Aids to determining fuel models for estimating fire behavior. USDA Forest Service Gen. Tech. Rep. INT-122.\n",
319+
"- Keetch, J.J. and Byram, G.M. (1968). A drought index for forest fire control. USDA Forest Service Res. Pap. SE-38.\n",
320+
"- USGS Burn Severity Portal: https://burnseverity.cr.usgs.gov/"
321+
]
322+
}
323+
],
324+
"metadata": {
325+
"kernelspec": {
326+
"display_name": "Python 3",
327+
"language": "python",
328+
"name": "python3"
329+
},
330+
"language_info": {
331+
"name": "python",
332+
"version": "3.10.0"
333+
}
334+
},
335+
"nbformat": 4,
336+
"nbformat_minor": 4
337+
}

docs/source/user_guide/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ User Guide
99

1010
data_types
1111
classification
12+
fire
1213
focal
1314
multispectral
1415
pathfinding

0 commit comments

Comments
 (0)