Skip to content

Create QUIC-Fire simulation inputs

You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.

QUIC-Fire is a fast, 3-D fire–atmosphere model. To simulate fire spread through custom fuels and terrain it reads a small set of gridded binary input files:

FileQuantityUnits
treesrhof.datfuel bulk densitykg/m³
treesmoist.datfuel moisturefraction (mass water / mass dry fuel)
treesfueldepth.datsurface fuel-bed depthm
topo.datterrain elevationm
treesss.datfuel particle size (2 / SAVR)m

By the end of this tutorial you’ll have produced every one of those files for a real landscape — about one square kilometer of the Blue Mountain Recreation Area near Missoula, Montana — straight from national data sources, with roads masked out of the fuels.

The pipeline has a shape worth keeping in mind: you build a handful of grids (surface fuel, canopy fuel, terrain, moisture), each aligned to the same 2 m lattice, and then a single QUIC-Fire export stitches them together into the .dat bundle. Every grid is created with one POST and finishes asynchronously, so the rhythm of the whole tutorial is create → poll until completed → use the id in the next call.

  1. An API key. Create one in the FastFuels web app under your account settings. Set it once here and it propagates to every code block on the page: my-api-key.

  2. curl (or Python with requests) and unzip for the final download.

That’s it — you’ll create everything else, including the domain, as you go.

A domain is the georeferenced extent everything else hangs off. POST a GeoJSON FeatureCollection to /domains. pad_to_resolution: 2 snaps the domain’s bounding box out to a clean 2 m grid — the cell size QUIC-Fire will use — so every grid you build lands on the same lattice.

POST /domains
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-114.09545796676623, 46.8324794598619],
[-114.11217537297199, 46.8324794598619],
[-114.11217537297199, 46.82496749915157],
[-114.09545796676623, 46.82496749915157],
[-114.09545796676623, 46.8324794598619]
]
]
}
}
],
"name": "Blue Mountain Recreation Area",
"description": "Approximately 1 square kilometer near Missoula, Montana.",
"pad_to_resolution": 2
}'

The API reprojects your WGS84 polygon into the appropriate UTM zone (here EPSG:32611) and reports the padded bbox. Record the domain id — every call below hangs off it: your-domain-id.

Roads carry no fuel, so we’ll mask them out of the surface fuels and remove trees that fall on them. The reusable geometry for that is a feature — here, the road network pulled from OpenStreetMap. POST to /features/road/osm:

POST features/road/osm
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/features/road/osm' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "OSM roads"
}'

The 201 mints a feature id — record it: your-road-feature-id. Poll it to completed (the road extractor runs asynchronously):

GET feature status
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/features/your-road-feature-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

On this domain the extractor returns 36 road polygons (it widens OSM centerlines into footprints by road class). You can confirm the count at /features/{{ROAD_FEATURE_ID}}/data/metadata.

Surface fuels come from LANDFIRE in two moves: fetch the categorical FBFM40 fuel-model grid, then look up the continuous loads each fuel model implies. We fold the road mask into the lookup so road cells come out at zero fuel.

alignment pins it to the domain at 2 m — matching every other grid:

POST grids/fbfm40/landfire
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/fbfm40/landfire' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "FBFM40 fuel model (LANDFIRE 2024)",
"version": "2024",
"alignment": { "target": "domain", "resolution": 2, "method": "nearest" }
}'

Record the grid id: your-fbfm40-grid-id. Poll it to completed with the same grid-status call you’ll reuse throughout:

GET grid status
# Poll any grid by id — swap in whichever grid you're waiting on.
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-grid-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

Point the lookup at the completed FBFM grid. bands are the quantities the QUIC-Fire export needs — fuel_load.1hr, fuel_depth, and savr.1hr. The modifications block zeroes fuel load and depth in any cell a road touches:

POST grids/lookup/fbfm40
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/lookup/fbfm40' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Surface fuel loads (FBFM40 lookup, roads masked)",
"source_grid_id": "your-fbfm40-grid-id",
"source_band": "fbfm",
"bands": ["fuel_load.1hr", "fuel_depth", "savr.1hr"],
"modifications": [
{
"conditions": [
{"source": "feature", "operator": "intersects", "target": "cell", "feature_id": "your-road-feature-id"}
],
"actions": [
{"band": "fuel_load.1hr", "modifier": "replace", "value": 0},
{"band": "fuel_depth", "modifier": "replace", "value": 0}
]
}
]
}'

Record the surface grid id: your-surface-grid-id. When it completes, fuel_load.1hr runs from 0 up to about 1.30 kg/m² across the fuel models present — and 22,124 of the grid’s 289,068 cells sit at zero, the road-masked and naturally non-burnable cells.

The 3-D canopy is three chained builds: a TreeMap PIM grid (species and density), a tree inventory (individual stems, with roadside trees removed), and a voxel grid that turns those stems into per-cell bulk density, moisture, and SAVR.

POST grids/pim/treemap
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/pim/treemap' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "TreeMap PIM grid"
}'

The PIM grid summarizes, in each cell, the species composition and stem density imputed from the closest-matching FIA field plot — the statistical template the next step draws individual stems from. Record the id: your-pim-grid-id.

Generate the tree inventory, removing roadside trees

Section titled “Generate the tree inventory, removing roadside trees”

Realize individual stems from the PIM grid. seed makes the stochastic point process reproducible, and the modifications rule drops every tree within 5 m of a road:

POST inventories/tree/pim
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/tree/pim' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Tree inventory (trees near roads removed)",
"source_pim_grid_id": "your-pim-grid-id",
"seed": 42,
"modifications": [
{
"conditions": [
{"source": "feature", "operator": "within", "feature_id": "your-road-feature-id", "buffer_m": 5}
],
"actions": [
{"modifier": "remove"}
]
}
]
}'

Record the inventory id: your-inventory-id, and poll it to completed:

GET inventory status
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/inventories/your-inventory-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

With this seed the point process places 41,995 stems; removing those within 5 m of a road leaves 36,232 (5,763 trees dropped). The completed columns describe each surviving tree — x, y, fia_species_code, dbh, height, crown_ratio.

Turn the stems into a 3-D grid. resolution is 2 m horizontal and 1 m vertical (the fire grid’s cell size), and bands are exactly the canopy quantities the export needs. The biomass_source derives foliage bulk density from National-Scale Volume and Biomass (nsvb) allometric equations:

POST grids/voxelize/inventory/tree
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/voxelize/inventory/tree' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Canopy fuel voxels",
"source_inventory_id": "your-inventory-id",
"resolution": { "horizontal": 2, "vertical": 1 },
"bands": ["bulk_density.foliage.live", "fuel_moisture.live", "savr.foliage"],
"biomass_source": {
"type": "allometry",
"equations": "nsvb",
"components": ["foliage"],
"component_states": { "foliage": { "live": 1.0, "dead": 0.0 } }
},
"seed": 42
}'

Record the canopy grid id: your-tree-grid-id. Its shape is [37, 442, 654] — 37 vertical layers of 1 m over the 442 × 654 horizontal grid. That vertical extent becomes the fire grid’s height.

QUIC-Fire solves a near-surface wind field over the ground before and during the burn, and terrain shapes both that wind and the way fire moves across slopes. The simulation reads that ground as an elevation surface (topo.dat). Pull elevation from USGS 3DEP, aligned to the same 2 m lattice:

POST grids/topography/3dep
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/topography/3dep' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Topography (3DEP 10 m)",
"source_resolution": 10,
"bands": ["elevation"],
"alignment": { "target": "domain", "resolution": 2 }
}'

Record the id: your-topography-grid-id. Across this domain, sampled elevation runs from about 957 m to 1172 m — the ground surface the fire grid sits on.

Step 6 — Build the surface moisture grid

Section titled “Step 6 — Build the surface moisture grid”

QUIC-Fire reads a surface fuel-moisture field alongside the loads, as a fraction of dry-fuel mass. The simplest choice is a single uniform value — here, 6%:

POST grids/uniform
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/uniform' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Surface fuel moisture (uniform 6%)",
"resolution": 2,
"bands": [
{ "key": "fuel_moisture.1hr", "value": 6.0 }
]
}'

Record the id: your-moisture-grid-id. That’s the last grid — you now have surface fuel, canopy fuel, terrain, and moisture, all on the same 2 m lattice.

This is where the pieces come together. The export binds each physical quantity QUIC-Fire needs to a {grid_id, band} role, and writes the .dat bundle. Five roles are required (canopy bulk density and moisture; surface load, depth, and moisture); topography adds topo.dat, and the SAVR pair adds treesss.dat.

We leave alignment at its default: the domain-anchored fire grid at 2 m horizontal cells — the resolution QUIC-Fire recommends — and 1 m vertical layers, which every grid above already matches.

POST grids/exports/quicfire
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/exports/quicfire' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Blue Mountain QUIC-Fire inputs",
"canopy_bulk_density": {"grid_id": "your-tree-grid-id", "band": "bulk_density.foliage.live"},
"canopy_moisture": {"grid_id": "your-tree-grid-id", "band": "fuel_moisture.live"},
"canopy_savr": {"grid_id": "your-tree-grid-id", "band": "savr.foliage"},
"surface_fuel_load": {"grid_id": "your-surface-grid-id", "band": "fuel_load.1hr"},
"surface_fuel_depth": {"grid_id": "your-surface-grid-id", "band": "fuel_depth"},
"surface_moisture": {"grid_id": "your-moisture-grid-id", "band": "fuel_moisture.1hr"},
"surface_savr": {"grid_id": "your-surface-grid-id", "band": "savr.1hr"},
"topography": {"grid_id": "your-topography-grid-id", "band": "elevation"}
}'

The 201 echoes a resolved.fire_grid — the exact grid the bundle will be written on (654 × 442 × 37 at 2 m / 2 m / 1 m) — and mints an export id. Record it: your-export-id.

Unlike the grids, the export’s lifecycle lives at /exports/{id} (not under the domain). Poll it there:

GET export status
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/exports/your-export-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

When it lands on completed, the response carries a signed_url — a temporary download link (valid 7 days) for the zipped bundle.

Step 8 — Download and inspect the input files

Section titled “Step 8 — Download and inspect the input files”

Copy the signed_url and download it. The URL carries its own credentials, so no api-key header is needed:

Download and unzip
# When the export status is "completed", copy the `signed_url` from the
# response and download it (the URL already carries its own credentials —
# no api-key header needed). It expires after 7 days.
curl -L 'PASTE_SIGNED_URL_HERE' -o quicfire_inputs.zip
unzip quicfire_inputs.zip -d quicfire_inputs
ls quicfire_inputs
# treesrhof.dat treesmoist.dat treesfueldepth.dat topo.dat
# treesss.dat metadata.json domain.geojson

You now have a complete QUIC-Fire input set:

FileWhat it holds
treesrhof.dat3-D fuel bulk density (canopy + surface merged at the ground layer)
treesmoist.dat3-D fuel moisture
treesfueldepth.datsurface fuel-bed depth (ground layer)
topo.dat2-D terrain elevation
treesss.dat3-D fuel particle size (2 / SAVR)
metadata.jsonthe fire-grid dimensions and the export’s provenance
domain.geojsonthe domain footprint, for reference

The .dat files are Fortran-order binary arrays on the fire grid. Their dimensions are recorded in metadata.json:

{
"format": "quicfire",
"exporter_version": "1",
"completed_on": "2026-06-03T17:52:58.381540+00:00",
"fire_grid": {
"nx": 654,
"ny": 442,
"dx": 2.0,
"dy": 2.0,
"dz": 1.0,
"nz": 37,
"z_origin": 0.0,
"transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0],
"crs": "EPSG:32611"
},
"export_id": "your-export-id",
"export_name": "Blue Mountain QUIC-Fire inputs",
"source": {
"moist_merge": "max",
"rhof_merge": "sum",
"savr_merge": "weighted_avg",
"domain_id": "your-domain-id",
"resolved": {
"fire_grid": {
"nx": 654,
"ny": 442,
"dx": 2.0,
"dy": 2.0,
"dz": 1.0,
"nz": 37,
"z_origin": 0.0,
"transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0],
"crs": "EPSG:32611"
}
},
"surface_moisture": {
"band": "fuel_moisture.1hr",
"grid_id": "your-moisture-grid-id"
},
"surface_fuel_depth": {
"band": "fuel_depth",
"grid_id": "your-surface-grid-id"
},
"surface_fuel_load": {
"band": "fuel_load.1hr",
"grid_id": "your-surface-grid-id"
},
"name": "quicfire",
"topography": {
"band": "elevation",
"grid_id": "your-topography-grid-id"
},
"canopy_savr": {
"band": "savr.foliage",
"grid_id": "your-tree-grid-id"
},
"canopy_bulk_density": {
"band": "bulk_density.foliage.live",
"grid_id": "your-tree-grid-id"
},
"alignment": {
"dx": 2.0,
"dy": 2.0,
"dz": 1.0,
"target": "domain"
},
"surface_savr": {
"band": "savr.1hr",
"grid_id": "your-surface-grid-id"
},
"canopy_moisture": {
"band": "fuel_moisture.live",
"grid_id": "your-tree-grid-id"
}
}
}

To run the simulation, place these files in your QUIC-Fire case directory. QUIC-Fire reads the trees*.dat fuel arrays on the FastFuels fire grid, so it also needs a gridlist (the cell counts and sizes — nx, ny, nz, dx, dy, dz — straight from metadata.json’s fire_grid) and a rasterorigin.txt (the grid’s south-west UTM corner, derivable from the same transform) alongside them. Wire topo.dat in as a custom terrain file through QU_TopoInputs.inp. See the QUIC-Fire input-files documentation for the exact deck settings and flags for your version.

Everything above, start to finish — create the domain, mask roads, build the four grids, export, and download the .dat bundle. Drop in your API key and run it:

build_quicfire_inputs.py — the whole pipeline
"""Build a complete QUIC-Fire input set with the FastFuels v2 API.
Creates a domain, masks roads, builds surface + canopy fuels and terrain,
then exports the QUIC-Fire .dat files. Run top to bottom; it polls each
asynchronous build to completion before moving on.
"""
import time
import zipfile
import requests
API_KEY = "my-api-key"
BASE = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app"
HEADERS = {"api-key": API_KEY}
def poll(url: str) -> dict:
"""GET `url` every 5 s until the resource is completed or failed."""
while True:
r = requests.get(url, headers=HEADERS).json()
if r["status"] in ("completed", "failed"):
assert r["status"] == "completed", r
return r
time.sleep(5)
# 1. Domain — Blue Mountain Recreation Area, padded to a clean 2 m lattice.
domain = requests.post(
f"{BASE}/domains",
headers=HEADERS,
json={
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-114.09545796676623, 46.8324794598619],
[-114.11217537297199, 46.8324794598619],
[-114.11217537297199, 46.82496749915157],
[-114.09545796676623, 46.82496749915157],
[-114.09545796676623, 46.8324794598619],
]],
},
}],
"name": "Blue Mountain Recreation Area",
"pad_to_resolution": 2,
},
).json()
domain_id = domain["id"]
grids = f"{BASE}/domains/{domain_id}/grids"
invs = f"{BASE}/domains/{domain_id}/inventories"
# 2. Road feature from OpenStreetMap (reused as a mask below).
road = requests.post(
f"{BASE}/domains/{domain_id}/features/road/osm",
headers=HEADERS, json={"name": "OSM roads"},
).json()
poll(f"{BASE}/domains/{domain_id}/features/{road['id']}")
# 3. Surface fuels: FBFM40 fuel-model grid -> per-model load lookup, with road
# cells zeroed. Both grids are domain-anchored at 2 m so they share the
# fire-grid lattice the export needs.
fbfm = requests.post(
f"{grids}/fbfm40/landfire",
headers=HEADERS,
json={
"name": "FBFM40 fuel model (LANDFIRE 2024)",
"version": "2024",
"alignment": {"target": "domain", "resolution": 2, "method": "nearest"},
},
).json()
poll(f"{grids}/{fbfm['id']}")
surface = requests.post(
f"{grids}/lookup/fbfm40",
headers=HEADERS,
json={
"name": "Surface fuel loads (FBFM40 lookup, roads masked)",
"source_grid_id": fbfm["id"],
"source_band": "fbfm",
"bands": ["fuel_load.1hr", "fuel_depth", "savr.1hr"],
"modifications": [{
"conditions": [{"source": "feature", "operator": "intersects",
"target": "cell", "feature_id": road["id"]}],
"actions": [{"band": "fuel_load.1hr", "modifier": "replace", "value": 0},
{"band": "fuel_depth", "modifier": "replace", "value": 0}],
}],
},
).json()
# 4. Canopy fuels: TreeMap PIM grid -> tree inventory (trees near roads
# removed) -> 3-D voxel grid of bulk density / moisture / SAVR.
pim = requests.post(
f"{grids}/pim/treemap", headers=HEADERS, json={"name": "TreeMap PIM grid"},
).json()
poll(f"{grids}/{pim['id']}")
inventory = requests.post(
f"{invs}/tree/pim",
headers=HEADERS,
json={
"name": "Tree inventory (trees near roads removed)",
"source_pim_grid_id": pim["id"],
"seed": 42,
"modifications": [{
"conditions": [{"source": "feature", "operator": "within",
"feature_id": road["id"], "buffer_m": 5}],
"actions": [{"modifier": "remove"}],
}],
},
).json()
poll(f"{invs}/{inventory['id']}")
tree_grid = requests.post(
f"{grids}/voxelize/inventory/tree",
headers=HEADERS,
json={
"name": "Canopy fuel voxels",
"source_inventory_id": inventory["id"],
"resolution": {"horizontal": 2, "vertical": 1},
"bands": ["bulk_density.foliage.live", "fuel_moisture.live", "savr.foliage"],
"biomass_source": {
"type": "allometry", "equations": "nsvb", "components": ["foliage"],
"component_states": {"foliage": {"live": 1.0, "dead": 0.0}},
},
"seed": 42,
},
).json()
# 5. Terrain (3DEP elevation) and a uniform surface-moisture grid.
topography = requests.post(
f"{grids}/topography/3dep",
headers=HEADERS,
json={
"name": "Topography (3DEP 10 m)",
"source_resolution": 10,
"bands": ["elevation"],
"alignment": {"target": "domain", "resolution": 2},
},
).json()
moisture = requests.post(
f"{grids}/uniform",
headers=HEADERS,
json={
"name": "Surface fuel moisture (uniform 6%)",
"resolution": 2,
"bands": [{"key": "fuel_moisture.1hr", "value": 6.0}],
},
).json()
# Wait for the four remaining builds.
for grid in (surface, tree_grid, topography, moisture):
poll(f"{grids}/{grid['id']}")
# 6. Bundle everything into QUIC-Fire .dat files. Each physical quantity is a
# {grid_id, band} role; the fire grid defaults to 2 m horizontal cells
# (QUIC-Fire's recommended size) and 1 m vertical layers.
export = requests.post(
f"{grids}/exports/quicfire",
headers=HEADERS,
json={
"name": "Blue Mountain QUIC-Fire inputs",
"canopy_bulk_density": {"grid_id": tree_grid["id"], "band": "bulk_density.foliage.live"},
"canopy_moisture": {"grid_id": tree_grid["id"], "band": "fuel_moisture.live"},
"canopy_savr": {"grid_id": tree_grid["id"], "band": "savr.foliage"},
"surface_fuel_load": {"grid_id": surface["id"], "band": "fuel_load.1hr"},
"surface_fuel_depth": {"grid_id": surface["id"], "band": "fuel_depth"},
"surface_moisture": {"grid_id": moisture["id"], "band": "fuel_moisture.1hr"},
"surface_savr": {"grid_id": surface["id"], "band": "savr.1hr"},
"topography": {"grid_id": topography["id"], "band": "elevation"},
},
).json()
export = poll(f"{BASE}/exports/{export['id']}")
# 7. Download and unzip the QUIC-Fire input bundle.
zip_path = "quicfire_inputs.zip"
with requests.get(export["signed_url"], stream=True) as r:
r.raise_for_status()
with open(zip_path, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 20):
f.write(chunk)
with zipfile.ZipFile(zip_path) as z:
z.extractall("quicfire_inputs")
print(z.namelist())
# -> ['treesrhof.dat', 'treesmoist.dat', 'treesfueldepth.dat', 'topo.dat',
# 'treesss.dat', 'metadata.json', 'domain.geojson']