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:
| File | Quantity | Units |
|---|---|---|
treesrhof.dat | fuel bulk density | kg/m³ |
treesmoist.dat | fuel moisture | fraction (mass water / mass dry fuel) |
treesfueldepth.dat | surface fuel-bed depth | m |
topo.dat | terrain elevation | m |
treesss.dat | fuel 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.
Prerequisites
Section titled “Prerequisites”-
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.
-
curl(or Python withrequests) andunzipfor the final download.
That’s it — you’ll create everything else, including the domain, as you go.
Step 1 — Create a domain
Section titled “Step 1 — Create a domain”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.
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}'import requests
url = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains"
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "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,}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
domain_id = created["id"]{ "bbox": [720226.0, 5189762.0, 721534.0, 5190646.0], "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [720226.0, 5189762.0], [721534.0, 5189762.0], [721534.0, 5190646.0], [720226.0, 5190646.0], [720226.0, 5189762.0] ] ] }, "properties": { "name": "domain" } }, { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [721502.7544906491, 5190645.048516054], [720227.9398802927, 5190598.00908098], [720258.6480286171, 5189763.323999467], [721533.6406826023, 5189810.364218195], [721502.7544906491, 5190645.048516054] ] ] }, "properties": { "name": "input" }, "id": "0" } ], "name": "Blue Mountain Recreation Area", "description": "Approximately 1 square kilometer near Missoula, Montana.", "crs": { "type": "name", "properties": { "name": "EPSG:32611" } }, "tags": [], "pad_to_resolution": 2.0, "id": "your-domain-id", "created_on": "2026-06-03T17:52:24.791462", "modified_on": "2026-06-03T17:52:24.791462"}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.
Step 2 — Add a road feature
Section titled “Step 2 — Add a road feature”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:
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"}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/features/road/osm")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = {"name": "OSM roads"}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
road_feature_id = created["id"]{ "id": "your-road-feature-id", "domain_id": "your-domain-id", "type": "road", "name": "OSM roads", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:25.356514Z", "modified_on": "2026-06-03T17:52:26.185546Z", "source": { "product": "osm", "description": "OpenStreetMap road network", "extent_buffer_m": 0.0 }, "georeference": { "crs": "EPSG:32611", "bounds": [720226.0, 5189762.0, 721534.0, 5190646.0] }, "error": null, "tags": []}The 201 mints a feature id — record it: your-road-feature-id.
Poll it to completed (the road extractor runs asynchronously):
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'import timeimport requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/features/your-road-feature-id")
headers = { "accept": "application/json", "api-key": "my-api-key",}
while True: feature = requests.get(url, headers=headers).json() status = feature["status"] print(status) if status in ("completed", "failed"): break time.sleep(5)
if feature["status"] == "failed": raise RuntimeError(feature["error"])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.
Step 3 — Build the surface fuel grid
Section titled “Step 3 — Build the surface fuel grid”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.
Fetch the FBFM40 grid
Section titled “Fetch the FBFM40 grid”alignment pins it to the domain at 2 m — matching every other grid:
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" }}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/fbfm40/landfire")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "name": "FBFM40 fuel model (LANDFIRE 2024)", "version": "2024", "alignment": {"target": "domain", "resolution": 2, "method": "nearest"},}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
fbfm_grid_id = created["id"]{ "id": "your-fbfm40-grid-id", "domain_id": "your-domain-id", "name": "FBFM40 fuel model (LANDFIRE 2024)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:25.988852Z", "modified_on": "2026-06-03T17:52:28.636590Z", "source": { "remove_non_burnable": null, "name": "landfire", "description": "LANDFIRE FBFM40 fuel model codes (Scott-Burgan 40 classification)", "version": "2024", "alignment": { "target": "domain", "resolution": 2.0, "method": "nearest" }, "product": "fbfm40", "extent_buffer_cells": 0 }, "modifications": [], "bands": [ { "key": "fbfm", "type": "categorical", "unit": null, "index": 0, "nodata": 32767 } ], "georeference": { "crs": "EPSG:32611", "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0], "shape": [442, 654] }, "error": null, "chunks": { "shape": [512, 512], "count": 2, "count_by_axis": { "x": 2, "y": 1 } }, "tags": []}Record the grid id: your-fbfm40-grid-id. Poll it to completed with
the same grid-status call you’ll reuse throughout:
# 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'# Poll any grid by id — swap in whichever grid you're waiting on.import timeimport requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/your-grid-id")
headers = { "accept": "application/json", "api-key": "my-api-key",}
while True: grid = requests.get(url, headers=headers).json() status = grid["status"] print(status) if status in ("completed", "failed"): break time.sleep(5)
if grid["status"] == "failed": raise RuntimeError(grid["error"])Look up surface loads, masking roads
Section titled “Look up surface loads, masking roads”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:
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} ] } ]}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/lookup/fbfm40")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "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}, ], } ],}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
surface_grid_id = created["id"]{ "id": "your-surface-grid-id", "domain_id": "your-domain-id", "name": "Surface fuel loads (FBFM40 lookup, roads masked)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:29.452367Z", "modified_on": "2026-06-03T17:52:33.022473Z", "source": { "source_grid_id": "your-fbfm40-grid-id", "name": "lookup", "source_band": "fbfm", "table": "fbfm40" }, "modifications": [ { "conditions": [ { "source": "feature", "operator": "intersects", "feature_id": "your-road-feature-id", "buffer_m": null, "target": "cell" } ], "actions": [ { "band": "fuel_load.1hr", "modifier": "replace", "value": 0 }, { "band": "fuel_depth", "modifier": "replace", "value": 0 } ] } ], "bands": [ { "key": "fuel_load.1hr", "type": "continuous", "unit": "kg/m**2", "index": 0, "nodata": null }, { "key": "fuel_depth", "type": "continuous", "unit": "m", "index": 1, "nodata": null }, { "key": "savr.1hr", "type": "continuous", "unit": "1/m", "index": 2, "nodata": null } ], "georeference": { "crs": "EPSG:32611", "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0], "shape": [442, 654] }, "error": null, "chunks": { "shape": [512, 512], "count": 2, "count_by_axis": { "x": 2, "y": 1 } }, "tags": []}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.
Step 4 — Build the canopy fuel grid
Section titled “Step 4 — Build the canopy fuel grid”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.
Build the TreeMap PIM grid
Section titled “Build the TreeMap PIM grid”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"}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/pim/treemap")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = {"name": "TreeMap PIM grid"}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
pim_grid_id = created["id"]{ "id": "your-pim-grid-id", "domain_id": "your-domain-id", "name": "TreeMap PIM grid", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:26.929403Z", "modified_on": "2026-06-03T17:52:29.412889Z", "source": { "bands": ["tm_id"], "name": "pim", "description": "TreeMap plot imputation raster (FIA plot IDs at 30m)", "version": "2022", "alignment": { "target": "domain", "resolution": null, "method": null }, "product": "treemap", "extent_buffer_cells": 0 }, "modifications": [], "bands": [ { "key": "tm_id", "type": "categorical", "unit": null, "index": 0, "nodata": 4294967295 } ], "georeference": { "crs": "EPSG:32611", "transform": [ 29.49945488757609, 0.0, 720226.0, 0.0, -29.499454887583852, 5190646.9836466275 ], "shape": [30, 45] }, "error": null, "chunks": { "shape": [512, 512], "count": 1, "count_by_axis": { "x": 1, "y": 1 } }, "tags": []}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:
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"} ] } ]}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/inventories/tree/pim")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "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"}], } ],}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
inventory_id = created["id"]{ "id": "your-inventory-id", "domain_id": "your-domain-id", "type": "tree", "name": "Tree inventory (trees near roads removed)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:30.248243Z", "modified_on": "2026-06-03T17:52:34.177619Z", "source": { "name": "pim", "seed": 42, "source_pim_grid_id": "your-pim-grid-id", "point_process": "inhomogeneous_poisson" }, "modifications": [ { "conditions": [ { "source": "feature", "operator": "within", "feature_id": "your-road-feature-id", "buffer_m": 5.0 } ], "actions": [ { "modifier": "remove" } ] } ], "columns": [ { "key": "x", "type": "continuous", "unit": "m" }, { "key": "y", "type": "continuous", "unit": "m" }, { "key": "fia_species_code", "type": "categorical", "unit": null }, { "key": "fia_status_code", "type": "categorical", "unit": null }, { "key": "dbh", "type": "continuous", "unit": "cm" }, { "key": "height", "type": "continuous", "unit": "m" }, { "key": "crown_ratio", "type": "continuous", "unit": null } ], "georeference": { "crs": "EPSG:32611", "bounds": [720226.0, 5189762.0, 721534.0, 5190646.0] }, "error": null, "tags": []}Record the inventory id: your-inventory-id, and poll it to
completed:
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'import timeimport requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/inventories/your-inventory-id")
headers = { "accept": "application/json", "api-key": "my-api-key",}
while True: inventory = requests.get(url, headers=headers).json() status = inventory["status"] print(status) if status in ("completed", "failed"): break time.sleep(5)
if inventory["status"] == "failed": raise RuntimeError(inventory["error"])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.
Voxelize the canopy
Section titled “Voxelize the canopy”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:
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}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/voxelize/inventory/tree")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "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,}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
tree_grid_id = created["id"]{ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel voxels", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:36.970140Z", "modified_on": "2026-06-03T17:52:50.616504Z", "source": { "moisture_model": { "live": { "method": "uniform", "value": 100.0 } }, "seed": 42, "resolution": { "horizontal": 2.0, "vertical": 1.0 }, "crown_profile_model": "purves", "entity": "tree", "bands": [ "bulk_density.foliage.live", "fuel_moisture.live", "savr.foliage" ], "source_inventory_id": "your-inventory-id", "biomass_source": { "type": "allometry", "components": ["foliage"], "equations": "nsvb", "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } } }, "max_crown_radius_source": { "type": "allometry" }, "input": "inventory", "operation": "voxelize" }, "modifications": [], "bands": [ { "key": "bulk_density.foliage.live", "type": "continuous", "unit": "kg/m**3", "index": 0, "nodata": null }, { "key": "fuel_moisture.live", "type": "continuous", "unit": "%", "index": 1, "nodata": null }, { "key": "savr.foliage", "type": "continuous", "unit": "1/m", "index": 2, "nodata": null } ], "georeference": { "crs": "EPSG:32611", "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0], "shape": [37, 442, 654], "z_resolution": 1.0, "z_origin": 0.0 }, "error": null, "chunks": { "shape": [37, 442, 442], "count": 2, "count_by_axis": { "z": 1, "x": 2, "y": 1 } }, "tags": []}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.
Step 5 — Build the topography grid
Section titled “Step 5 — Build the topography grid”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:
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 }}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/topography/3dep")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "name": "Topography (3DEP 10 m)", "source_resolution": 10, "bands": ["elevation"], "alignment": {"target": "domain", "resolution": 2},}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
topo_grid_id = created["id"]{ "id": "your-topography-grid-id", "domain_id": "your-domain-id", "name": "Topography (3DEP 10 m)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:27.489822Z", "modified_on": "2026-06-03T17:52:30.471520Z", "source": { "product": "topography", "tile_metadata": { "native_crs": "EPSG:4326", "tiles": [ "https://prd-tnm.s3.amazonaws.com/StagedProducts/Elevation/13/TIFF/current/n47w115/USGS_13_n47w115.tif" ], "acquisition_dates": null, "tile_source": null, "tile_count": 1 }, "name": "3dep", "description": "3DEP topographic data (elevation, slope, aspect)", "source_resolution": 10, "alignment": { "target": "domain", "resolution": 2.0, "method": null }, "bands": ["elevation"], "extent_buffer_cells": 0 }, "modifications": [], "bands": [ { "key": "elevation", "type": "continuous", "unit": "m", "index": 0, "nodata": -999999.0 } ], "georeference": { "crs": "EPSG:32611", "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0], "shape": [442, 654] }, "error": null, "chunks": { "shape": [512, 512], "count": 2, "count_by_axis": { "x": 2, "y": 1 } }, "tags": []}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%:
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 } ]}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/uniform")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "name": "Surface fuel moisture (uniform 6%)", "resolution": 2, "bands": [{"key": "fuel_moisture.1hr", "value": 6.0}],}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
moisture_grid_id = created["id"]{ "id": "your-moisture-grid-id", "domain_id": "your-domain-id", "name": "Surface fuel moisture (uniform 6%)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:28.073960Z", "modified_on": "2026-06-03T17:52:30.691809Z", "source": { "bands": [ { "key": "fuel_moisture.1hr", "value": 6.0 } ], "name": "uniform", "resolution": 2.0 }, "modifications": [], "bands": [ { "key": "fuel_moisture.1hr", "type": "continuous", "unit": "%", "index": 0, "nodata": null } ], "georeference": { "crs": "EPSG:32611", "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0], "shape": [442, 654] }, "error": null, "chunks": { "shape": [512, 512], "count": 2, "count_by_axis": { "x": 2, "y": 1 } }, "tags": []}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.
Step 7 — Create the QUIC-Fire export
Section titled “Step 7 — Create the QUIC-Fire export”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.
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"}}'import requests
url = ( "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app" "/domains/your-domain-id/grids/exports/quicfire")
headers = { "accept": "application/json", "api-key": "my-api-key", "Content-Type": "application/json",}
data = { "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"},}
response = requests.post(url, headers=headers, json=data)response.raise_for_status()created = response.json()
export_id = created["id"]{ "id": "your-export-id", "domain_id": "your-domain-id", "name": "Blue Mountain QUIC-Fire inputs", "description": "", "status": "pending", "progress": null, "created_on": "2026-06-03T17:52:56.528316", "modified_on": "2026-06-03T17:52:56.528316", "source": { "name": "quicfire", "domain_id": "your-domain-id", "alignment": { "target": "domain", "dx": 2.0, "dy": 2.0, "dz": 1.0 }, "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" }, "rhof_merge": "sum", "moist_merge": "max", "savr_merge": "weighted_avg", "resolved": { "fire_grid": { "nx": 654, "ny": 442, "nz": 37, "dx": 2.0, "dy": 2.0, "dz": 1.0, "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0], "z_origin": 0.0, "crs": "EPSG:32611" } } }, "signed_url": null, "expires_on": "2026-06-10T17:52:56.528316", "error": null, "tags": []}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:
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'import timeimport requests
url = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/exports/your-export-id"
headers = { "accept": "application/json", "api-key": "my-api-key",}
while True: export = requests.get(url, headers=headers).json() status = export["status"] print(status) if status in ("completed", "failed"): break time.sleep(5)
if export["status"] == "failed": raise RuntimeError(export["error"])
signed_url = export["signed_url"]{ "id": "your-export-id", "domain_id": "your-domain-id", "name": "Blue Mountain QUIC-Fire inputs", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-06-03T17:52:56.528316Z", "modified_on": "2026-06-03T17:53:01.399170Z", "source": { "surface_moisture": { "band": "fuel_moisture.1hr", "grid_id": "your-moisture-grid-id" }, "surface_savr": { "band": "savr.1hr", "grid_id": "your-surface-grid-id" }, "moist_merge": "max", "canopy_savr": { "band": "savr.foliage", "grid_id": "your-tree-grid-id" }, "surface_fuel_depth": { "band": "fuel_depth", "grid_id": "your-surface-grid-id" }, "rhof_merge": "sum", "domain_id": "your-domain-id", "canopy_moisture": { "band": "fuel_moisture.live", "grid_id": "your-tree-grid-id" }, "surface_fuel_load": { "band": "fuel_load.1hr", "grid_id": "your-surface-grid-id" }, "canopy_bulk_density": { "band": "bulk_density.foliage.live", "grid_id": "your-tree-grid-id" }, "name": "quicfire", "savr_merge": "weighted_avg", "alignment": { "target": "domain", "dz": 1.0, "dx": 2.0, "dy": 2.0 }, "topography": { "band": "elevation", "grid_id": "your-topography-grid-id" }, "resolved": { "fire_grid": { "ny": 442, "dx": 2.0, "crs": "EPSG:32611", "z_origin": 0.0, "dy": 2.0, "nz": 37, "nx": 654, "dz": 1.0, "transform": [2.0, 0.0, 720226.0, 0.0, -2.0, 5190646.0] } } }, "signed_url": "https://storage.googleapis.com/silvx-fastfuels-exports-v2/your-export-id/Blue_Mountain_QUIC-Fire_inputs.zip?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Expires=604800&X-Goog-SignedHeaders=host&X-Goog-Signature=...", "expires_on": "2026-06-10T17:52:56.528316Z", "error": null, "tags": []}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:
# 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_inputsls quicfire_inputs# treesrhof.dat treesmoist.dat treesfueldepth.dat topo.dat# treesss.dat metadata.json domain.geojson# When the export status is "completed", grab 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.import zipfile
import requests
signed_url = "PASTE_SIGNED_URL_HERE"
with requests.get(signed_url, stream=True) as response: response.raise_for_status() with open("quicfire_inputs.zip", "wb") as f: for chunk in response.iter_content(chunk_size=1 << 20): f.write(chunk)
with zipfile.ZipFile("quicfire_inputs.zip") as archive: archive.extractall("quicfire_inputs") print(archive.namelist())# ['treesrhof.dat', 'treesmoist.dat', 'treesfueldepth.dat', 'topo.dat',# 'treesss.dat', 'metadata.json', 'domain.geojson']You now have a complete QUIC-Fire input set:
| File | What it holds |
|---|---|
treesrhof.dat | 3-D fuel bulk density (canopy + surface merged at the ground layer) |
treesmoist.dat | 3-D fuel moisture |
treesfueldepth.dat | surface fuel-bed depth (ground layer) |
topo.dat | 2-D terrain elevation |
treesss.dat | 3-D fuel particle size (2 / SAVR) |
metadata.json | the fire-grid dimensions and the export’s provenance |
domain.geojson | the 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.
The whole pipeline in one script
Section titled “The whole pipeline in one script”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 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 eachasynchronous build to completion before moving on."""
import timeimport 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']Next steps
Section titled “Next steps”- Tune the fuelscape. Thin fuel along road shoulders instead of zeroing it, or remove only large trees near roads — see Mask a fuel grid with features and Remove trees with features.
- Swap a data source. Build the tree inventory from a canopy height model or from your own tree list instead of TreeMap.
- Inspect a grid before exporting — fetch and stream the grid data to check values cell by cell.
- Change the fire grid. Pass an explicit
alignmentto the export for a coarser or finer simulation (e.g.dx: 4ordz: 0.5); every role grid must still match the resulting lattice.