Build surface fuels from a tree grid with DUET
You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.
DUET turns the canopy in a 3D tree grid into a 2D surface-fuel pattern. The model introduced by McDanold et al. (2023) starts with a voxelated canopy, uses species traits and wind-informed elliptical fall trajectories to deposit leaf and needle litter, and predicts grass from canopy shade and the accumulated, decaying litter cover. This guide builds the required tree grid, inspects DUET’s output, and demonstrates the API’s optional calibration methods.
Prerequisites
Section titled “Prerequisites”-
An API key: my-api-key.
-
A domain: your-domain-id.
-
A completed tree inventory with
dbh,crown_ratio, andfia_species_codepopulated on every tree: your-inventory-id. A TreeMap inventory is ready to use. Complete a thin CHM or upload inventory with GDAM allometry first. -
If you plan to calibrate, target values you can defend for this landscape and use case — field measurements, a suitable published dataset, or explicit scenario assumptions. The numbers below are examples, not defaults.
Step 1 — Build a DUET-compatible tree grid
Section titled “Step 1 — Build a DUET-compatible tree grid”DUET requires a completed 3D tree grid carrying exactly these inputs:
bulk_density.foliage.live— foliage mass distributed through each crown;spcd— the FIA species code controlling litter properties; andfuel_moisture.live— live canopy moisture.
Request all three in one voxelization. spcd is not part of the default
voxelization, so a canopy grid built for another purpose may not be usable as-is.
curl -X 'POST' \ 'https://api-v2-prod-782971006568.us-west1.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": "DUET-compatible canopy grid", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "bands": [ "bulk_density.foliage.live", "spcd", "fuel_moisture.live" ], "seed": 42}'import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
# A completed inventory carrying dbh, crown_ratio, and fia_species_code.inventory = ff.get_inventory("your-domain-id", "your-inventory-id")
# Voxelization is a method on the Inventory. spcd is required for DUET and is# not part of the default voxelization.tree_grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "spcd", "fuel_moisture.live"], seed=42, name="DUET-compatible canopy grid",){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "DUET-compatible canopy grid", "description": "", "status": "pending", "progress": null, "created_on": "2026-08-24T18:24:45.020345", "modified_on": "2026-08-24T18:24:45.020345", "checksum": "5ecd7048b0674f76ba66ee0c129e8702", "source": { "operation": "voxelize", "input": "inventory", "entity": "tree", "source_inventory_id": "your-inventory-id", "source_inventory_checksum": "157b99ea40134a2a8891d5e013cd7949", "resolution": { "horizontal": 2.0, "vertical": 1.0 }, "bands": ["bulk_density.foliage.live", "spcd", "fuel_moisture.live"], "crown_profile_model": "purves", "biomass_source": { "type": "allometry", "equations": "nsvb", "components": ["foliage"], "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } } }, "max_crown_radius_source": { "type": "allometry" }, "moisture_model": { "live": { "method": "uniform", "value": 100.0 } }, "seed": 42 }, "modifications": [], "bands": [ { "key": "bulk_density.foliage.live", "name": "Live Foliage Bulk Density", "description": "Per-voxel bulk density of live foliage.", "type": "continuous", "unit": "kg/m**3", "index": 0, "nodata": null, "summary": null }, { "key": "spcd", "name": "Species Code", "description": "FIA species code (SPCD) of the dominant tree in the voxel.", "type": "categorical", "unit": null, "index": 1, "nodata": null, "summary": null }, { "key": "fuel_moisture.live", "name": "Live Fuel Moisture", "description": "Moisture content of live canopy fuels (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 2, "nodata": null, "summary": null } ], "georeference": null, "error": null, "chunks": null, "tags": []}Record the id as your-tree-grid-id and poll it to completed:
curl -X 'GET' \ 'https://api-v2-prod-782971006568.us-west1.run.app/domains/your-domain-id/grids/your-tree-grid-id' \ -H 'accept: application/json' \ -H 'api-key: my-api-key'tree_grid.wait(){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "DUET-compatible canopy grid", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-08-24T18:24:45.020345Z", "modified_on": "2026-08-24T18:24:58.076692Z", "checksum": "5ecd7048b0674f76ba66ee0c129e8702", "source": { "moisture_model": { "live": { "value": 100.0, "method": "uniform" } }, "resolution": { "vertical": 1.0, "horizontal": 2.0 }, "source_inventory_checksum": "157b99ea40134a2a8891d5e013cd7949", "biomass_source": { "type": "allometry", "equations": "nsvb", "components": ["foliage"], "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } } }, "crown_profile_model": "purves", "bands": ["bulk_density.foliage.live", "spcd", "fuel_moisture.live"], "max_crown_radius_source": { "type": "allometry" }, "entity": "tree", "input": "inventory", "seed": 42, "operation": "voxelize", "source_inventory_id": "your-inventory-id" }, "modifications": [], "bands": [ { "key": "bulk_density.foliage.live", "name": "Live Foliage Bulk Density", "description": "Per-voxel bulk density of live foliage.", "type": "continuous", "unit": "kg/m**3", "index": 0, "nodata": null, "summary": null }, { "key": "spcd", "name": "Species Code", "description": "FIA species code (SPCD) of the dominant tree in the voxel.", "type": "categorical", "unit": null, "index": 1, "nodata": null, "summary": null }, { "key": "fuel_moisture.live", "name": "Live Fuel Moisture", "description": "Moisture content of live canopy fuels (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 2, "nodata": null, "summary": 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": { "x": 2, "y": 1, "z": 1 } }, "tags": []}The live example is a 2 m × 2 m × 1 m grid. Its 3D shape is [37, 442, 654]
— vertical layers, rows, columns — while DUET’s output will be a 2D
[442, 654] surface on the same horizontal lattice.
Step 2 — Run DUET without output calibration
Section titled “Step 2 — Run DUET without output calibration”This request asks for all three surface-fuel parameters — load, depth, and
moisture — across grass, aggregate litter,
coniferous litter, deciduous litter, and total surface fuel. You can request a
smaller subset for routine runs. Omitting calibration stores DUET’s modeled
values without the API’s optional rescaling step.
curl -X 'POST' \ 'https://api-v2-prod-782971006568.us-west1.run.app/domains/your-domain-id/grids/duet' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "name": "DUET surface fuels (raw pattern)", "source_grid_id": "your-tree-grid-id", "years_since_burn": 25, "wind_direction": 270, "wind_variability": 30, "bands": [ "fuel_load.grass", "fuel_load.litter", "fuel_load.litter.coniferous", "fuel_load.litter.deciduous", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.litter.coniferous", "fuel_depth.litter.deciduous", "fuel_depth.total", "fuel_moisture.grass", "fuel_moisture.litter", "fuel_moisture.litter.coniferous", "fuel_moisture.litter.deciduous", "fuel_moisture.total" ]}'# Omitting `calibration` stores DUET's modeled values without rescaling.raw_grid = ff.grids.create_surface_fuel_grid_from_duet( tree_grid, years_since_burn=25, wind_direction=270, wind_variability=30, bands=[ "fuel_load.grass", "fuel_load.litter", "fuel_load.litter.coniferous", "fuel_load.litter.deciduous", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.litter.coniferous", "fuel_depth.litter.deciduous", "fuel_depth.total", "fuel_moisture.grass", "fuel_moisture.litter", "fuel_moisture.litter.coniferous", "fuel_moisture.litter.deciduous", "fuel_moisture.total", ], name="DUET surface fuels (raw pattern)",){ "id": "your-raw-duet-grid-id", "domain_id": "your-domain-id", "name": "DUET surface fuels (raw pattern)", "description": "", "status": "pending", "progress": null, "created_on": "2026-08-24T18:25:42.533295", "modified_on": "2026-08-24T18:25:42.533295", "checksum": "e625b550288f44a08268768ad7b04469", "source": { "years_since_burn": 25, "wind_direction": 270, "wind_variability": 30, "operation": "duet", "input": "grid", "entity": "tree", "source_grid_id": "your-tree-grid-id", "source_grid_checksum": "5ecd7048b0674f76ba66ee0c129e8702", "bands": [ "fuel_load.grass", "fuel_load.litter", "fuel_load.litter.coniferous", "fuel_load.litter.deciduous", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.litter.coniferous", "fuel_depth.litter.deciduous", "fuel_depth.total", "fuel_moisture.grass", "fuel_moisture.litter", "fuel_moisture.litter.coniferous", "fuel_moisture.litter.deciduous", "fuel_moisture.total" ] }, "modifications": [], "bands": [ { "key": "fuel_load.grass", "name": "Grass Fuel Load", "description": "Oven-dry mass per unit ground area of grass.", "type": "continuous", "unit": "kg/m**2", "index": 0, "nodata": null, "summary": null }, { "key": "fuel_load.litter", "name": "Litter Fuel Load", "description": "Oven-dry mass per unit ground area of litter.", "type": "continuous", "unit": "kg/m**2", "index": 1, "nodata": null, "summary": null }, { "key": "fuel_load.litter.coniferous", "name": "Coniferous Litter Fuel Load", "description": "Oven-dry mass per unit ground area of coniferous litter.", "type": "continuous", "unit": "kg/m**2", "index": 2, "nodata": null, "summary": null }, { "key": "fuel_load.litter.deciduous", "name": "Deciduous Litter Fuel Load", "description": "Oven-dry mass per unit ground area of deciduous litter.", "type": "continuous", "unit": "kg/m**2", "index": 3, "nodata": null, "summary": null }, { "key": "fuel_load.total", "name": "Total Surface Fuel Load", "description": "Oven-dry mass per unit ground area of all surface fuel (grass plus litter).", "type": "continuous", "unit": "kg/m**2", "index": 4, "nodata": null, "summary": null }, { "key": "fuel_depth.grass", "name": "Grass Fuel Depth", "description": "Vertical depth of the grass fuel bed.", "type": "continuous", "unit": "m", "index": 5, "nodata": null, "summary": null }, { "key": "fuel_depth.litter", "name": "Litter Fuel Depth", "description": "Vertical depth of the litter fuel bed.", "type": "continuous", "unit": "m", "index": 6, "nodata": null, "summary": null }, { "key": "fuel_depth.litter.coniferous", "name": "Coniferous Litter Fuel Depth", "description": "Vertical depth of the coniferous litter fuel bed.", "type": "continuous", "unit": "m", "index": 7, "nodata": null, "summary": null }, { "key": "fuel_depth.litter.deciduous", "name": "Deciduous Litter Fuel Depth", "description": "Vertical depth of the deciduous litter fuel bed.", "type": "continuous", "unit": "m", "index": 8, "nodata": null, "summary": null }, { "key": "fuel_depth.total", "name": "Total Surface Fuel Depth", "description": "Vertical depth of the total surface fuel bed (grass plus litter).", "type": "continuous", "unit": "m", "index": 9, "nodata": null, "summary": null }, { "key": "fuel_moisture.grass", "name": "Grass Fuel Moisture", "description": "Moisture content of grass (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 10, "nodata": null, "summary": null }, { "key": "fuel_moisture.litter", "name": "Litter Fuel Moisture", "description": "Moisture content of litter (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 11, "nodata": null, "summary": null }, { "key": "fuel_moisture.litter.coniferous", "name": "Coniferous Litter Fuel Moisture", "description": "Moisture content of coniferous litter (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 12, "nodata": null, "summary": null }, { "key": "fuel_moisture.litter.deciduous", "name": "Deciduous Litter Fuel Moisture", "description": "Moisture content of deciduous litter (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 13, "nodata": null, "summary": null }, { "key": "fuel_moisture.total", "name": "Total Surface Fuel Moisture", "description": "Moisture content of all surface fuel (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 14, "nodata": null, "summary": null } ], "georeference": null, "error": null, "chunks": null, "tags": []}Record the id as your-raw-duet-grid-id and poll it. DUET runs one year of accumulation at a time, so this job commonly takes longer than a fetch or lookup.
curl -X 'GET' \ 'https://api-v2-prod-782971006568.us-west1.run.app/domains/your-domain-id/grids/your-raw-duet-grid-id' \ -H 'accept: application/json' \ -H 'api-key: my-api-key'raw_grid.wait(){ "id": "your-raw-duet-grid-id", "domain_id": "your-domain-id", "name": "DUET surface fuels (raw pattern)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-08-24T18:25:42.533295Z", "modified_on": "2026-08-24T18:27:09.254972Z", "checksum": "e625b550288f44a08268768ad7b04469", "source": { "source_grid_checksum": "5ecd7048b0674f76ba66ee0c129e8702", "source_grid_id": "your-tree-grid-id", "bands": [ "fuel_load.grass", "fuel_load.litter", "fuel_load.litter.coniferous", "fuel_load.litter.deciduous", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.litter.coniferous", "fuel_depth.litter.deciduous", "fuel_depth.total", "fuel_moisture.grass", "fuel_moisture.litter", "fuel_moisture.litter.coniferous", "fuel_moisture.litter.deciduous", "fuel_moisture.total" ], "wind_variability": 30, "wind_direction": 270, "input": "grid", "entity": "tree", "years_since_burn": 25, "operation": "duet" }, "modifications": [], "bands": [ { "key": "fuel_load.grass", "name": "Grass Fuel Load", "description": "Oven-dry mass per unit ground area of grass.", "type": "continuous", "unit": "kg/m**2", "index": 0, "nodata": null, "summary": null }, { "key": "fuel_load.litter", "name": "Litter Fuel Load", "description": "Oven-dry mass per unit ground area of litter.", "type": "continuous", "unit": "kg/m**2", "index": 1, "nodata": null, "summary": null }, { "key": "fuel_load.litter.coniferous", "name": "Coniferous Litter Fuel Load", "description": "Oven-dry mass per unit ground area of coniferous litter.", "type": "continuous", "unit": "kg/m**2", "index": 2, "nodata": null, "summary": null }, { "key": "fuel_load.litter.deciduous", "name": "Deciduous Litter Fuel Load", "description": "Oven-dry mass per unit ground area of deciduous litter.", "type": "continuous", "unit": "kg/m**2", "index": 3, "nodata": null, "summary": null }, { "key": "fuel_load.total", "name": "Total Surface Fuel Load", "description": "Oven-dry mass per unit ground area of all surface fuel (grass plus litter).", "type": "continuous", "unit": "kg/m**2", "index": 4, "nodata": null, "summary": null }, { "key": "fuel_depth.grass", "name": "Grass Fuel Depth", "description": "Vertical depth of the grass fuel bed.", "type": "continuous", "unit": "m", "index": 5, "nodata": null, "summary": null }, { "key": "fuel_depth.litter", "name": "Litter Fuel Depth", "description": "Vertical depth of the litter fuel bed.", "type": "continuous", "unit": "m", "index": 6, "nodata": null, "summary": null }, { "key": "fuel_depth.litter.coniferous", "name": "Coniferous Litter Fuel Depth", "description": "Vertical depth of the coniferous litter fuel bed.", "type": "continuous", "unit": "m", "index": 7, "nodata": null, "summary": null }, { "key": "fuel_depth.litter.deciduous", "name": "Deciduous Litter Fuel Depth", "description": "Vertical depth of the deciduous litter fuel bed.", "type": "continuous", "unit": "m", "index": 8, "nodata": null, "summary": null }, { "key": "fuel_depth.total", "name": "Total Surface Fuel Depth", "description": "Vertical depth of the total surface fuel bed (grass plus litter).", "type": "continuous", "unit": "m", "index": 9, "nodata": null, "summary": null }, { "key": "fuel_moisture.grass", "name": "Grass Fuel Moisture", "description": "Moisture content of grass (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 10, "nodata": null, "summary": null }, { "key": "fuel_moisture.litter", "name": "Litter Fuel Moisture", "description": "Moisture content of litter (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 11, "nodata": null, "summary": null }, { "key": "fuel_moisture.litter.coniferous", "name": "Coniferous Litter Fuel Moisture", "description": "Moisture content of coniferous litter (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 12, "nodata": null, "summary": null }, { "key": "fuel_moisture.litter.deciduous", "name": "Deciduous Litter Fuel Moisture", "description": "Moisture content of deciduous litter (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 13, "nodata": null, "summary": null }, { "key": "fuel_moisture.total", "name": "Total Surface Fuel Moisture", "description": "Moisture content of all surface fuel (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 14, "nodata": null, "summary": 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": []}
A live 25-year DUET run over a mixed Blue Mountain inventory. The dark
outline marks the source canopy footprint. Litter follows the crowns while
grass occupies much of the open space between them. The three surface panels
show fuel load from a request without output calibration. Values are in
kg/m²; each panel uses its own color scale, with the upper limit clipped at
the 99.5th percentile to keep local variation visible.
Use the output to answer spatial questions: does litter follow the canopy, does grass fill plausible openings, and did every expected litter class appear? If the pattern is unexpected, check the inventory, species codes, and stand/wind inputs before deciding whether calibration is appropriate.
Choose the output bands
Section titled “Choose the output bands”DUET separates fuel by type, not by dead-fuel timelag class. Choose bands for the task you need:
| Task | Representative bands |
|---|---|
| Compare grass and all litter | fuel_load.grass, fuel_load.litter |
| Keep mixed-stand litter types separate | fuel_load.litter.coniferous, fuel_load.litter.deciduous |
| Supply one combined surface layer | fuel_load.total, fuel_depth.total, fuel_moisture.total |
| Inspect bed structure rather than mass | fuel_depth.grass, fuel_depth.litter |
| Inspect moisture by fuel type | fuel_moisture.grass, fuel_moisture.litter |
The same fuel-type suffixes are available for load, depth, and moisture. Use the live API reference for the complete enum and validation rules.
Set stand history and wind
Section titled “Set stand history and wind”Three inputs control the run’s time and transport:
| Field | What to set |
|---|---|
years_since_burn | Completed years since the most recent fire, from 1 to 100. Low values leave little time for litter to accumulate; larger values also take longer to run. |
wind_direction | Prevailing wind direction in whole degrees clockwise from north. |
wind_variability | Angular spread around that direction, in whole degrees. |
Use values that describe your stand history and the wind regime whose litter
pattern you intend to model. The example uses 25 years and a west wind
(270° ± 30°). For the modeling rationale, see
About surface fuels and the original
DUET model description (McDanold et al. 2023).
Step 3 — Optionally anchor the output to reference data
Section titled “Step 3 — Optionally anchor the output to reference data”DUET derives spatial variation from the canopy, species, wind, and time since burn. Calibration combines that modeled pattern with independent information about fuel conditions. Use it when you have field measurements, published statistics, or scenario values for fuel load, depth, or moisture and want the DUET output expressed on that basis — for example, as input to a fire model or to compare scenarios using consistent target conditions.
Add a calibration object to the create request and set targets independently
for each parameter and fuel type you want to calibrate. If you do not have
suitable reference data, omit calibration and retain DUET’s native output.
This live-verified request demonstrates all three methods in one run:
meansdscales grass load toward an example covered-cell mean and standard deviation. Use it only when a sufficiently large target dataset is reasonably described by those statistics.maxminmaps litter load onto an example range. Use it when observations are limited or their distribution does not resemble DUET’s raw distribution.constantassigns an example depth or moisture to every cell where that fuel is present. Use it only when one defensible value is all you have.
curl -X 'POST' \ 'https://api-v2-prod-782971006568.us-west1.run.app/domains/your-domain-id/grids/duet' \ -H 'accept: application/json' \ -H 'api-key: my-api-key' \ -H 'Content-Type: application/json' \ -d '{ "name": "DUET surface fuels (example calibration targets)", "source_grid_id": "your-tree-grid-id", "years_since_burn": 25, "wind_direction": 270, "wind_variability": 30, "bands": [ "fuel_load.grass", "fuel_load.litter", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.total", "fuel_moisture.total" ], "calibration": { "fuel_load": { "grass": { "method": "meansd", "mean": 0.5, "sd": 0.25 }, "litter": { "method": "maxmin", "max": 5.0, "min": 0.0 } }, "fuel_depth": { "grass": { "method": "constant", "value": 0.3 }, "litter": { "method": "constant", "value": 0.06 } }, "fuel_moisture": { "all": { "method": "constant", "value": 8.0 } } }}'# Example calibration targets. Replace these with defensible measured,# published, or scenario values before downstream use.calibration = ff.duet_calibration( fuel_load={ "grass": {"method": "meansd", "mean": 0.5, "sd": 0.25}, "litter": {"method": "maxmin", "max": 5.0, "min": 0.0}, }, fuel_depth={ "grass": {"method": "constant", "value": 0.3}, "litter": {"method": "constant", "value": 0.06}, }, fuel_moisture={"all": {"method": "constant", "value": 8.0}},)calibrated_grid = ff.grids.create_surface_fuel_grid_from_duet( tree_grid, years_since_burn=25, wind_direction=270, wind_variability=30, bands=[ "fuel_load.grass", "fuel_load.litter", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.total", "fuel_moisture.total", ], calibration=calibration, name="DUET surface fuels (example calibration targets)",){ "id": "your-calibrated-duet-grid-id", "domain_id": "your-domain-id", "name": "DUET surface fuels (example calibration targets)", "description": "", "status": "pending", "progress": null, "created_on": "2026-08-24T18:26:09.755318", "modified_on": "2026-08-24T18:26:09.755318", "checksum": "60d0e8d0b664480baf9c259408c89ace", "source": { "years_since_burn": 25, "wind_direction": 270, "wind_variability": 30, "operation": "duet", "input": "grid", "entity": "tree", "source_grid_id": "your-tree-grid-id", "source_grid_checksum": "5ecd7048b0674f76ba66ee0c129e8702", "bands": [ "fuel_load.grass", "fuel_load.litter", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.total", "fuel_moisture.total" ], "calibration": { "fuel_load": { "grass": { "method": "meansd", "mean": 0.5, "sd": 0.25 }, "litter": { "method": "maxmin", "max": 5.0, "min": 0.0 } }, "fuel_depth": { "grass": { "method": "constant", "value": 0.3 }, "litter": { "method": "constant", "value": 0.06 } }, "fuel_moisture": { "all": { "method": "constant", "value": 8.0 } } } }, "modifications": [], "bands": [ { "key": "fuel_load.grass", "name": "Grass Fuel Load", "description": "Oven-dry mass per unit ground area of grass.", "type": "continuous", "unit": "kg/m**2", "index": 0, "nodata": null, "summary": null }, { "key": "fuel_load.litter", "name": "Litter Fuel Load", "description": "Oven-dry mass per unit ground area of litter.", "type": "continuous", "unit": "kg/m**2", "index": 1, "nodata": null, "summary": null }, { "key": "fuel_load.total", "name": "Total Surface Fuel Load", "description": "Oven-dry mass per unit ground area of all surface fuel (grass plus litter).", "type": "continuous", "unit": "kg/m**2", "index": 2, "nodata": null, "summary": null }, { "key": "fuel_depth.grass", "name": "Grass Fuel Depth", "description": "Vertical depth of the grass fuel bed.", "type": "continuous", "unit": "m", "index": 3, "nodata": null, "summary": null }, { "key": "fuel_depth.litter", "name": "Litter Fuel Depth", "description": "Vertical depth of the litter fuel bed.", "type": "continuous", "unit": "m", "index": 4, "nodata": null, "summary": null }, { "key": "fuel_depth.total", "name": "Total Surface Fuel Depth", "description": "Vertical depth of the total surface fuel bed (grass plus litter).", "type": "continuous", "unit": "m", "index": 5, "nodata": null, "summary": null }, { "key": "fuel_moisture.total", "name": "Total Surface Fuel Moisture", "description": "Moisture content of all surface fuel (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 6, "nodata": null, "summary": null } ], "georeference": null, "error": null, "chunks": null, "tags": []}Record your-calibrated-duet-grid-id and poll the second grid to
completed:
curl -X 'GET' \ 'https://api-v2-prod-782971006568.us-west1.run.app/domains/your-domain-id/grids/your-calibrated-duet-grid-id' \ -H 'accept: application/json' \ -H 'api-key: my-api-key'calibrated_grid.wait(){ "id": "your-calibrated-duet-grid-id", "domain_id": "your-domain-id", "name": "DUET surface fuels (example calibration targets)", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-08-24T18:26:09.755318Z", "modified_on": "2026-08-24T18:28:12.022273Z", "checksum": "60d0e8d0b664480baf9c259408c89ace", "source": { "source_grid_checksum": "5ecd7048b0674f76ba66ee0c129e8702", "source_grid_id": "your-tree-grid-id", "bands": [ "fuel_load.grass", "fuel_load.litter", "fuel_load.total", "fuel_depth.grass", "fuel_depth.litter", "fuel_depth.total", "fuel_moisture.total" ], "wind_variability": 30, "entity": "tree", "input": "grid", "wind_direction": 270, "calibration": { "fuel_load": { "litter": { "min": 0.0, "method": "maxmin", "max": 5.0 }, "grass": { "sd": 0.25, "method": "meansd", "mean": 0.5 } }, "fuel_moisture": { "all": { "value": 8.0, "method": "constant" } }, "fuel_depth": { "litter": { "value": 0.06, "method": "constant" }, "grass": { "value": 0.3, "method": "constant" } } }, "years_since_burn": 25, "operation": "duet" }, "modifications": [], "bands": [ { "key": "fuel_load.grass", "name": "Grass Fuel Load", "description": "Oven-dry mass per unit ground area of grass.", "type": "continuous", "unit": "kg/m**2", "index": 0, "nodata": null, "summary": null }, { "key": "fuel_load.litter", "name": "Litter Fuel Load", "description": "Oven-dry mass per unit ground area of litter.", "type": "continuous", "unit": "kg/m**2", "index": 1, "nodata": null, "summary": null }, { "key": "fuel_load.total", "name": "Total Surface Fuel Load", "description": "Oven-dry mass per unit ground area of all surface fuel (grass plus litter).", "type": "continuous", "unit": "kg/m**2", "index": 2, "nodata": null, "summary": null }, { "key": "fuel_depth.grass", "name": "Grass Fuel Depth", "description": "Vertical depth of the grass fuel bed.", "type": "continuous", "unit": "m", "index": 3, "nodata": null, "summary": null }, { "key": "fuel_depth.litter", "name": "Litter Fuel Depth", "description": "Vertical depth of the litter fuel bed.", "type": "continuous", "unit": "m", "index": 4, "nodata": null, "summary": null }, { "key": "fuel_depth.total", "name": "Total Surface Fuel Depth", "description": "Vertical depth of the total surface fuel bed (grass plus litter).", "type": "continuous", "unit": "m", "index": 5, "nodata": null, "summary": null }, { "key": "fuel_moisture.total", "name": "Total Surface Fuel Moisture", "description": "Moisture content of all surface fuel (% of oven-dry weight).", "type": "continuous", "unit": "%", "index": 6, "nodata": null, "summary": 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": []}Calibration is per parameter and fuel type. Do not expect the mean over the
whole domain to equal a meansd.mean target when only part of the domain
contains that fuel; the target applies to fuel-bearing cells.

The same DUET run before and after example calibration. The meansd method
relates grass values to a target mean and standard deviation, while maxmin
maps litter values to a target range. The spatial support remains the same;
the displayed value distributions change. These example targets illustrate
the API methods rather than recommended values for Blue Mountain. Each map’s
color scale is clipped at the 99.5th percentile to keep local variation
visible.
Step 4 — Inspect the result
Section titled “Step 4 — Inspect the result”The example below inspects the calibrated grid. Use the uncalibrated grid ID instead if you omitted Step 3. Check the metadata before reading values:
statusiscompleted;georeference.shapeis the expected 2D horizontal shape;- every requested band is present with the expected unit (
kg/m**2,m, or%); and - for a calibrated grid,
source.calibrationrecords the targets you intended.
Then stream representative bands and check their nonzero coverage and range:
import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
# Use the uncalibrated grid id instead if you omitted calibration.grid = ff.get_grid("your-domain-id", "your-calibrated-duet-grid-id")grid.wait()
# Check the metadata before reading values.print(grid.status, grid.to_dict()["georeference"]["shape"])
# to_numpy fetches every chunk of a band and reassembles it into one 2D array.for band in ("fuel_load.grass", "fuel_load.litter", "fuel_load.total"): values = grid.to_numpy(band) positive = values[values > 0] print( band, f"coverage={positive.size / values.size:.1%}", f"range={positive.min():.3f}–{positive.max():.3f}", )grid.to_numpy(band) fetches every chunk of a band and reassembles it into one
2D array. Plot the arrays or compare them to field plots before downstream use.
The general
grid-data guide covers JSON,
sparse, and binary reads in more detail.
Use the grid downstream
Section titled “Use the grid downstream”- Align before combining. The DUET result inherits the source tree grid’s horizontal lattice. Resample or reproject other 2D grids to match it; a 3D tree grid itself cannot be resampled.
- Export DUET bands by role. A QUIC-Fire export can use
fuel_load.total,fuel_depth.total, andfuel_moisture.totalas its 2D surface load, depth, and moisture roles. Decide whether uncalibrated or calibrated values are appropriate for your application and document the inputs or targets you used. DUET does not produce surface SAVR. If you needtreesss.dat, supply both canopy and surface SAVR from other aligned grids; otherwise omit both optional SAVR roles. See the QUIC-Fire inputs tutorial for the full export assembly. - Export for GIS or analysis. Use a GeoTIFF export for selected 2D bands, or NetCDF/Zarr when that format better fits your workflow.
Common pitfalls
Section titled “Common pitfalls”- Missing source bands. A normal canopy voxelization often lacks
spcd. Re-voxelize the inventory with all three required bands; DUET validates them before starting. - Invalid species codes. Use FIA species codes on the source inventory. Generic or placeholder values that are not FIA species cannot be assigned litter properties; correct the inventory and voxelize again.
- Calibrating a fuel type that is absent. A pure-conifer stand cannot satisfy a deciduous target. Inspect the raw split-litter bands and remove targets for fuel types DUET did not produce.
- Calibrating without a defensible target. Calibration expresses the DUET output against values you supply; it does not determine suitable values for your site. Record the measurements, publication, or scenario assumptions behind each target.
- Too few years since burn. DUET starts with consumed grass and litter at the burn year, so a very small value can yield little accumulated litter.
- Expecting an inventory to contain surface fuel. The inventory contains trees. DUET derives a surface pattern from their canopy structure; it does not reveal measured litter or grass that was stored on the inventory.
- Assuming a fast job. Runtime grows with
years_since_burnand grid size. Poll asynchronously and treatfailedas terminal. - Using the wrong bands at export. DUET’s grass/litter vocabulary is not the
FBFM lookup’s
1hr/10hr/100hrvocabulary. Assign the DUET band you chose explicitly to the downstream role by its physical meaning and unit.