using Elmfire
using PlotsWeather
Weather data management and interpolation
This module handles weather data for fire simulations, including spatially and temporally varying conditions.
Types
ConstantWeather
Constant (spatially and temporally uniform) weather conditions.
struct ConstantWeather{T<:AbstractFloat}
wind_speed_20ft::T # 20-ft wind speed (mph)
wind_direction::T # Wind direction (degrees, meteorological: FROM)
M1::T # 1-hr dead fuel moisture (fraction)
M10::T # 10-hr dead fuel moisture (fraction)
M100::T # 100-hr dead fuel moisture (fraction)
MLH::T # Live herbaceous moisture (fraction)
MLW::T # Live woody moisture (fraction)
endConstructor:
ConstantWeather{T}(;
wind_speed_mph = 10.0,
wind_direction = 0.0,
M1 = 0.06,
M10 = 0.08,
M100 = 0.10,
MLH = 0.60,
MLW = 0.90
) -> ConstantWeather{T}# Create weather with default values
weather = ConstantWeather{Float64}()
println("Wind: $(weather.wind_speed_20ft) mph from $(weather.wind_direction)°")
println("Dead fuel moisture: M1=$(weather.M1), M10=$(weather.M10), M100=$(weather.M100)")
println("Live fuel moisture: MLH=$(weather.MLH), MLW=$(weather.MLW)")Wind: 10.0 mph from 0.0°
Dead fuel moisture: M1=0.06, M10=0.08, M100=0.1
Live fuel moisture: MLH=0.6, MLW=0.9
# Custom weather conditions
dry_weather = ConstantWeather{Float64}(
wind_speed_mph = 25.0, # Strong wind
wind_direction = 270.0, # From west
M1 = 0.03, # Very dry fine fuels
M10 = 0.04,
M100 = 0.06,
MLH = 0.30, # Dry live fuels
MLW = 0.50
)ConstantWeather{Float64}(25.0, 270.0, 0.03, 0.04, 0.06, 0.3, 0.5)
Weather Variables
| Variable | Description | Typical Range |
|---|---|---|
wind_speed_20ft |
Wind speed at 20 ft height (mph) | 0-40 |
wind_direction |
Direction wind is FROM (degrees, N=0, E=90) | 0-360 |
M1 |
1-hour dead fuel moisture (fraction) | 0.03-0.15 |
M10 |
10-hour dead fuel moisture (fraction) | 0.04-0.20 |
M100 |
100-hour dead fuel moisture (fraction) | 0.06-0.25 |
MLH |
Live herbaceous moisture (fraction) | 0.30-1.20 |
MLW |
Live woody moisture (fraction) | 0.50-1.50 |
WeatherGrid
A grid of weather values at a single time, for spatially varying conditions.
struct WeatherGrid{T<:AbstractFloat}
ws::Matrix{T} # Wind speed (mph)
wd::Matrix{T} # Wind direction (degrees, FROM)
m1::Matrix{T} # 1-hour dead fuel moisture
m10::Matrix{T} # 10-hour dead fuel moisture
m100::Matrix{T} # 100-hour dead fuel moisture
mlh::Matrix{T} # Live herbaceous moisture
mlw::Matrix{T} # Live woody moisture
ncols::Int # Number of columns
nrows::Int # Number of rows
cellsize::T # Cell size (meters)
xllcorner::T # X coordinate of lower-left corner
yllcorner::T # Y coordinate of lower-left corner
endConstructors:
# Create empty weather grid
WeatherGrid{T}(ncols, nrows, cellsize; xllcorner=0.0, yllcorner=0.0)
# Create uniform grid from ConstantWeather
WeatherGrid{T}(weather::ConstantWeather, ncols, nrows, cellsize)# Create a weather grid with spatial variation
ncols, nrows = 10, 10
cellsize = 1000.0 # 1km cells
wgrid = WeatherGrid{Float64}(ncols, nrows, cellsize)
# Add wind gradient (wind speed increases to the east)
for ix in 1:ncols
wgrid.ws[ix, :] .= 5.0 + 2.0 * ix # 7-25 mph gradient
wgrid.wd[ix, :] .= 270.0 # From west
end
println("Wind speed range: $(minimum(wgrid.ws)) - $(maximum(wgrid.ws)) mph")Wind speed range: 7.0 - 25.0 mph
WeatherTimeSeries
A time series of weather grids for temporally varying conditions.
struct WeatherTimeSeries{T<:AbstractFloat}
grids::Vector{WeatherGrid{T}} # Weather grids at each time
times::Vector{T} # Times (minutes from start)
dt::T # Time step between grids
endConstructors:
# From vector of grids and times
WeatherTimeSeries{T}(grids::Vector{WeatherGrid}, times::Vector)
# Constant weather over duration
WeatherTimeSeries{T}(weather::ConstantWeather, ncols, nrows, cellsize, duration)# Create time-varying weather
weather1 = ConstantWeather{Float64}(wind_speed_mph = 10.0, wind_direction = 270.0,
M1 = 0.08, M10 = 0.10, M100 = 0.12, MLH = 0.70, MLW = 1.0)
weather2 = ConstantWeather{Float64}(wind_speed_mph = 20.0, wind_direction = 315.0,
M1 = 0.05, M10 = 0.07, M100 = 0.09, MLH = 0.50, MLW = 0.80)
grid1 = WeatherGrid{Float64}(weather1, 1, 1, 1e6)
grid2 = WeatherGrid{Float64}(weather2, 1, 1, 1e6)
wts = WeatherTimeSeries{Float64}([grid1, grid2], [0.0, 60.0])
println("Weather at t=0: $(wts.grids[1].ws[1,1]) mph from $(wts.grids[1].wd[1,1])°")
println("Weather at t=60: $(wts.grids[2].ws[1,1]) mph from $(wts.grids[2].wd[1,1])°")Weather at t=0: 10.0 mph from 270.0°
Weather at t=60: 20.0 mph from 315.0°
WeatherInterpolator
Handles interpolation of weather data to simulation grid and time.
struct WeatherInterpolator{T<:AbstractFloat}
weather_series::WeatherTimeSeries{T}
xcol_map::Vector{T} # Fractional weather column for each sim column
yrow_map::Vector{T} # Fractional weather row for each sim row
sim_ncols::Int
sim_nrows::Int
endFunctions
create_constant_interpolator
Create a weather interpolator for constant (uniform) weather conditions.
create_constant_interpolator(
weather::ConstantWeather{T},
sim_ncols::Int, sim_nrows::Int,
sim_cellsize::T
) -> WeatherInterpolator{T}weather = ConstantWeather{Float64}(wind_speed_mph = 15.0, wind_direction = 270.0,
M1 = 0.06, M10 = 0.08, M100 = 0.10, MLH = 0.60, MLW = 0.90)
interp = create_constant_interpolator(weather, 100, 100, 30.0)
# Get weather at any cell - always returns the same values
w = get_weather_at(interp, 50, 50, 30.0)
println("Wind: $(w.ws) mph from $(w.wd)°")Wind: 15.0 mph from 270.0°
get_weather_at
Get interpolated weather values at a simulation grid cell and time.
get_weather_at(interp::WeatherInterpolator, ix::Int, iy::Int, t::T) -> NamedTupleReturns a named tuple with fields: ws, wd, m1, m10, m100, mlh, mlw
w = get_weather_at(interp, 25, 75, 0.0)
println("Wind speed: $(w.ws) mph")
println("Wind direction: $(w.wd)°")
println("1-hr moisture: $(w.m1)")
println("Live herb moisture: $(w.mlh)")Wind speed: 15.0 mph
Wind direction: 270.0°
1-hr moisture: 0.06
Live herb moisture: 0.6
find_time_indices
Find the indices and interpolation weight for a given time.
find_time_indices(wts::WeatherTimeSeries, t::T) -> Tuple{Int, Int, T}Returns (i_lo, i_hi, f) where the interpolated value is: value = (1-f) * grids[i_lo] + f * grids[i_hi]
# Time series with weather at t=0 and t=60
i_lo, i_hi, f = find_time_indices(wts, 30.0)
println("At t=30: i_lo=$i_lo, i_hi=$i_hi, f=$f (50% interpolation)")At t=30: i_lo=1, i_hi=2, f=0.5 (50% interpolation)
interpolate_wind_direction
Interpolate wind direction, properly handling the 0°/360° wrap-around.
interpolate_wind_direction(wd1::T, wd2::T, f::T) -> T# Normal interpolation
wd = interpolate_wind_direction(270.0, 315.0, 0.5)
println("Interpolated direction (270° to 315°): $(round(wd, digits=1))°")
# Wrap-around interpolation
wd = interpolate_wind_direction(350.0, 10.0, 0.5)
println("Interpolated direction (350° to 10°): $(round(wd, digits=1))°")Interpolated direction (270° to 315°): 292.5°
Interpolated direction (350° to 10°): 360.0°
create_grid_mapping
Create mapping from simulation grid to weather grid coordinates for bilinear interpolation.
create_grid_mapping(
weather_grid::WeatherGrid,
sim_ncols, sim_nrows, sim_cellsize,
sim_xllcorner, sim_yllcorner
) -> Tuple{Vector{T}, Vector{T}}Returns (xcol_map, yrow_map) vectors of fractional weather grid coordinates. Simulation cell (ix, iy) maps to fractional weather position (xcol_map[ix], yrow_map[iy]), which is used for bilinear interpolation across the four surrounding weather grid cells.
Spatial Interpolation
get_weather_at uses bilinear interpolation for spatial weather fields, providing smooth gradients across the simulation domain. Wind direction is interpolated via sin/cos decomposition to correctly handle the 0°/360° wrap-around.
For temporal interpolation between weather grids, linear interpolation is used (with the same sin/cos method for wind direction).
Weather Effects on Fire Spread
Wind Speed Effects
fuel_table = create_standard_fuel_table(Float64)
wind_speeds = [5, 10, 15, 20, 25, 30]
areas = Float64[]
for ws in wind_speeds
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
)
state = FireState{Float64}(100, 100, 30.0)
ignite!(state, 50, 50, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 20.0)
push!(areas, get_burned_area_acres(state))
end
plot(wind_speeds, areas,
xlabel = "Wind Speed (mph)",
ylabel = "Burned Area (acres)",
title = "Effect of Wind Speed on Fire Spread (20 min)",
linewidth = 2,
marker = :circle,
legend = false
)Fuel Moisture Effects
moisture_levels = [0.03, 0.06, 0.09, 0.12, 0.15]
areas = Float64[]
for m1 in moisture_levels
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
)
state = FireState{Float64}(100, 100, 30.0)
ignite!(state, 50, 50, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 20.0)
push!(areas, get_burned_area_acres(state))
end
plot(moisture_levels .* 100, areas,
xlabel = "1-hr Fuel Moisture (%)",
ylabel = "Burned Area (acres)",
title = "Effect of Fuel Moisture on Fire Spread (20 min)",
linewidth = 2,
marker = :circle,
legend = false
)Wind Direction
Wind direction is specified in meteorological convention: the direction the wind is blowing FROM.
| Direction | Degrees | Fire Spreads To |
|---|---|---|
| North | 0° or 360° | South |
| East | 90° | West |
| South | 180° | North |
| West | 270° | East |
directions = [0, 90, 180, 270]
labels = ["From N", "From E", "From S", "From W"]
plots_arr = []
for (dir, lbl) in zip(directions, labels)
weather = ConstantWeather{Float64}(
wind_speed_mph = 15.0,
wind_direction = Float64(dir),
M1 = 0.06, M10 = 0.08, M100 = 0.10,
MLH = 0.60, MLW = 0.90
)
state = FireState{Float64}(80, 80, 30.0)
ignite!(state, 40, 40, 0.0)
simulate_uniform!(state, 1, fuel_table, weather, 0.0, 0.0, 0.0, 20.0)
push!(plots_arr, heatmap(state.burned', title = lbl, color = :YlOrRd,
aspect_ratio = 1, colorbar = false))
end
plot(plots_arr..., layout = (2, 2), size = (600, 600))Fire Weather Scenarios
Typical Conditions
# Moderate conditions
moderate = ConstantWeather{Float64}(
wind_speed_mph = 8.0,
wind_direction = 270.0,
M1 = 0.08, M10 = 0.10, M100 = 0.12,
MLH = 0.80, MLW = 1.00
)
# Hot/dry conditions
hot_dry = ConstantWeather{Float64}(
wind_speed_mph = 15.0,
wind_direction = 270.0,
M1 = 0.04, M10 = 0.06, M100 = 0.08,
MLH = 0.40, MLW = 0.60
)
# Red flag conditions
red_flag = ConstantWeather{Float64}(
wind_speed_mph = 25.0,
wind_direction = 45.0, # Santa Ana direction
M1 = 0.02, M10 = 0.04, M100 = 0.06,
MLH = 0.30, MLW = 0.50
)
println("Moderate: $(moderate.wind_speed_20ft) mph, $(moderate.M1*100)% M1")
println("Hot/Dry: $(hot_dry.wind_speed_20ft) mph, $(hot_dry.M1*100)% M1")
println("Red Flag: $(red_flag.wind_speed_20ft) mph, $(red_flag.M1*100)% M1")Moderate: 8.0 mph, 8.0% M1
Hot/Dry: 15.0 mph, 4.0% M1
Red Flag: 25.0 mph, 2.0% M1
Comparing Scenarios
scenarios = [
("Moderate", moderate),
("Hot/Dry", hot_dry),
("Red Flag", red_flag)
]
plots_arr = []
for (name, weather) in scenarios
state = FireState{Float64}(100, 100, 30.0)
ignite!(state, 30, 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_arr, heatmap(state.burned',
title = "$name\n$acres acres",
color = :YlOrRd, aspect_ratio = 1, colorbar = false))
end
plot(plots_arr..., layout = (1, 3), size = (900, 300))