Skip to content

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.

  1. An API key: my-api-key.

  2. A domain: your-domain-id.

  3. A completed tree inventory with dbh, crown_ratio, and fia_species_code populated on every tree: your-inventory-id. A TreeMap inventory is ready to use. Complete a thin CHM or upload inventory with GDAM allometry first.

  4. 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; and
  • fuel_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.

POST grids/voxelize/inventory/tree
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
}'

Record the id as your-tree-grid-id and poll it to completed:

GET tree grid status
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'

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.

POST grids/duet — uncalibrated
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"
]
}'

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.

GET raw DUET grid status
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'
Four maps of the same Blue Mountain forest detail arranged in a two-by-two grid. Canopy foliage appears as green crown clusters. Orange DUET litter follows those clusters and their edges, grass is strongest in many canopy openings, and total surface fuel combines both patterns. The three DUET panels are labeled as uncalibrated values.

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.

DUET separates fuel by type, not by dead-fuel timelag class. Choose bands for the task you need:

TaskRepresentative bands
Compare grass and all litterfuel_load.grass, fuel_load.litter
Keep mixed-stand litter types separatefuel_load.litter.coniferous, fuel_load.litter.deciduous
Supply one combined surface layerfuel_load.total, fuel_depth.total, fuel_moisture.total
Inspect bed structure rather than massfuel_depth.grass, fuel_depth.litter
Inspect moisture by fuel typefuel_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.

Three inputs control the run’s time and transport:

FieldWhat to set
years_since_burnCompleted 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_directionPrevailing wind direction in whole degrees clockwise from north.
wind_variabilityAngular 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:

  • meansd scales 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.
  • maxmin maps litter load onto an example range. Use it when observations are limited or their distribution does not resemble DUET’s raw distribution.
  • constant assigns an example depth or moisture to every cell where that fuel is present. Use it only when one defensible value is all you have.
POST grids/duet — calibrated
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 }
}
}
}'

Record your-calibrated-duet-grid-id and poll the second grid to completed:

GET calibrated DUET grid status
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'

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.

Four maps compare DUET grass and litter fuel loads before and after calibration. The locations of grass and litter remain the same while the color-bar values change. Grass is calibrated with a target mean and standard deviation; litter is calibrated to a target range.

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.

The example below inspects the calibrated grid. Use the uncalibrated grid ID instead if you omitted Step 3. Check the metadata before reading values:

  • status is completed;
  • georeference.shape is 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.calibration records the targets you intended.

Then stream representative bands and check their nonzero coverage and range:

Inspect calibrated DUET bands
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.

  • 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, and fuel_moisture.total as 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 need treesss.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.
  • 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_burn and grid size. Poll asynchronously and treat failed as terminal.
  • Using the wrong bands at export. DUET’s grass/litter vocabulary is not the FBFM lookup’s 1hr/10hr/100hr vocabulary. Assign the DUET band you chose explicitly to the downstream role by its physical meaning and unit.