using Elmfire
using PlotsWeather Effects
Wind, moisture, and time-varying weather
Weather is the primary driver of fire behavior. This tutorial explores how different weather parameters affect fire spread.
Wind Speed Effects
Wind is the most critical weather factor for fire spread:
fuel_table = create_standard_fuel_table(Float64)
wind_speeds = 0:5:30
burned_areas = Float64[]
for ws in wind_speeds
state = FireState{Float64}(100, 100, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = Float64(ws),
wind_direction = 270.0,
M1 = 0.06, M10 = 0.08, M100 = 0.10,
MLH = 0.60, MLW = 0.90
)
ignite!(state, 50, 50, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 30.0)
push!(burned_areas, get_burned_area_acres(state))
end
plot(wind_speeds, burned_areas,
xlabel = "Wind Speed (mph)",
ylabel = "Burned Area (acres) in 30 min",
title = "Wind Speed vs Fire Size",
linewidth = 2,
marker = :circle,
legend = false
)Wind Creates Elliptical Spread
The fire spread pattern changes dramatically with wind:
speeds = [0, 10, 20, 30]
plots = []
for ws in speeds
state = FireState{Float64}(100, 100, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = Float64(ws),
wind_direction = 270.0,
M1 = 0.06, M10 = 0.08, M100 = 0.10,
MLH = 0.60, MLW = 0.90
)
ignite!(state, 50, 50, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 30.0)
push!(plots, heatmap(state.burned',
title = "$(ws) mph",
color = :YlOrRd,
aspect_ratio = 1,
colorbar = false,
axis = false
))
end
plot(plots..., layout = (2, 2), size = (700, 700),
plot_title = "Wind Speed Effect on Fire Shape")Elliptical Spread Model
Elmfire uses the Anderson elliptical spread model:
# Visualize how wind speed affects fire shape
# Higher wind = more elongated ellipse stretching downwind
wind_speeds = [0.0, 2.0, 5.0, 8.0]
p = plot(aspect_ratio = 1, legend = :topright,
xlabel = "Distance (ft)", ylabel = "Distance (ft)",
title = "Fire Spread Shape vs Wind Speed\n(after 1 minute, wind blowing East →)")
for ws in wind_speeds
# Use a base spread rate that increases with wind (simplified)
base_ros = 10.0 + ws * 2 # Faster spread with more wind
es = elliptical_spread(base_ros, ws)
# Ellipse with ignition at upwind focus (origin)
a = (es.head + es.back) / 2 # Semi-major axis
c = (es.head - es.back) / 2 # Focus offset (ignition is at upwind focus)
b = es.flank # Semi-minor axis
# Parametric ellipse centered at (c, 0), with ignition at origin
t = range(0, 2π, length=200)
x = [c + a * cos(τ) for τ in t]
y = [b * sin(τ) for τ in t]
plot!(p, x, y, label = "$(Int(ws)) mph wind", linewidth = 2)
end
# Mark ignition point
scatter!(p, [0], [0], marker = :star, markersize = 12, color = :red, label = "Ignition")
# Wind arrow pointing east (direction wind blows)
quiver!(p, [-8], [8], quiver=([10], [0]), color = :gray, linewidth = 2)
pWind Direction
Wind direction determines which way the fire spreads fastest:
directions = 0:45:315
dir_labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
plots = []
for (wd, lbl) in zip(directions, dir_labels)
state = FireState{Float64}(80, 80, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = 15.0,
wind_direction = Float64(wd),
M1 = 0.06, M10 = 0.08, M100 = 0.10,
MLH = 0.60, MLW = 0.90
)
ignite!(state, 40, 40, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 25.0)
push!(plots, heatmap(state.burned',
title = "From $lbl ($(wd)°)",
color = :YlOrRd,
aspect_ratio = 1,
colorbar = false,
axis = false
))
end
plot(plots..., layout = (2, 4), size = (1000, 500))Fuel Moisture
Fuel moisture critically affects fire intensity and spread rate:
moistures = 0.02:0.02:0.16
areas = Float64[]
intensities = Float64[]
for m1 in moistures
state = FireState{Float64}(100, 100, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = 10.0,
wind_direction = 270.0,
M1 = m1,
M10 = m1 + 0.02,
M100 = m1 + 0.04,
MLH = 0.60,
MLW = 0.90
)
ignite!(state, 50, 50, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 30.0)
push!(areas, get_burned_area_acres(state))
push!(intensities, maximum(state.fireline_intensity))
end
p1 = plot(moistures .* 100, areas,
xlabel = "1-hr Fuel Moisture (%)",
ylabel = "Burned Area (acres)",
title = "Moisture vs Fire Size",
linewidth = 2,
legend = false
)
p2 = plot(moistures .* 100, intensities,
xlabel = "1-hr Fuel Moisture (%)",
ylabel = "Max Intensity (kW/m)",
title = "Moisture vs Intensity",
linewidth = 2,
legend = false,
color = :red
)
plot(p1, p2, layout = (1, 2), size = (800, 350))Moisture Damping Coefficient
The moisture damping reduces fire intensity as moisture increases:
# Get a fuel model
fm = get_fuel_model(fuel_table, 1, 60)
# Calculate moisture damping for range of moistures
# moisture_damping takes the ratio: moisture / extinction moisture
m_range = 0.02:0.01:fm.mex_dead
damping = [moisture_damping(Float64(m / fm.mex_dead)) for m in m_range]
plot(m_range .* 100, damping,
xlabel = "Dead Fuel Moisture (%)",
ylabel = "Moisture Damping Coefficient",
title = "Moisture Damping (FBFM01, Mex_dead=$(round(fm.mex_dead*100))%)",
linewidth = 2,
legend = false,
xlims = (0, 30)
)
vline!([fm.mex_dead * 100], linestyle = :dash, label = "Extinction")Live Fuel Moisture
Live herbaceous and woody moisture affect flame length and intensity:
mlh_values = [0.30, 0.60, 0.90, 1.20] # Live herbaceous
mlh_labels = ["30%", "60%", "90%", "120%"]
plots = []
for (mlh, lbl) in zip(mlh_values, mlh_labels)
state = FireState{Float64}(80, 80, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = 10.0,
wind_direction = 270.0,
M1 = 0.06, M10 = 0.08, M100 = 0.10,
MLH = mlh,
MLW = 0.90
)
ignite!(state, 40, 40, 0.0)
simulate_uniform!(state, 2, fuel_table, weather, 0.0, 0.0, 0.0, 25.0) # FBFM02 has live fuels
flin = copy(state.fireline_intensity)
flin[flin .== 0] .= NaN
push!(plots, heatmap(flin',
title = "MLH = $lbl",
color = :inferno,
aspect_ratio = 1,
clims = (0, 500)
))
end
plot(plots..., layout = (2, 2), size = (700, 700),
plot_title = "Live Herbaceous Moisture Effect on Intensity")Combining Effects
Wind and moisture interact to produce complex fire behavior:
wind_speeds = [5, 15]
moistures = [0.04, 0.10]
plots = []
for ws in wind_speeds
for m1 in moistures
state = FireState{Float64}(100, 100, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = Float64(ws),
wind_direction = 270.0,
M1 = m1, M10 = m1 + 0.02, M100 = m1 + 0.04,
MLH = 0.60, MLW = 0.90
)
ignite!(state, 50, 50, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 30.0)
acres = round(get_burned_area_acres(state), digits=1)
push!(plots, heatmap(state.burned',
title = "$(ws)mph, $(Int(m1*100))% M\n$acres ac",
color = :YlOrRd,
aspect_ratio = 1,
colorbar = false
))
end
end
plot(plots..., layout = (2, 2), size = (700, 700))Time-Varying Weather
For extended simulations, weather changes over time. Use WeatherTimeSeries:
ncols, nrows = 100, 100
cellsize = 30.0
# Create weather grids for different times
function make_weather_grid(ws, wd, m1)
ConstantWeather{Float64}(
wind_speed_mph = ws,
wind_direction = wd,
M1 = m1, M10 = m1 + 0.02, M100 = m1 + 0.04,
MLH = 0.60, MLW = 0.90
)
end
# Morning: light winds from east, higher moisture
grid1 = WeatherGrid{Float64}(make_weather_grid(5.0, 90.0, 0.08), 1, 1, 1e6)
# Afternoon: strong winds from west, drier
grid2 = WeatherGrid{Float64}(make_weather_grid(15.0, 270.0, 0.05), 1, 1, 1e6)
# Evening: moderate winds from south
grid3 = WeatherGrid{Float64}(make_weather_grid(10.0, 180.0, 0.06), 1, 1, 1e6)
# Create time series
times = [0.0, 60.0, 120.0]
weather_series = WeatherTimeSeries{Float64}([grid1, grid2, grid3], times)
# Create interpolator
weather_interp = WeatherInterpolator(weather_series, ncols, nrows, cellsize)
# Run simulation
state = FireState{Float64}(ncols, nrows, cellsize)
fuel_ids = fill(1, ncols, nrows)
slope = zeros(Float64, ncols, nrows)
aspect = zeros(Float64, ncols, nrows)
ignite!(state, 50, 50, 0.0)
simulate_full!(state, fuel_ids, fuel_table, weather_interp, slope, aspect,
0.0, 180.0) # 3 hours
# Visualize with time markers
toa = copy(state.time_of_arrival)
toa[toa .< 0] .= NaN
heatmap(toa',
title = "Time of Arrival - Changing Weather\n(Wind shifts at 60 & 120 min)",
xlabel = "X", ylabel = "Y",
color = :viridis,
aspect_ratio = 1,
size = (600, 500)
)Weather at a Specific Time
Query interpolated weather values:
# Get weather at center cell at different times
ix, iy = 50, 50
for t in [0.0, 30.0, 60.0, 90.0, 120.0]
w = get_weather_at(weather_interp, ix, iy, t)
println("t=$(Int(t))min: WS=$(round(w.ws, digits=1))mph, WD=$(round(w.wd))°, M1=$(round(w.m1*100, digits=1))%")
endt=0min: WS=5.0mph, WD=90.0°, M1=8.0%
t=30min: WS=10.0mph, WD=180.0°, M1=6.5%
t=60min: WS=15.0mph, WD=270.0°, M1=5.0%
t=90min: WS=12.5mph, WD=225.0°, M1=5.5%
t=120min: WS=10.0mph, WD=180.0°, M1=6.0%
Wind Adjustment Factor
The 20-ft wind is adjusted to mid-flame height based on fuel bed depth:
depths = 0.1:0.1:5.0
wafs = [wind_adjustment_factor(d) for d in depths]
plot(depths, wafs,
xlabel = "Fuel Bed Depth (ft)",
ylabel = "Wind Adjustment Factor",
title = "20-ft to Mid-flame Wind Adjustment",
linewidth = 2,
legend = false,
ylims = (0, 1)
)Taller fuel beds have less wind reduction at mid-flame height.
Extreme Weather Scenarios
Model extreme fire weather conditions:
scenarios = [
("Moderate", 10.0, 0.08),
("Red Flag", 25.0, 0.04),
("Extreme", 40.0, 0.02)
]
plots = []
for (name, ws, m1) in scenarios
state = FireState{Float64}(150, 150, 30.0)
weather = ConstantWeather{Float64}(
wind_speed_mph = ws,
wind_direction = 270.0,
M1 = m1, M10 = m1 + 0.02, M100 = m1 + 0.04,
MLH = 0.40, MLW = 0.70
)
ignite!(state, 75, 75, 0.0)
simulate_uniform!(state, 4, fuel_table, weather, 10.0, 180.0, 0.0, 30.0)
acres = round(get_burned_area_acres(state), digits=1)
push!(plots, heatmap(state.burned',
title = "$name\n$acres acres in 30 min",
color = :YlOrRd,
aspect_ratio = 1,
colorbar = false
))
end
plot(plots..., layout = (1, 3), size = (900, 300))