grid = LevelSetGrid(100, 100, dx=30.0)LevelSetGrid{Float64} 100×100 (t=0.0, burned=0/10000)
The LevelSet module implements a fire spread simulator using the level set method. A signed distance function φ(x, y, t) implicitly tracks the fire front:
| Region | Condition | Meaning |
|---|---|---|
| Burned | φ < 0 |
Fire has passed |
| Front | φ = 0 |
Active fire perimeter |
| Unburned | φ > 0 |
Fuel ahead of fire |
The front evolves according to the Hamilton-Jacobi equation:
\[\frac{\partial \phi}{\partial t} + F |\nabla \phi| = 0\]
where \(F(x, y, t) \geq 0\) is the local spread rate (e.g. from the Rothermel model).
LevelSetGridThe bc keyword controls how the solver handles grid edges. Three built-in options are available:
| Type | Behavior | Physical Interpretation |
|---|---|---|
ZeroNeumann() |
Zero-gradient at edges (default). Missing neighbors contribute zero to finite differences. | Open boundaries — the fire front can reach and exit the domain naturally. Most common choice for realistic simulations where the domain is a subset of a larger landscape. |
Dirichlet() |
Fixed-value edges. Edge cells are never updated by advance! or reinitialize!. |
Firebreak / barrier — edge cells retain their initial values (unburned). Useful for modeling impassable boundaries like rivers, roads, or cleared fire lines. |
Periodic() |
Wrap-around. Edges reference values from the opposite side of the grid. | Tiling / infinite landscape — fire exiting one edge enters from the opposite side. Useful for studying fire behavior in a homogeneous landscape without edge effects. |
Dirichlet: LevelSetGrid{Float64}(50×50)
Periodic: LevelSetGrid{Float64}(50×50)
Define a custom BC by subtyping AbstractBoundaryCondition and implementing the four one-sided finite-difference helpers. Optionally implement _skip_update to exclude certain cells from updates.
using Wildfires.LevelSet
struct MyBC <: AbstractBoundaryCondition end
# One-sided finite differences at boundaries:
# _Dxm(φ, i, j, dx, bc) → backward x-difference
# _Dxp(φ, i, j, nx, dx, bc) → forward x-difference
# _Dym(φ, i, j, dy, bc) → backward y-difference
# _Dyp(φ, i, j, ny, dy, bc) → forward y-difference
# Optional: skip updating certain cells (default: false)
# _skip_update(i, j, ny, nx, bc) → BoolSee the built-in implementations in src/LevelSet.jl for reference.
The spread rate field F has the same dimensions as the grid. Each cell specifies the local spread rate in m/min.
The advance! function accepts an optional solver argument that controls the numerical scheme for evolving the level set equation. Three solvers are available:
| Solver | Order | Time Stepping | Stencil | Use Case |
|---|---|---|---|---|
Godunov() |
1st | Forward Euler | 1-cell | Default — robust, simple, good for most cases |
Superbee() |
2nd | RK2 (Heun) | 2-cell | Sharper fronts, less numerical diffusion |
WENO5() |
5th | SSP-RK3 | 3-cell | Highest accuracy, sharpest front resolution |
LevelSetGrid{Float64} 100×100 (t=0.5, burned=52/10000, ignited=52)
When no solver is specified, advance!(grid, F, dt) defaults to Godunov() for backward compatibility.
Godunov() — First-Order UpwindThe default solver computes \(|\nabla\phi|\) using Godunov’s upwind scheme, a monotone numerical flux for Hamilton-Jacobi equations. The idea is to use one-sided (upwind) finite differences that respect the direction information is traveling — toward the fire front.
At each cell, four one-sided differences are computed:
\[D_x^- = \frac{\phi_{i,j} - \phi_{i,j-1}}{\Delta x}, \quad D_x^+ = \frac{\phi_{i,j+1} - \phi_{i,j}}{\Delta x}\]
\[D_y^- = \frac{\phi_{i,j} - \phi_{i-1,j}}{\Delta y}, \quad D_y^+ = \frac{\phi_{i+1,j} - \phi_{i,j}}{\Delta y}\]
Since the spread rate \(F \geq 0\), the front moves in the direction of \(\nabla\phi\) (outward from burned regions). Godunov’s scheme selects the appropriate one-sided difference in each coordinate:
\[\hat{D}_x = \max\!\bigl(\max(D_x^-,\, 0),\; -\min(D_x^+,\, 0)\bigr)\]
\[\hat{D}_y = \max\!\bigl(\max(D_y^-,\, 0),\; -\min(D_y^+,\, 0)\bigr)\]
Then \(|\nabla\phi| \approx \sqrt{\hat{D}_x^2 + \hat{D}_y^2}\) and the update is:
\[\phi^{n+1}_{i,j} = \phi^n_{i,j} - \Delta t \; F_{i,j} \; \sqrt{\hat{D}_x^2 + \hat{D}_y^2}\]
This selection rule ensures that only upstream information influences the update, preventing oscillations that would occur with central differences. At domain boundaries, the one-sided differences that would require out-of-bounds data are set to zero (implicit zero-Neumann / reflective boundary condition).
Superbee() — Second-Order TVDThe Superbee solver converts the Hamilton-Jacobi equation into advection form and applies a Superbee flux limiter with RK2 (Heun’s method) time stepping, following the approach used in ELMFIRE. This produces sharper fire fronts with less numerical diffusion than the first-order Godunov scheme.
How it works:
The Superbee limiter is total variation diminishing (TVD), preventing spurious oscillations while maintaining second-order accuracy at smooth regions. It uses a 2-cell stencil (accessing φ[i±2, j±2]), wider than Godunov’s 1-cell stencil.
Parameters:
| Parameter | Default | Description |
|---|---|---|
phi_clamp |
100.0 |
Clamp φ values to [-phi_clamp, phi_clamp] after each RK stage |
grad_clamp |
1000.0 |
Clamp flux-limited gradients to prevent runaway values |
These guards prevent numerical blowup when the level set field develops steep gradients or large values far from the front.
WENO5() — Fifth-Order WENOThe WENO5 solver uses the Jiang-Shu WENO5 (Weighted Essentially Non-Oscillatory) reconstruction for spatial derivatives with SSP-RK3 (Strong Stability Preserving Runge-Kutta) time stepping. This provides 5th-order accuracy in smooth regions while maintaining sharp resolution near discontinuities.
How it works:
The 3-cell stencil (accessing φ[i±3, j±3]) is the widest of the three solvers.
Parameters:
| Parameter | Default | Description |
|---|---|---|
phi_clamp |
100.0 |
Clamp φ values to [-phi_clamp, phi_clamp] after each RK stage |
using Wildfires.SpreadModel
using Wildfires.Rothermel: SHORT_GRASS, FuelClasses
M = FuelClasses(d1=0.06, d10=0.07, d100=0.08, herb=0.0, wood=0.0)
model = FireSpreadModel(SHORT_GRASS, UniformWind(speed=8.0), UniformMoisture(M), FlatTerrain())
fig = Figure(size=(900, 300))
for (col, (label, solver)) in enumerate([
("Godunov (1st order)", Godunov()),
("Superbee (2nd order TVD)", Superbee()),
("WENO5 (5th order)", WENO5()),
])
g = LevelSetGrid(200, 200, dx=30.0)
ignite!(g, 3000.0, 3000.0, 50.0)
simulate!(g, model, steps=100, dt=0.5, solver=solver)
ax = Axis(fig[1, col], title=label, aspect=DataAspect())
fireplot!(ax, g)
hidedecorations!(ax)
end
figGodunov() is the safe default — it’s simple, robust, and sufficient for most simulations.Superbee() is worth trying when you need sharper fire front resolution, especially for high-resolution grids or when comparing against observed fire perimeters. It requires roughly 2x the computation per time step (two RK stages + wider stencil).WENO5() provides the highest accuracy and sharpest fronts, but at roughly 3x the cost of Godunov (three RK stages + wider stencil). Best for high-resolution research simulations.The time step dt must satisfy the Courant-Friedrichs-Lewy (CFL) condition for numerical stability:
\[\text{dt} \leq \text{cfl} \cdot \frac{\min(\text{dx}, \text{dy})}{\max(F)}\]
where cfl is a safety coefficient (default 0.5). If dt is too large, the upwind scheme becomes unstable and φ values will diverge.
The cfl_dt function computes a stable time step:
The simulate! function uses automatic CFL-limited time stepping by default. Pass an explicit dt to override:
Over many time steps, φ can drift away from a true signed distance function (gradients become too flat or too steep). Periodic reinitialization restores |∇φ| ≈ 1, which keeps the upwind scheme accurate.
Two reinitialization methods are available:
| Method | Accuracy | Cost | Use Case |
|---|---|---|---|
IterativeReinit(iterations=5) |
Approximate | Low | Default — fast, good enough for most simulations |
NewtonReinit(upsample=8) |
Machine-precision | Higher | Research — exact signed distance restoration |
NewtonReinit — Newton Closest-PointThe Newton method produces machine-precision signed distance fields by:
φ using Interpolations.jlIn simulate!, pass the reinit keyword:
The curvature keyword in simulate! adds curvature-dependent speed regularization:
\[F_{\text{eff}} = \max(F - b \cdot \kappa, 0)\]
where \(b\) is the curvature coefficient and \(\kappa\) is the mean curvature of the level set. This smooths the fire front by reducing speed at convex bulges and increasing it at concave indentations.
The mean curvature is computed via central differences:
\[\kappa = \frac{\phi_{xx}\phi_y^2 - 2\phi_x\phi_y\phi_{xy} + \phi_{yy}\phi_x^2}{(\phi_x^2 + \phi_y^2)^{3/2}}\]
When curvature > 0, the CFL time step also enforces the parabolic constraint \(\Delta t \leq \Delta x^2 / (2b)\). For typical fire simulation parameters, this is never the bottleneck.
The LevelSetGrid integrates with Makie’s heatmap, contour, contourf, and surface via convert_arguments:
fireplotThe fireplot recipe combines a heatmap of φ with a contour line at the fire front:
Simulate fire spread with a spatially varying spread rate and visualize snapshots:
# Fresh grid with ignition
g = LevelSetGrid(200, 200, dx=15.0)
ignite!(g, 1500.0, 1500.0, 50.0)
# Spread rate: faster on the right side
F = [5.0 + 10.0 * (j / 200)
for i in 1:200, j in 1:200]
fig = Figure(size=(700, 250))
times = [0, 50, 100]
for (col, target_t) in enumerate(times)
while g.t < target_t
advance!(g, F, 0.25)
end
col > 1 && reinitialize!(g)
t = round(g.t, digits=1)
ax = Axis(fig[1, col],
title="t = $t min",
aspect=DataAspect())
fireplot!(ax, g)
hidedecorations!(ax)
end
fig