Spread Model

The SpreadModel module provides composable, differentiable components for driving level set fire simulations. Instead of manually constructing a spread rate matrix F, you build a FireSpreadModel from pluggable wind, moisture, and terrain components — each a callable (t, x, y) struct.

Architecture

FireSpreadModel(fuel, wind, moisture, terrain)
       │
       ▼
model(t, x, y) → rate_of_spread(fuel; moisture, wind, slope)
       │
       ▼
simulate!(grid, model) → level set evolution

Each component is a callable struct with signature (t, x, y):

Component Returns Example
wind::AbstractWind (speed, direction) — speed [km/h], direction [radians] UniformWind(speed=8.0)
moisture::AbstractMoisture FuelClasses — moisture fractions UniformMoisture(...)
terrain::AbstractTerrain (slope, aspect) — slope [fraction], aspect [radians] FlatTerrain()

Quick Example

M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)

model = FireSpreadModel(
    SHORT_GRASS,
    UniformWind(speed=8.0),
    UniformMoisture(M),
    FlatTerrain()
)

# Evaluate spread rate at a single point
model(0.0, 100.0, 100.0)
31.119112730486354
# Full simulation
grid = LevelSetGrid(200, 200, dx=30.0)
ignite!(grid, 3000.0, 3000.0, 50.0)
simulate!(grid, model, steps=100, dt=0.5)

fig = Figure()
ax = Axis(fig[1, 1], title="t = $(grid.t) min", aspect=DataAspect(),
    xlabel="x (m)", ylabel="y (m)")
fireplot!(ax, grid)
fig

Wind

UniformWind

Spatially and temporally constant wind field.

wind = UniformWind(speed=10.0, direction=π/4)
wind(0.0, 0.0, 0.0)  # (speed, direction)
(10.0, 0.7853981633974483)

Moisture

UniformMoisture

Spatially and temporally constant fuel moisture.

moist = UniformMoisture(FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0))
moist(0.0, 0.0, 0.0)
FuelClasses{Float64}(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)

DynamicMoisture

Spatially varying fuel moisture that responds to fire-induced drying. As the fire front approaches, radiative heat dries unburned fuel ahead of the front. The 1-hr dead fuel moisture (d1) varies spatially while other size classes remain constant.

The drying model at each unburned cell:

\[\frac{dM}{dt} = -\frac{\text{dry\_rate}}{\phi^2 + 1} + \text{recovery\_rate} \cdot (M_{\text{ambient}} - M)\]

where \(\phi\) is the level set value (approximate distance to the fire front in meters).

grid_d = LevelSetGrid(100, 100, dx=30.0)
ignite!(grid_d, 1500.0, 1500.0, 100.0)

M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)
dm = DynamicMoisture(grid_d, M, dry_rate=0.1, recovery_rate=0.001)
DynamicMoisture{Float64, Matrix{Float64}}([0.06 0.06 … 0.06 0.06; 0.06 0.06 … 0.06 0.06; … ; 0.06 0.06 … 0.06 0.06; 0.06 0.06 … 0.06 0.06], FuelClasses{Float64}(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0), 0.06, 0.1, 0.001, 0.03, 30.0, 30.0, 0.0, 0.0)

Comparing static vs. dynamic moisture:

M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)

# Static
grid_s = LevelSetGrid(150, 150, dx=20.0)
ignite!(grid_s, 1500.0, 1500.0, 50.0)
model_s = FireSpreadModel(SHORT_GRASS, UniformWind(speed=8.0), UniformMoisture(M), FlatTerrain())
simulate!(grid_s, model_s, steps=150, dt=0.5)

# Dynamic
grid_d = LevelSetGrid(150, 150, dx=20.0)
ignite!(grid_d, 1500.0, 1500.0, 50.0)
model_d = FireSpreadModel(SHORT_GRASS, UniformWind(speed=8.0), DynamicMoisture(grid_d, M), FlatTerrain())
simulate!(grid_d, model_d, steps=150, dt=0.5)

fig = Figure(size=(700, 300))
ax1 = Axis(fig[1, 1], title="Static Moisture\n$(count(<(0), grid_s.φ)) cells burned",
    aspect=DataAspect())
fireplot!(ax1, grid_s)
hidedecorations!(ax1)

ax2 = Axis(fig[1, 2], title="Dynamic Moisture\n$(count(<(0), grid_d.φ)) cells burned",
    aspect=DataAspect())
fireplot!(ax2, grid_d)
hidedecorations!(ax2)
fig

Terrain

FlatTerrain

Zero slope everywhere.

FlatTerrain()(0.0, 0.0, 0.0)  # (slope, aspect)
(0.0, 0.0)

UniformSlope

Spatially constant terrain slope.

slope = UniformSlope(slope=0.3, aspect=0.0)
slope(0.0, 0.0, 0.0)
(0.3, 0.0)

Effect of slope on fire spread:

M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)

fig = Figure(size=(800, 250))
for (col, s) in enumerate([0.0, 0.3, 0.6])
    g = LevelSetGrid(150, 150, dx=20.0)
    ignite!(g, 1500.0, 1500.0, 50.0)
    m = FireSpreadModel(SHORT_GRASS, UniformWind(speed=5.0), UniformMoisture(M), UniformSlope(slope=s))
    simulate!(g, m, steps=150, dt=0.5)
    ax = Axis(fig[1, col], title="slope = $s", aspect=DataAspect())
    fireplot!(ax, g)
    hidedecorations!(ax)
end
fig

Custom Components

To create a custom component, define a callable struct that subtypes AbstractWind, AbstractMoisture, or AbstractTerrain.

For example, a wind field that varies in space:

struct GradientWind <: AbstractWind
    base_speed::Float64
    gradient::Float64   # speed increase per meter in x
end

function (w::GradientWind)(t, x, y)
    speed = w.base_speed + w.gradient * x
    direction = 0.0  # wind from the west
    (speed, direction)
end

M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)
g = LevelSetGrid(150, 150, dx=20.0)
ignite!(g, 1500.0, 1500.0, 50.0)
m = FireSpreadModel(SHORT_GRASS, GradientWind(5.0, 0.003), UniformMoisture(M), FlatTerrain())
simulate!(g, m, steps=150, dt=0.5)

fig = Figure()
ax = Axis(fig[1, 1], title="Gradient Wind", aspect=DataAspect(), xlabel="x (m)", ylabel="y (m)")
fireplot!(ax, g)
fig

For dynamic components that respond to fire state, also implement update!:

SpreadModel.update!(w::MyDynamicWind, grid::LevelSetGrid, dt) = ...

Performance: Float64 vs Float32

The level set grid and all simulation operations support Float32 for reduced memory and potentially faster computation. To create a Float32 simulation, pass Float32 values to all constructors:

Note

The LevelSetGrid constructor determines its element type via promote_type on all keyword arguments (dx, dy, x0, y0). The defaults are Float64, so you must pass Float32 values for all of them — e.g. LevelSetGrid(200, 200, dx=30f0, x0=0f0, y0=0f0).

using BenchmarkTools

function run_simulation(::Type{T}, n=200) where T
    grid = LevelSetGrid(n, n,
        dx=T(30), x0=zero(T), y0=zero(T))
    ignite!(grid, T(3000), T(3000), T(50))
    F = fill(T(10), size(grid))
    for _ in 1:100
        advance!(grid, F, T(0.5))
    end
    reinitialize!(grid)
    grid
end

b64 = @benchmark run_simulation(Float64)
b32 = @benchmark run_simulation(Float32)
nothing
| Metric | Float64 | Float32 | Ratio |
|--------|---------|---------|-------|
| Median time | 32.58 ms | 23.39 ms | 1.39x |
| Memory | 32.7 MiB | 16.3 MiB | 2.00x |

References

  • Rothermel, R.C. (1972). A Mathematical Model for Predicting Fire Spread in Wildland Fuels. Res. Paper INT-115, USDA Forest Service.
  • Osher, S. & Sethian, J.A. (1988). Fronts propagating with curvature-dependent speed. J. Computational Physics, 79(1), 12-49.