FUEGO COMMAND — LIVE SIMULATION ● STREAMING
wind 22kt · E t+45min
60fps

The sub-minute rerun

The architectural challenge was making re-runs fast enough that a responder changing a fire date doesn't wait on a federal API. So a rerun is isolated to weather and ignition (sim_slug), while terrain and fuel rasters (geo_slug) are downloaded once and reused across every scenario on that AOI — minutes down to under 15 seconds.

Ingest → Transform → Model → Vectorize → Stream

The step that matters is vectorization: burn grids leave the backend as GeoJSON, not rasters. Cell2Fire runs as a subprocess behind FastAPI so the heavy C++ model stays server-side, and the browser only ever receives geometry it can animate at 60fps.

01 · INGEST
USGS LANDFIRE API
async job, poll, ZIP → GeoTIFF
→
02 · TRANSFORM
GDAL / Rasterio
reproject EPSG:5070, export .asc
→
03 · MODEL
Cell2Fire (C++)
cell-grid sim, per-step burn CSVs
→
04 · VECTORIZE
rasterio + shapely
burn grid → GeoJSON series
→
05 · STREAM
FastAPI → Mapbox GL
JSON, 60fps animation

Architecture schematic

USGS Cloud
LANDFIRE multi-band GeoTIFF
⇄
FastAPI Server
pipeline · cache · Cell2Fire subprocess
⇄
Mapbox GL JS
React UI · burn animation

Keeping raster logic entirely server-side means the frontend can't break on a malformed .asc grid or a projection mismatch — the two failure modes that usually take a geospatial web app down. It also keeps payloads to kilobytes, which is why the map redraws in seconds rather than minutes.

340,000 cells → 8KB of GeoJSON

Cell2Fire writes dense CSV burn grids in EPSG:5070. Shipping that to a browser would kill frame rate — so every timestep gets unioned, simplified, and reprojected before it ever reaches Mapbox.

RASTER → GEOJSON · src/vectorizer.py
# Convert one Cell2Fire burn-grid timestep into a Mapbox-ready polygon
def vectorize_timestep(burn_grid, transform, src_crs):
    # Extract burned cells as raw polygons (rasterio.features)
    shapes = features.shapes(burn_grid, transform=transform)
    polygons = [shape(geom) for geom, val in shapes if val == 1]

    # Union + smooth to remove blocky grid-cell artifacts
    merged = unary_union(polygons).simplify(tolerance=1.5)

    # EPSG:5070 → WGS84, truncate to ~1m precision
    wgs84 = reproject(merged, src_crs, "EPSG:4326")
    return truncate_coords(wgs84, precision=5)

# ~340K raster cells per frame → single-digit KB of GeoJSON,
# keeping Mapbox GL JS smooth at 60fps.
EPSG:5070
NAD83 Albers, equal-area, 30m grid
5-min
weather interpolation step, w/ FFMC chaining
1-indexed
Ncell, w/ nearest-burnable-cell fallback

A model someone can hold during an actual event

Any CONUS location goes from a map click to a validated Cell2Fire run with no manual QGIS steps. Caching terrain and recomputing only weather ships kilobytes instead of megabytes — fast enough for a responder to run mid-event.