Components

The Components module defines the abstract interfaces and concrete types used by SpreadModel to represent wind, moisture, terrain, burnout, burn-in, and directional spread. Each component is a callable struct with signature (t, x, y) that returns the relevant physical quantity at a given time and location.

Abstract Interfaces

Every component category has an abstract supertype. Custom components subtype these and implement the callable interface.

Abstract Type Signature Returns
AbstractWind wind(t, x, y) (speed, direction) — speed [km/h], direction FROM [rad]
AbstractMoisture moisture(t, x, y) FuelClasses — moisture fractions per size class
AbstractTerrain terrain(t, x, y) (slope, aspect) — slope [fraction], aspect [rad]
AbstractBurnout burnout(t_burning) Float64 ∈ [0, 1] — spread intensity scale factor
AbstractBurnin burnin(t_burning) Float64 ∈ [0, 1] — spread intensity ramp-up

Future interfaces include AbstractSpotting (ember transport) and AbstractSuppression (fireline construction).

Wind

UniformWind{T}(; speed, direction=0.0)

Spatially and temporally constant wind field.

Fields

  • speed::T - Midflame wind speed [km/h]
  • direction::T - Direction wind blows FROM [radians]
wind = UniformWind(speed=8.0, direction=π/4)
wind(0.0, 0.0, 0.0)  # (speed, direction)
(8.0, 0.7853981633974483)

To create a spatially varying wind field, subtype AbstractWind:

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

function (w::GradientWind)(t, x, y)
    speed = w.base_speed + w.gradient * x
    (speed, w.direction)
end

Moisture

UniformMoisture{T}(moisture::FuelClasses{T})

Spatially and temporally constant fuel moisture.

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

Dynamic Moisture

DynamicMoisture(grid::LevelSetGrid, moisture::FuelClasses; dry_rate=0.1, recovery_rate=0.001, min_d1=0.03)

Spatially varying fuel moisture that responds to fire-induced drying.

The 1-hr dead fuel moisture (d1) varies spatially while other size classes remain constant. As the fire front approaches, radiative heat flux dries unburned fuel ahead of the front. Moisture also recovers toward the ambient value over time.

The drying model at each unburned cell:

dM/dt = -dry_rate / (φ² + 1) + recovery_rate · (M_ambient - M)

where φ is the level set value (approximate distance to the fire front in meters). Moisture is clamped to [min_d1, ambient_d1] to prevent unrealistic drying.

Fields

  • d1::M - Spatially varying 1-hr dead fuel moisture (M <: AbstractMatrix{T}) [fraction]
  • base::FuelClasses{T} - Moisture values for other size classes
  • ambient_d1::T - Equilibrium d1 moisture from weather [fraction]
  • dry_rate::T - Fire-induced drying coefficient [fraction/min]
  • recovery_rate::T - Moisture recovery rate toward ambient [1/min]
  • min_d1::T - Minimum d1 moisture floor [fraction]
  • dx::T, dy::T, x0::T, y0::T - Grid geometry for coordinate lookup
grid = LevelSetGrid(100, 100, dx=30.0)
ignite!(grid, 1500.0, 1500.0, 200.0)
M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)
dm = DynamicMoisture(grid, M, dry_rate=0.1, recovery_rate=0.001)
dm(0.0, 1500.0, 1500.0)  # moisture at ignition point
FuelClasses{Float64}(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)

Terrain

FlatTerrain()

Flat terrain (zero slope everywhere).

FlatTerrain()(0.0, 0.0, 0.0)  # (slope, aspect)
(0.0, 0.0)
UniformSlope{T}(; slope, aspect=0.0)

Spatially constant terrain slope.

Fields

  • slope::T - Terrain slope as rise/run [fraction]
  • aspect::T - Downslope direction [radians]
slope = UniformSlope(slope=0.3, aspect=0.0)
slope(0.0, 0.0, 0.0)
(0.3, 0.0)

Burnout

Burnout models scale the spread rate of already-burning cells to simulate fuel exhaustion.

Type Behavior
NoBurnout() No decay — fire spreads at full intensity indefinitely
ExponentialBurnout(τ) Intensity decays as exp(-t/τ)
LinearBurnout(τ) Intensity decreases linearly to zero over τ minutes
Code
t = 0:0.001:0.05
bo_exp = ExponentialBurnout(0.01)
bo_lin = LinearBurnout(0.02)

fig = Figure(size=(600, 300))
ax = Axis(fig[1, 1], xlabel="Time since ignition (min)", ylabel="Spread intensity",
    title="Burnout Models")
lines!(ax, collect(t), bo_exp.(t), label="Exponential (τ=0.01)")
lines!(ax, collect(t), bo_lin.(t), label="Linear (τ=0.02)")
hlines!(ax, [1.0], color=:gray, linestyle=:dash, label="No burnout")
axislegend(ax, position=:rt)
fig

Burn-in

Burn-in models ramp up spread intensity after ignition, preventing freshly ignited cells from immediately propagating fire in all directions.

Type Behavior
NoBurnin() Full intensity immediately upon ignition
ExponentialBurnin(τ) Ramps as 1 - exp(-t/τ)
LinearBurnin(τ) Linear ramp from 0 to 1 over τ minutes
Code
t = 0:0.01:5.0
bi_exp = ExponentialBurnin(1.0)
bi_lin = LinearBurnin(2.0)

fig = Figure(size=(600, 300))
ax = Axis(fig[1, 1], xlabel="Time since ignition (min)", ylabel="Spread intensity",
    title="Burn-in Models")
lines!(ax, collect(t), bi_exp.(t), label="Exponential (τ=1.0)")
lines!(ax, collect(t), bi_lin.(t), label="Linear (τ=2.0)")
hlines!(ax, [1.0], color=:gray, linestyle=:dash, label="No burn-in")
axislegend(ax, position=:rb)
fig

Directional Spread

Directional models control how the head-fire rate of spread varies with angle from the push direction (combined wind + slope vector).

CosineBlending()

Cosine-based directional spread model (default).

The spread rate at angle θ from the push direction is:

R(θ) = R_base + (R_head - R_base) · max(0, cos θ)

This produces fires that are somewhat wider than observed in practice.

EllipticalBlending(; formula=:anderson)

Elliptical fire spread model based on Anderson (1983).

The normal speed at angle θ from the push direction is:

F_n(θ) = R_head/(1+ε) · ((cos²θ + sin²θ/LB²) + ε·cos θ)

where ε is the fire eccentricity and LB the length-to-breadth ratio, both derived from the effective midflame wind speed. The first term is the ellipse expansion and the second is the drift that places the ignition at the rear focus (as in the Anderson/Richards fire ellipse convention).

Fields

  • formula::Symbol - Length-to-breadth formula: :anderson (default) or :green

Examples

model = FireSpreadModel(
    SHORT_GRASS,
    UniformWind(speed=8.0),
    UniformMoisture(FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)),
    FlatTerrain(),
    EllipticalBlending(),
)

Comparison

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

g_cos = LevelSetGrid(150, 150, dx=30.0)
ignite!(g_cos, 2250.0, 2250.0, 200.0)
m_cos = FireSpreadModel(SHORT_GRASS, UniformWind(speed=10.0), UniformMoisture(M), FlatTerrain())
simulate!(g_cos, m_cos, steps=200)

g_ell = LevelSetGrid(150, 150, dx=30.0)
ignite!(g_ell, 2250.0, 2250.0, 200.0)
m_ell = FireSpreadModel(SHORT_GRASS, UniformWind(speed=10.0), UniformMoisture(M), FlatTerrain(), EllipticalBlending())
simulate!(g_ell, m_ell, steps=200)

fig = Figure(size=(700, 300))
ax1 = Axis(fig[1, 1], title="CosineBlending (default)", aspect=DataAspect())
ax2 = Axis(fig[1, 2], title="EllipticalBlending", aspect=DataAspect())
heatmap!(ax1, burned(g_cos))
heatmap!(ax2, burned(g_ell))
fig

Length-to-Breadth Ratio

length_to_breadth(U; formula=:anderson)

Compute the fire length-to-breadth ratio from effective midflame wind speed U [m/s].

Formulas

  • :anderson (default) — Anderson (1983): LB = 0.936 · exp(0.2566 · U) + 0.461 · exp(-0.1548 · U) - 0.397
  • :green — Green (1983): LB = 1.1 · U^0.464 (suitable for grass)

Returns at least 1.0 (a circle at zero wind).

Examples

length_to_breadth(0.0)   # ≈ 1.0 (circle)
length_to_breadth(2.0)   # > 1.0 (elongated)
Code
U = 0:0.1:10.0

fig = Figure(size=(600, 300))
ax = Axis(fig[1, 1], xlabel="Wind speed (m/s)", ylabel="Length-to-breadth ratio",
    title="Fire Shape vs. Wind Speed")
lines!(ax, collect(U), length_to_breadth.(U, formula=:anderson), label="Anderson (1983)")
lines!(ax, collect(U), length_to_breadth.(U, formula=:green), label="Green (1983)")
axislegend(ax, position=:lt)
fig

Custom Components

All components follow the same pattern: subtype the abstract type and implement the callable interface.

struct MyWindField <: AbstractWind
    data::Matrix{Float64}
    dx::Float64
    dy::Float64
end

function (w::MyWindField)(t, x, y)
    # Look up wind from gridded data
    i = clamp(round(Int, y / w.dy), 1, size(w.data, 1))
    j = clamp(round(Int, x / w.dx), 1, size(w.data, 2))
    speed = w.data[i, j]
    direction = 0.0  # always from north
    (speed, direction)
end

For dynamic components that evolve with the fire, implement update!:

function SpreadModel.update!(w::MyWindField, grid::LevelSetGrid, dt)
    # Update wind field based on fire state
end