Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions docs/source/user_guide/fire.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -178,26 +178,14 @@
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Fireline Intensity\n",
"\n",
"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."
]
"source": "### Fireline Intensity\n\nByram'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.\n\n`spread_rate_units` defaults to `'m/min'` so the output of `rate_of_spread` (below) can be passed straight in. The synthetic `spread` array here is already in m/s, so we pass `spread_rate_units='m/s'`."
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fuel = make_da((veg * 3.0 + rng.uniform(0, 0.5, (H, W))).astype(np.float32), 'fuel')\n",
"spread = make_da((0.02 + 0.03 * rng.uniform(0, 1, (H, W))).astype(np.float32), 'spread')\n",
"\n",
"intensity_agg = fireline_intensity(fuel, spread, heat_content=18000)\n",
"\n",
"print(f\"Intensity range: {float(intensity_agg.min()):.1f} to {float(intensity_agg.max()):.1f} kW/m\")\n",
"shade(intensity_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')"
]
"source": "fuel = make_da((veg * 3.0 + rng.uniform(0, 0.5, (H, W))).astype(np.float32), 'fuel')\nspread = make_da((0.02 + 0.03 * rng.uniform(0, 1, (H, W))).astype(np.float32), 'spread')\n\nintensity_agg = fireline_intensity(fuel, spread, heat_content=18000,\n spread_rate_units='m/s')\n\nprint(f\"Intensity range: {float(intensity_agg.min()):.1f} to {float(intensity_agg.max()):.1f} kW/m\")\nshade(intensity_agg, cmap=['lightyellow', 'orange', 'red', 'darkred'], how='linear')"
},
{
"cell_type": "markdown",
Expand Down Expand Up @@ -334,4 +322,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
}
}
3 changes: 1 addition & 2 deletions examples/fire_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,10 @@ def make_wind(h: int, w: int, rng: np.random.Generator):
fuel_consumed_da = template.copy(
data=(fuel_load_kg_m2 * fuel_density).astype(np.float32),
)
ros_m_s_da = template.copy(data=(ros_vals / 60.0).astype(np.float32))

print("Computing fireline intensity & flame length ...")
intensity_da = fireline_intensity(
fuel_consumed_da, ros_m_s_da, heat_content=heat_content_kj,
fuel_consumed_da, ros_da, heat_content=heat_content_kj,
)
flame_len_da = flame_length(intensity_da)
flame_len_vals = flame_len_da.values
Expand Down
33 changes: 28 additions & 5 deletions xrspatial/fire.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,11 +533,18 @@ def _fli_dask_cupy(fuel_data, spread_data, heat_content):
def fireline_intensity(fuel_consumed_agg: xr.DataArray,
spread_rate_agg: xr.DataArray,
heat_content: float = 18000.0,
spread_rate_units: str = 'm/min',
name: str = 'fireline_intensity') -> xr.DataArray:
"""Byram's fireline intensity.

``I = H * w * R`` where *H* is heat content (kJ/kg), *w* is fuel
consumed (kg/m^2), and *R* is rate of spread (m/s).
consumed (kg/m^2), and *R* is rate of spread in m/s (see
``spread_rate_units`` for the accepted input unit).

The spread rate is accepted in m/min by default so that the output of
:func:`rate_of_spread` can be passed straight in. Byram's equation
needs *R* in m/s, so m/min inputs are divided by 60 internally. Pass
``spread_rate_units='m/s'`` if you already hold spread rates in m/s.

Supports NumPy, CuPy, Dask with NumPy, and Dask with CuPy backed
xarray DataArrays; the output backend matches the input.
Expand All @@ -547,9 +554,13 @@ def fireline_intensity(fuel_consumed_agg: xr.DataArray,
fuel_consumed_agg : xr.DataArray
Fuel consumed per unit area (kg/m^2).
spread_rate_agg : xr.DataArray
Rate of spread (m/s).
Rate of spread, in the unit given by ``spread_rate_units``.
:func:`rate_of_spread` produces m/min.
heat_content : float, default=18000
Heat content of fuel (kJ/kg).
spread_rate_units : str, default='m/min'
Unit of ``spread_rate_agg``: ``'m/min'`` (matches
:func:`rate_of_spread`) or ``'m/s'``.
name : str, default='fireline_intensity'
Name of output DataArray.

Expand All @@ -566,7 +577,7 @@ def fireline_intensity(fuel_consumed_agg: xr.DataArray,
>>> from xrspatial import fireline_intensity
>>> fuel = xr.DataArray(np.array([[2.0, 0.5]], dtype='f4'))
>>> spread = xr.DataArray(np.array([[0.1, 0.2]], dtype='f4'))
>>> fireline_intensity(fuel, spread).values
>>> fireline_intensity(fuel, spread, spread_rate_units='m/s').values
array([[3600., 1800.]], dtype=float32)
"""
_validate_raster(fuel_consumed_agg, func_name='fireline_intensity',
Expand All @@ -576,6 +587,16 @@ def fireline_intensity(fuel_consumed_agg: xr.DataArray,
validate_arrays(fuel_consumed_agg, spread_rate_agg)
_validate_scalar(heat_content, func_name='fireline_intensity',
name='heat_content', dtype=(int, float), min_val=0)
if spread_rate_units not in ('m/min', 'm/s'):
raise ValueError(
"fireline_intensity: spread_rate_units must be 'm/min' or "
f"'m/s', got {spread_rate_units!r}"
)

# Byram's equation needs R in m/s; convert from m/min when needed.
spread_data = spread_rate_agg.data.astype('f4')
if spread_rate_units == 'm/min':
spread_data = spread_data / 60.0

mapper = ArrayTypeFunctionMapping(
numpy_func=_fli_cpu,
Expand All @@ -585,7 +606,7 @@ def fireline_intensity(fuel_consumed_agg: xr.DataArray,
)
out = mapper(fuel_consumed_agg)(
fuel_consumed_agg.data.astype('f4'),
spread_rate_agg.data.astype('f4'),
spread_data,
float(heat_content),
)
return xr.DataArray(out, name=name,
Expand Down Expand Up @@ -872,7 +893,9 @@ def rate_of_spread(slope_agg: xr.DataArray,
Returns
-------
xr.DataArray
Rate of spread in m/min (float32).
Rate of spread in m/min (float32). This is the default input unit
of :func:`fireline_intensity`, so the result can be passed there
directly.

Examples
--------
Expand Down
47 changes: 46 additions & 1 deletion xrspatial/tests/test_fire.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ def test_known_values(self):
spread = np.array([[0.1, 0.2], [np.nan, 0.3]], dtype=np.float32)
result = fireline_intensity(create_test_raster(fuel),
create_test_raster(spread),
heat_content=18000)
heat_content=18000,
spread_rate_units='m/s')
expected = np.array([
[18000 * 2.0 * 0.1, 18000 * 0.5 * 0.2],
[np.nan, np.nan],
Expand All @@ -325,11 +326,55 @@ def test_known_values(self):
def test_custom_heat_content(self):
fuel = np.array([[1.0]], dtype=np.float32)
spread = np.array([[1.0]], dtype=np.float32)
result = fireline_intensity(create_test_raster(fuel),
create_test_raster(spread),
heat_content=20000,
spread_rate_units='m/s')
np.testing.assert_allclose(result.data[0, 0], 20000.0, rtol=1e-5)

def test_default_units_are_m_per_min(self):
# Default spread_rate_units='m/min' divides by 60 before Byram's
# equation, so chaining rate_of_spread (m/min) is correct.
fuel = np.array([[1.0]], dtype=np.float32)
spread = np.array([[60.0]], dtype=np.float32) # m/min == 1 m/s
result = fireline_intensity(create_test_raster(fuel),
create_test_raster(spread),
heat_content=20000)
np.testing.assert_allclose(result.data[0, 0], 20000.0, rtol=1e-5)

def test_m_per_min_is_sixty_times_m_per_s(self):
fuel = np.array([[2.0, 0.5]], dtype=np.float32)
spread = np.array([[6.0, 12.0]], dtype=np.float32)
f_agg = create_test_raster(fuel)
s_agg = create_test_raster(spread)
as_min = fireline_intensity(f_agg, s_agg, spread_rate_units='m/min')
as_sec = fireline_intensity(f_agg, s_agg, spread_rate_units='m/s')
np.testing.assert_allclose(as_sec.data, as_min.data * 60.0,
rtol=1e-5)

def test_invalid_units_raises(self):
fuel = create_test_raster(np.array([[1.0]], dtype=np.float32))
spread = create_test_raster(np.array([[1.0]], dtype=np.float32))
with pytest.raises(ValueError):
fireline_intensity(fuel, spread, spread_rate_units='ft/s')

def test_chaining_rate_of_spread(self):
# The footgun: feeding rate_of_spread (m/min) straight into
# fireline_intensity must match an explicit m/min conversion.
slope = create_test_raster(np.full((3, 3), 10.0, dtype=np.float32))
wind = create_test_raster(np.full((3, 3), 8.0, dtype=np.float32))
moist = create_test_raster(np.full((3, 3), 0.08, dtype=np.float32))
ros = rate_of_spread(slope, wind, moist, fuel_model=2)
fuel = create_test_raster(np.full((3, 3), 1.5, dtype=np.float32))
chained = fireline_intensity(fuel, ros)
expected = fireline_intensity(fuel, ros, spread_rate_units='m/min')
np.testing.assert_allclose(chained.data, expected.data, rtol=1e-5,
equal_nan=True)
# And 60x the m/s interpretation of the same numbers.
as_sec = fireline_intensity(fuel, ros, spread_rate_units='m/s')
np.testing.assert_allclose(as_sec.data, chained.data * 60.0,
rtol=1e-5, equal_nan=True)

def test_nan_propagation(self):
fuel = np.array([[np.nan, 1.0]], dtype=np.float32)
spread = np.array([[1.0, np.nan]], dtype=np.float32)
Expand Down
Loading