Terrain Effects

Slope, aspect, and topographic influence

Terrain dramatically influences fire behavior. This tutorial explores slope and aspect effects.

using Elmfire
using Plots

Slope Effects

Fire spreads faster uphill due to: - Preheating of fuels above the fire - Convective heat transfer - Flame tilting toward uphill fuels

fuel_table = create_standard_fuel_table(Float64)

slopes = 0:5:35
areas = Float64[]

for slp in slopes
    state = FireState{Float64}(100, 100, 30.0)
    weather = ConstantWeather{Float64}(
        wind_speed_mph = 5.0,
        wind_direction = 180.0,  # From south
        M1 = 0.06, M10 = 0.08, M100 = 0.10,
        MLH = 0.60, MLW = 0.90
    )
    ignite!(state, 50, 50, 0.0)
    # Slope faces south (uphill to north)
    simulate_uniform!(state, 1, fuel_table, weather, Float64(slp), 180.0, 0.0, 30.0)
    push!(areas, get_burned_area_acres(state))
end

plot(slopes, areas,
    xlabel = "Slope (degrees)",
    ylabel = "Burned Area (acres)",
    title = "Slope Effect on Fire Spread (30 min)",
    linewidth = 2,
    marker = :circle,
    legend = false
)

Visualizing Slope Effect

slopes = [0, 10, 20, 30]
plots = []

for slp in slopes
    state = FireState{Float64}(100, 100, 30.0)
    weather = ConstantWeather{Float64}(
        wind_speed_mph = 5.0,
        wind_direction = 180.0,
        M1 = 0.06, M10 = 0.08, M100 = 0.10,
        MLH = 0.60, MLW = 0.90
    )
    ignite!(state, 50, 30, 0.0)
    simulate_uniform!(state, 1, fuel_table, weather, Float64(slp), 180.0, 0.0, 30.0)

    push!(plots, heatmap(state.burned',
        title = "Slope: $(slp)°",
        color = :YlOrRd,
        aspect_ratio = 1,
        colorbar = false
    ))
end

plot(plots..., layout = (2, 2), size = (700, 700),
    plot_title = "Fire Spread on Different Slopes (Uphill to North)")

Aspect Effects

Aspect determines which direction the slope faces:

aspects = [0, 90, 180, 270]
aspect_labels = ["North-facing", "East-facing", "South-facing", "West-facing"]
plots = []

for (asp, lbl) in zip(aspects, aspect_labels)
    state = FireState{Float64}(100, 100, 30.0)
    weather = ConstantWeather{Float64}(
        wind_speed_mph = 5.0,
        wind_direction = 270.0,  # Wind from west
        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, 15.0, Float64(asp), 0.0, 25.0)

    push!(plots, heatmap(state.burned',
        title = lbl,
        color = :YlOrRd,
        aspect_ratio = 1,
        colorbar = false
    ))
end

plot(plots..., layout = (2, 2), size = (700, 700),
    plot_title = "15° Slope with Different Aspects (Wind from West)")

Slope Factor Calculation

The slope factor in the Rothermel model:

slopes_deg = 0:1:45
slope_factors = [calculate_tanslp2(Float64(s)) for s in slopes_deg]

plot(slopes_deg, slope_factors,
    xlabel = "Slope (degrees)",
    ylabel = "tan²(slope)",
    title = "Slope Factor in Rothermel Model",
    linewidth = 2,
    legend = false
)

Wind vs Slope Interaction

Wind and slope can reinforce or oppose each other:

# Create scenarios
scenarios = [
    ("Wind uphill", 180.0, 180.0),      # Wind from S, slope faces S (uphill N)
    ("Wind downhill", 0.0, 180.0),       # Wind from N, slope faces S (downhill N)
    ("Wind cross-slope", 270.0, 180.0),  # Wind from W, slope faces S
]

plots = []

for (name, wind_dir, aspect) in scenarios
    state = FireState{Float64}(100, 100, 30.0)
    weather = ConstantWeather{Float64}(
        wind_speed_mph = 10.0,
        wind_direction = wind_dir,
        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, 20.0, aspect, 0.0, 25.0)

    acres = round(get_burned_area_acres(state), digits=1)
    push!(plots, heatmap(state.burned',
        title = "$name\n$acres acres",
        color = :YlOrRd,
        aspect_ratio = 1,
        colorbar = false
    ))
end

plot(plots..., layout = (1, 3), size = (900, 300))

Complex Terrain

Simulate fire on varying terrain:

ncols, nrows = 100, 100
cellsize = 30.0

# Create a simple terrain: valley running E-W
slope = zeros(Float64, ncols, nrows)
aspect = zeros(Float64, ncols, nrows)

for ix in 1:ncols
    for iy in 1:nrows
        dist_from_center = abs(iy - 50)
        slope[ix, iy] = min(25.0, dist_from_center * 0.6)
        aspect[ix, iy] = iy < 50 ? 180.0 : 0.0  # South-facing above, North-facing below
    end
end

state = FireState{Float64}(ncols, nrows, cellsize)
fuel_ids = fill(1, ncols, nrows)
weather = ConstantWeather{Float64}(
    wind_speed_mph = 8.0,
    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)  # Ignite at valley bottom
simulate!(state, fuel_ids, fuel_table, weather, slope, aspect, 0.0, 45.0)

p1 = heatmap(slope', title = "Slope (degrees)", color = :terrain, aspect_ratio = 1)
p2 = heatmap(aspect', title = "Aspect (degrees)", color = :hsv, aspect_ratio = 1)
p3 = heatmap(state.burned', title = "Burned Area", color = :YlOrRd, aspect_ratio = 1)

toa = copy(state.time_of_arrival)
toa[toa .< 0] .= NaN
p4 = heatmap(toa', title = "Time of Arrival", color = :viridis, aspect_ratio = 1)

plot(p1, p2, p3, p4, layout = (2, 2), size = (800, 800))

Ridge and Valley Effects

Fire behavior changes dramatically at terrain features:

# Create terrain with ridge
ncols, nrows = 120, 120
slope = zeros(Float64, ncols, nrows)
aspect = zeros(Float64, ncols, nrows)

# Ridge running N-S at x=60
for ix in 1:ncols
    for iy in 1:nrows
        dist = abs(ix - 60)
        if dist < 30
            slope[ix, iy] = 20.0 * (1 - dist/30)
            aspect[ix, iy] = ix < 60 ? 90.0 : 270.0  # E or W facing
        end
    end
end

state = FireState{Float64}(ncols, nrows, 30.0)
fuel_ids = fill(1, ncols, nrows)
weather = ConstantWeather{Float64}(
    wind_speed_mph = 10.0,
    wind_direction = 270.0,  # From west
    M1 = 0.05, M10 = 0.07, M100 = 0.09,
    MLH = 0.55, MLW = 0.85
)

ignite!(state, 30, 60, 0.0)  # Ignite west of ridge
simulate!(state, fuel_ids, fuel_table, weather, slope, aspect, 0.0, 45.0)

p1 = heatmap(slope', title = "Ridge Terrain (Slope)", color = :terrain, aspect_ratio = 1)
p2 = heatmap(state.burned', title = "Fire Spread Over Ridge", color = :YlOrRd, aspect_ratio = 1)

plot(p1, p2, layout = (1, 2), size = (800, 400))

Fire accelerates up the windward slope and may slow on the lee side.

Steep Terrain Considerations

On very steep slopes (>30°), fire behavior can become extreme:

# Extreme slope scenario
state = FireState{Float64}(100, 100, 30.0)
weather = ConstantWeather{Float64}(
    wind_speed_mph = 15.0,
    wind_direction = 180.0,  # From south (uphill)
    M1 = 0.04, M10 = 0.06, M100 = 0.08,
    MLH = 0.40, MLW = 0.70
)

ignite!(state, 50, 20, 0.0)
simulate_uniform!(state, 4, fuel_table, weather, 35.0, 180.0, 0.0, 20.0)

# Check fireline intensity
max_flin = maximum(state.fireline_intensity)
println("Maximum fireline intensity: $(round(max_flin, digits=0)) kW/m")

# Classify intensity
if max_flin > 10000
    println("EXTREME fire behavior - spotting likely, aerial suppression ineffective")
elseif max_flin > 4000
    println("VERY HIGH intensity - indirect attack recommended")
elseif max_flin > 2000
    println("HIGH intensity - mechanical equipment needed")
else
    println("MODERATE intensity - direct attack possible")
end

flin = copy(state.fireline_intensity)
flin[flin .== 0] .= NaN

heatmap(flin',
    title = "Fireline Intensity on 35° Slope\nMax: $(round(max_flin, digits=0)) kW/m",
    color = :inferno,
    aspect_ratio = 1
)
Maximum fireline intensity: 54264.0 kW/m
EXTREME fire behavior - spotting likely, aerial suppression ineffective