WUI Modeling

Wildland-urban interface and building ignition

The Wildland-Urban Interface (WUI) is where homes and structures meet wildland vegetation. This tutorial covers modeling building ignition during wildfires.

using Elmfire
using Plots
using Random
Random.seed!(42)
TaskLocalRNG()

WUI Concepts

Buildings in the WUI can ignite from:

  1. Direct flame contact - Fire reaches the structure
  2. Radiative heat - Heat flux from nearby flames
  3. Embers (firebrands) - Burning debris lands on combustibles
  4. Building-to-building spread - Fire spreads between structures

Creating Buildings

Define individual buildings:

# Create a wood-frame building
building = WUIBuilding{Float64}(
    1,              # ID
    30, 30;         # Grid coordinates
    construction_type = :wood,
    combustible_fraction = 0.7,
    ignition_temperature = 300.0
)

println("Building $(building.id) at ($(building.ix), $(building.iy))")
println("  Type: $(building.construction_type)")
println("  Combustible fraction: $(building.combustible_fraction)")
Building 1 at (30, 30)
  Type: wood
  Combustible fraction: 0.7

Construction Types

Different construction materials have different vulnerability:

construction_types = [:wood, :masonry, :mixed]

for ctype in construction_types
    b = WUIBuilding{Float64}(1, 10, 10; construction_type = ctype)
    # Test ignition probability at same heat flux
    prob = building_ignition_probability(b, 30.0, 10.0)
    println("$ctype: Ignition probability = $(round(prob * 100, digits=1))%")
end
wood: Ignition probability = 100.0%
masonry: Ignition probability = 83.2%
mixed: Ignition probability = 99.8%

Creating a WUI Grid

For simulations, create a grid of buildings:

ncols, nrows = 100, 100

# Create a grid of buildings
buildings = create_building_grid(
    Float64,
    ncols, nrows,
    10,             # Building spacing (cells)
    3,              # Building footprint (cells)
    20, 20;         # Starting position
    construction_type = :wood
)

# Create WUI grid
wui_grid = WUIGrid{Float64}(buildings, ncols, nrows)

println("Created $(length(buildings)) buildings")

# Visualize building locations
building_map = zeros(ncols, nrows)
for b in buildings
    building_map[b.ix, b.iy] = 1
end

heatmap(building_map',
    title = "Building Locations",
    color = [:white, :blue],
    aspect_ratio = 1,
    colorbar = false
)
Created 64 buildings

Radiative Heat Flux

Buildings ignite from radiant heat when nearby fire intensities are high:

# Heat flux vs distance for different fireline intensities
distances = 1:1:50  # meters
intensities = [500, 1000, 2000, 4000]  # kW/m

p = plot(xlabel = "Distance (m)", ylabel = "Heat Flux (kW/m²)",
    title = "Radiative Heat Flux from Fire", legend = :topright)

for I in intensities
    fluxes = [compute_radiative_heat_flux(Float64(I), Float64(d), 5.0) for d in distances]
    plot!(p, distances, fluxes, label = "I = $I kW/m", linewidth = 2)
end

# Add ignition threshold
hline!(p, [12.5], linestyle = :dash, color = :red, label = "Wood ignition threshold")

p

Building Ignition Probability

Ignition probability depends on heat flux and exposure time:

wood_building = WUIBuilding{Float64}(1, 10, 10; construction_type = :wood)
masonry_building = WUIBuilding{Float64}(2, 10, 10; construction_type = :masonry)

# Heat flux range
heat_fluxes = 5:5:50  # kW/m²
exposure_time = 10.0  # minutes

p = plot(xlabel = "Heat Flux (kW/m²)", ylabel = "Ignition Probability",
    title = "Building Ignition Probability (10 min exposure)",
    legend = :bottomright)

wood_probs = [building_ignition_probability(wood_building, Float64(q), exposure_time)
    for q in heat_fluxes]
masonry_probs = [building_ignition_probability(masonry_building, Float64(q), exposure_time)
    for q in heat_fluxes]

plot!(p, heat_fluxes, wood_probs, label = "Wood", linewidth = 2)
plot!(p, heat_fluxes, masonry_probs, label = "Masonry", linewidth = 2)

p

Effect of Exposure Time

exposure_times = [1, 5, 10, 20, 30]  # minutes
heat_flux = 25.0  # kW/m²

probs = [building_ignition_probability(wood_building, heat_flux, Float64(t))
    for t in exposure_times]

bar(string.(exposure_times),
    probs .* 100,
    xlabel = "Exposure Time (min)",
    ylabel = "Ignition Probability (%)",
    title = "Ignition Probability vs Exposure Time\n(Wood building, 25 kW/m² heat flux)",
    legend = false,
    color = :orange
)

Hamada Urban Fire Spread

The Hamada model describes fire spread between buildings:

source = WUIBuilding{Float64}(1, 10, 10; construction_type = :wood)
params = HamadaParameters{Float64}(
    critical_separation = 15.0,  # meters
    wind_spread_factor = 1.5,
    ember_generation_rate = 0.1,
    base_spread_rate = 1.0
)

# Probability vs distance
separations = 1:1:20  # cells
cellsize = 1.0  # feet (small for test)

probs = Float64[]
for sep in separations
    target = WUIBuilding{Float64}(2, 10 + sep, 10; construction_type = :wood)
    prob = hamada_spread_probability(source, target, params, 10.0, 270.0, cellsize)
    push!(probs, prob)
end

# Convert cell distance to meters
dist_m = separations .* cellsize .* 0.3048

plot(dist_m, probs,
    xlabel = "Building Separation (m)",
    ylabel = "Spread Probability",
    title = "Hamada Building-to-Building Spread",
    linewidth = 2,
    legend = false
)
vline!([params.critical_separation], linestyle = :dash, label = "Critical separation")

WUI Simulation Example

Simulate a fire approaching a residential area:

ncols, nrows = 120, 120
cellsize = 30.0

# Create fire state
state = FireState{Float64}(ncols, nrows, cellsize)
fuel_table = create_standard_fuel_table(Float64)

# Fuel: grass with houses in upper portion
fuel_ids = fill(1, ncols, nrows)

# Terrain
slope = zeros(Float64, ncols, nrows)
aspect = zeros(Float64, ncols, nrows)

# Weather - wind blowing north
weather = ConstantWeather{Float64}(
    wind_speed_mph = 12.0,
    wind_direction = 180.0,  # From south
    M1 = 0.05, M10 = 0.07, M100 = 0.09,
    MLH = 0.50, MLW = 0.80
)

# Create WUI with buildings in northern portion
buildings = WUIBuilding{Float64}[]
id = 0
for ix in 30:15:90
    for iy in 70:15:100
        id += 1
        ctype = rand([:wood, :wood, :masonry])  # 2/3 wood, 1/3 masonry
        push!(buildings, WUIBuilding{Float64}(id, ix, iy; construction_type = ctype))
    end
end

wui_grid = WUIGrid{Float64}(buildings, ncols, nrows)
println("Created $(length(buildings)) buildings")

# Ignite in southern portion
ignite!(state, 60, 20, 0.0)

# Run simulation and track WUI state
weather_interp = create_constant_interpolator(weather, ncols, nrows, cellsize)
rng = MersenneTwister(42)

# Simple simulation loop with WUI updates
t = 0.0
dt = 1.0
t_end = 60.0

while t < t_end
    # Update WUI state
    ignitions = update_wui_state!(wui_grid, state, weather, t, dt, rng)

    if !isempty(ignitions)
        for ig in ignitions
            println("Building $(ig.building_id) ignited at t=$(round(t, digits=1)) via $(ig.ignition_source)")
        end
    end

    t += dt
end

# Also run fire simulation
state2 = FireState{Float64}(ncols, nrows, cellsize)
ignite!(state2, 60, 20, 0.0)
simulate_uniform!(state2, 1, fuel_table, weather, 0.0, 0.0, 0.0, t_end)

# Visualize
p1 = heatmap(state2.burned',
    title = "Fire Spread",
    color = :YlOrRd,
    aspect_ratio = 1
)

# Add building markers
building_x = [b.ix for b in buildings]
building_y = [b.iy for b in buildings]
colors = [wui_grid.ignited[i] ? :red : :blue for i in 1:length(buildings)]

scatter!(p1, building_x, building_y,
    color = colors,
    markersize = 6,
    markershape = :square,
    label = false
)

p1
Created 15 buildings

WUI Statistics

Get summary statistics from the simulation:

stats = get_wui_statistics(wui_grid)

println("WUI Simulation Results:")
println("  Total buildings: $(stats.total_buildings)")
println("  Ignited buildings: $(stats.ignited_buildings)")
println("  Ignition fraction: $(round(stats.ignition_fraction * 100, digits=1))%")
println("  Mean ignition time: $(round(stats.mean_ignition_time, digits=1)) min")
println("  First ignition: $(round(stats.first_ignition_time, digits=1)) min")
println("  Last ignition: $(round(stats.last_ignition_time, digits=1)) min")
println("  Wood ignited: $(stats.wood_ignited)")
println("  Masonry ignited: $(stats.masonry_ignited)")
WUI Simulation Results:
  Total buildings: 15
  Ignited buildings: 0
  Ignition fraction: 0.0%
  Mean ignition time: 0.0 min
  First ignition: -1.0 min
  Last ignition: -1.0 min
  Wood ignited: 0
  Masonry ignited: 0

View Factor Calculation

The view factor determines how much radiative heat a building receives:

flame_heights = [2, 5, 10, 20]  # meters
distances = 1:1:30  # meters

p = plot(xlabel = "Distance (m)", ylabel = "View Factor",
    title = "View Factor vs Distance", legend = :topright)

for H in flame_heights
    vfs = [compute_view_factor(Float64(H), Float64(d)) for d in distances]
    plot!(p, distances, vfs, label = "Flame = $H m", linewidth = 2)
end

p

Defense Zone Effectiveness

Simulate different defensible space configurations:

# Test different buffer distances
buffer_distances = [0, 30, 60, 100]  # feet
results = []

for buffer in buffer_distances
    # Create scenario where fire approaches building
    state = FireState{Float64}(80, 80, 30.0)

    # Create fuel map with buffer zone
    fuel_ids = fill(4, 80, 80)  # Chaparral
    if buffer > 0
        buffer_cells = div(buffer, 30)
        # Clear area around building at (40, 60)
        for ix in max(1, 40-buffer_cells):min(80, 40+buffer_cells)
            for iy in max(1, 60-buffer_cells):min(80, 60+buffer_cells)
                fuel_ids[ix, iy] = 1  # Grass (lower intensity)
            end
        end
    end

    ignite!(state, 40, 20, 0.0)
    simulate!(state, fuel_ids, fuel_table, weather, slope, aspect, 0.0, 45.0)

    # Check if fire reached building location
    reached = state.burned[40, 60]
    intensity = state.fireline_intensity[40, 60]

    push!(results, (buffer, reached, intensity))
end

for (buf, reached, intensity) in results
    status = reached ? "REACHED (I=$(round(intensity, digits=0)) kW/m)" : "Did not reach"
    println("Buffer $(buf)ft: $status")
end
Buffer 0ft: Did not reach
Buffer 30ft: Did not reach
Buffer 60ft: Did not reach
Buffer 100ft: Did not reach

Best Practices for WUI Modeling

  1. Building Data: Use actual building footprints and construction types when available
  2. Defensible Space: Model vegetation clearing around structures
  3. Ember Transport: Enable spotting for long-range ignitions
  4. Ensemble Approach: Run multiple scenarios to capture uncertainty
  5. Validation: Compare with post-fire damage assessments

Limitations

  • Simplified building geometry (point representation)
  • Uniform construction within building type
  • No interior fire dynamics
  • Limited suppression effects

For detailed structure loss modeling, consider coupling with dedicated WUI loss models.