using Elmfire
using Plots
using Random
Random.seed!(42)TaskLocalRNG()
Ember generation and transport
Spotting (ember transport) creates new ignitions downwind of the main fire. This tutorial covers Elmfire.jl’s spotting capabilities.
Embers (firebrands) are generated by: - Burning vegetation lofted by convection - Higher intensity fires produce more embers - Crown fires generate significantly more embers
# Create spotting parameters
spotting_params = SpottingParameters{Float64}(
mean_distance = 100.0, # Mean spotting distance (m)
normalized_variance = 0.5, # Normalized variance
ws_exponent = 1.0, # Wind speed exponent
flin_exponent = 0.5, # Fireline intensity exponent
nembers_max = 10, # Max embers per cell per timestep
surface_spotting_percent = 1.0, # Surface fire spotting probability (%)
crown_spotting_percent = 10.0, # Crown fire spotting probability (%)
pign = 50.0, # Ignition probability (%)
min_distance = 10.0, # Min distance (m)
max_distance = 2000.0 # Max distance (m)
)SpottingParameters{Float64}(100.0, 0.5, 1.0, 0.5, 10, 1.0, 10.0, 50.0, 10.0, 2000.0)
Embers are transported downwind with distances that scale with wind speed and fireline intensity.
These simulations set use_sardoy = true, which draws the distance from the Sardoy (2008) correlation — a lognormal whose parameters come from the fireline intensity and 10 m wind directly. For the chaparral fire below that gives a median throw of about 124 m.
The alternative is the empirical scaling below, where the mean distance is mean_distance × I^flin_exponent × U^ws_exponent. Note that mean_distance is a coefficient, not a distance: with flin_exponent = 0.5 and a chaparral fireline intensity near 59,000 kW/m, a mean_distance of 100 implies a mean throw of hundreds of kilometres, which max_distance then truncates. Prefer the Sardoy model unless you have calibrated the coefficient for your own fuels.
The SpottingParameters control:
ncols, nrows = 150, 150
cellsize = 30.0
state = FireState{Float64}(ncols, nrows, cellsize)
fuel_table = create_standard_fuel_table(Float64)
fuel_ids = fill(4, ncols, nrows) # Chaparral (high intensity)
slope = zeros(Float64, ncols, nrows)
aspect = zeros(Float64, ncols, nrows)
weather = ConstantWeather{Float64}(
wind_speed_mph = 20.0,
wind_direction = 270.0,
M1 = 0.04, M10 = 0.06, M100 = 0.08,
MLH = 0.40, MLW = 0.70
)
config = SimulationConfig{Float64}(
enable_crown_fire = false,
enable_spotting = true,
spotting_params = spotting_params,
use_sardoy = true
)
ignite!(state, 40, 75, 0.0)
weather_interp = create_constant_interpolator(weather, ncols, nrows, cellsize)
simulate_full!(state, fuel_ids, fuel_table, weather_interp, slope, aspect,
0.0, 45.0; config = config, rng = MersenneTwister(42))
# Visualize
toa = copy(state.time_of_arrival)
toa[toa .< 0] .= NaN
heatmap(toa',
title = "Fire Spread with Spotting Enabled",
xlabel = "X (cells)",
ylabel = "Y (cells)",
color = :viridis,
aspect_ratio = 1,
size = (600, 500)
)# Without spotting
state_no_spot = FireState{Float64}(ncols, nrows, cellsize)
ignite!(state_no_spot, 40, 75, 0.0)
config_no_spot = SimulationConfig{Float64}(enable_spotting = false)
simulate_full!(state_no_spot, fuel_ids, fuel_table, weather_interp, slope, aspect,
0.0, 45.0; config = config_no_spot)
# With spotting
state_spot = FireState{Float64}(ncols, nrows, cellsize)
ignite!(state_spot, 40, 75, 0.0)
simulate_full!(state_spot, fuel_ids, fuel_table, weather_interp, slope, aspect,
0.0, 45.0; config = config, rng = MersenneTwister(42))
p1 = heatmap(state_no_spot.burned',
title = "Without Spotting\n$(round(get_burned_area_acres(state_no_spot), digits=1)) acres",
color = :YlOrRd, aspect_ratio = 1)
p2 = heatmap(state_spot.burned',
title = "With Spotting\n$(round(get_burned_area_acres(state_spot), digits=1)) acres",
color = :YlOrRd, aspect_ratio = 1)
plot(p1, p2, layout = (1, 2), size = (800, 400))The number of embers depends on:
Extreme fires can spot miles ahead:
# High-intensity scenario
ncols_big, nrows_big = 300, 200
state_big = FireState{Float64}(ncols_big, nrows_big, cellsize)
fuel_ids_big = fill(4, ncols_big, nrows_big)
slope_big = zeros(Float64, ncols_big, nrows_big)
aspect_big = zeros(Float64, ncols_big, nrows_big)
extreme_weather = ConstantWeather{Float64}(
wind_speed_mph = 35.0,
wind_direction = 270.0,
M1 = 0.03, M10 = 0.05, M100 = 0.07,
MLH = 0.35, MLW = 0.60
)
extreme_spotting = SpottingParameters{Float64}(
mean_distance = 200.0, # Longer mean distance (m)
normalized_variance = 0.5,
ws_exponent = 1.0,
flin_exponent = 0.5,
nembers_max = 20, # More embers
surface_spotting_percent = 2.0,
crown_spotting_percent = 15.0,
pign = 60.0, # Higher ignition probability
min_distance = 10.0,
max_distance = 5000.0 # Longer max distance (m)
)
config_extreme = SimulationConfig{Float64}(
enable_spotting = true,
spotting_params = extreme_spotting,
use_sardoy = true
)
ignite!(state_big, 50, 100, 0.0)
weather_interp_big = create_constant_interpolator(extreme_weather, ncols_big, nrows_big, cellsize)
simulate_full!(state_big, fuel_ids_big, fuel_table, weather_interp_big,
slope_big, aspect_big, 0.0, 60.0;
config = config_extreme, rng = MersenneTwister(42))
toa = copy(state_big.time_of_arrival)
toa[toa .< 0] .= NaN
heatmap(toa',
title = "Extreme Fire with Long-Range Spotting",
color = :viridis,
aspect_ratio = 1,
size = (800, 500)
)