Voxelize a tree inventory into a 3D canopy grid
You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.
This guide shows you how to turn a completed tree inventory into a 3D canopy
fuel grid: a voxel lattice where each cell carries live-foliage bulk density
(kg/m**3). The first two sections create the grid and check it. Each
section after that sets one option of the request. For what each option does
and how to choose between them, see
How tree voxelization works.
Prerequisites
Section titled “Prerequisites”-
An API key: my-api-key.
-
A domain: your-domain-id.
-
A completed tree inventory that carries
dbh,crown_ratio, andfia_species_code. Record its id: your-inventory-id.
Unless a section says otherwise, the responses and figures on this page come
from a TreeMap inventory of 44,099 trees on the Blue Mountain domain from the
QUIC-Fire tutorial,
voxelized with seed: 42.
Create the canopy grid
Section titled “Create the canopy grid”POST to grids/voxelize/inventory/tree. Set these fields:
source_inventory_id: the completed inventory.resolution: the voxel size in meters,horizontalfor x/y andverticalfor z.bands: the outputs. This request asks forbulk_density.foliage.live, which is also the default.biomass_source: estimates foliage mass from thensvballometric equations and counts all of it as live.seed: fixes which voxels each crown fills, so the grid is reproducible.
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 grid", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "bands": ["bulk_density.foliage.live"], "biomass_source": { "type": "allometry", "equations": "nsvb", "components": ["foliage"], "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } } }, "seed": 42}'import fastfuels_sdk.v2 as fffrom fastfuels_sdk.v2.client_library.models import ( AllometryBiomassSource, AllometryBiomassSourceComponentStates, BiomassComponent, BiomassEquations,)
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 completed Inventory. resolution is horizontal# (x/y) and vertical (z) in meters; seed fixes the stochastic crown thinning.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, bands=["bulk_density.foliage.live"], biomass_source=AllometryBiomassSource( equations=BiomassEquations.NSVB, components=[BiomassComponent.FOLIAGE], component_states=AllometryBiomassSourceComponentStates.from_dict( {"foliage": {"live": 1.0, "dead": 0.0}} ), ), seed=42, name="Canopy fuel grid",){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid", "description": "", "status": "pending", "progress": null, "created_on": "2026-09-22T17:16:27.115053", "modified_on": "2026-09-22T17:16:27.115053", "checksum": "a6fa743f34cc49ddbdd925cf4df7ee19", "source": { "operation": "voxelize", "input": "inventory", "entity": "tree", "source_inventory_id": "your-inventory-id", "source_inventory_checksum": "8b8f79eeb1ce4d1492e6096292f488e9", "resolution": { "horizontal": 2.0, "vertical": 1.0 }, "bands": ["bulk_density.foliage.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" }, "seed": 42 }, "modifications": [], "bands": [ { "key": "bulk_density.foliage.live", "type": "continuous", "unit": "kg/m**3", "index": 0, "nodata": null } ], "georeference": null, "error": null, "chunks": null, "tags": []}Record the grid id: your-tree-grid-id. Then poll until the status is
completed:
curl -X 'GET' \ 'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-tree-grid-id' \ -H 'accept: application/json' \ -H 'api-key: my-api-key'grid.wait()
# grid.georeference.shape is [nz, ny, nx].{ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-09-22T17:16:27.115053Z", "modified_on": "2026-09-22T17:17:02.380315Z", "checksum": "a6fa743f34cc49ddbdd925cf4df7ee19", "source": { "resolution": { "vertical": 1.0, "horizontal": 2.0 }, "biomass_source": { "equations": "nsvb", "type": "allometry", "components": ["foliage"], "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } } }, "input": "inventory", "max_crown_radius_source": { "type": "allometry" }, "crown_profile_model": "purves", "operation": "voxelize", "entity": "tree", "source_inventory_checksum": "8b8f79eeb1ce4d1492e6096292f488e9", "bands": ["bulk_density.foliage.live"], "source_inventory_id": "your-inventory-id", "seed": 42 }, "modifications": [], "bands": [ { "key": "bulk_density.foliage.live", "type": "continuous", "unit": "kg/m**3", "index": 0, "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": { "y": 1, "z": 1, "x": 2 } }, "tags": []}Check the result
Section titled “Check the result”Read georeference.shape as [nz, ny, nx]. Here it is [37, 442, 654]: 37
vertical layers of 1 m over a 442 × 654 horizontal lattice of 2 m cells. The
horizontal lattice and origin come from the domain and resolution. The
number of layers is the tallest tree’s height divided by the vertical size,
rounded up. The source inventory’s checksum is recorded as
source.source_inventory_checksum, so you can tell later whether the
inventory has changed since this grid was built.

The finished canopy grid. Each green box is one 2 × 2 × 1 m cell that holds live foliage. Together they are about 496,000 of the grid’s 10.7 million cells, built from 44,099 trees. The grid stores height above ground, so the render stands the cells on a 3DEP topography grid on the same lattice. The color scale stops at the 99th percentile of filled cells.
To check the vertical structure, read a single row of the grid and plot it as
height against x. To read the values, see
fetch the grid data.
Voxel grids are mostly empty, so request ?array_format=sparse. A full-height
3D chunk is usually too large to return as dense values.

Top: the grid summed over height gives the live foliage load in each column. Bottom: the vertical section along the pink line. Each crown is a cluster of 2 × 1 m cells whose height and depth follow that tree’s height and crown ratio. The gaps inside crowns are part of the model, not missing data. Cells with no foliage are grey.
Set the resolution
Section titled “Set the resolution”Set resolution to the lattice of the grids you will combine this one with.
{"horizontal": 2, "vertical": 1} is the default. The same request at
{"horizontal": 1, "vertical": 0.5} completes with shape [74, 884, 1308],
eight times as many cells.
Every grid in a combined export must share one lattice. If your surface or terrain grids already exist on a different lattice, resample them to match this one. For what resolution changes in the canopy, see Resolution.
Set the crown shape
Section titled “Set the crown shape”Set crown_profile_model to one of purves (the default), beta, cone,
cylinder, single_ellipsoid, dual_ellipsoid, single_paraboloid, or
dual_paraboloid. This request draws every crown as a dual paraboloid:
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 grid, dual paraboloid crowns", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "crown_profile_model": "dual_paraboloid", "seed": 42}'# Continues from the create step: the same completed inventory.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, crown_profile_model="dual_paraboloid", seed=42, name="Canopy fuel grid, dual paraboloid crowns",)The completed grid records the model in source.crown_profile_model. For the
shape of each model, see
Crown profile models.
Use measured crown radii
Section titled “Use measured crown radii”To set each crown’s maximum radius from a column of the inventory instead of
allometry, set max_crown_radius_source to the column. The crown keeps the
shape crown_profile_model gives it.
You need a completed inventory with dbh, crown_ratio, fia_species_code
and a crown-radius column in meters. One way to get it: detect trees from a CHM
with
crown_segmentation,
which adds a crown_radius column, then
complete the inventory with GDAM,
which keeps it. You can also
upload an
inventory with a crown_radius column.
The examples in this section and the next two come from a 500 m × 500 m domain
at Lubrecht Experimental Forest, Montana: 3,346 trees detected on a NAIP CHM,
completed with GDAM, and voxelized at 1 m with seed: 42.
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 grid, measured crown radius", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 1, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "tree_id"], "max_crown_radius_source": { "type": "inventory_column", "column": "crown_radius" }, "seed": 42}'import fastfuels_sdk.v2 as fffrom fastfuels_sdk.v2.client_library.models import InventoryColumnMaxCrownRadiusSource
ff.set_api_key("my-api-key")
# A GDAM-completed inventory whose CHM detection added a crown_radius column.inventory = ff.get_inventory("your-domain-id", "your-inventory-id")
grid = inventory.voxelize( horizontal_resolution_m=1, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "tree_id"], max_crown_radius_source=InventoryColumnMaxCrownRadiusSource(column="crown_radius"), seed=42, name="Canopy fuel grid, measured crown radius",)grid.wait(){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid, measured crown radius", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-10-13T02:19:09.528535Z", "modified_on": "2026-10-13T02:19:20.374807Z", "checksum": "75ca4ee468a24183985803b61feb99a1", "source": { "max_crown_radius_source": { "type": "inventory_column", "unit": "m", "column": "crown_radius" }, "crown_geometry_source": { "type": "parametric" }, "operation": "voxelize", "crown_profile_model": "purves", "seed": 42, "resolution": { "vertical": 1.0, "horizontal": 1.0 }, "input": "inventory", "biomass_source": { "type": "allometry", "components": ["foliage"], "component_states": { "foliage": { "dead": 0.0, "live": 1.0 } }, "equations": "nsvb" }, "source_inventory_id": "your-inventory-id", "entity": "tree", "source_inventory_checksum": "4874a36dc9f1416bb59ffb1635d404c3", "bands": ["bulk_density.foliage.live", "tree_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": "tree_id", "name": "Tree ID", "description": "The source inventory's `tree_id` of the dominant tree contributing to the voxel; 0 where no tree.", "type": "categorical", "unit": null, "index": 1, "nodata": null, "summary": null } ], "georeference": { "crs": "urn:ogc:def:crs:EPSG::32612", "transform": [1.0, 0.0, 314296.0, 0.0, -1.0, 5194931.0], "shape": [32, 500, 500], "z_resolution": 1.0, "z_origin": 0.0 }, "error": null, "chunks": { "shape": [32, 500, 500], "count": 1, "count_by_axis": { "x": 1, "y": 1, "z": 1 } }, "tags": []}A tree with no value in the column gets its allometric radius.
Use measured crown shapes
Section titled “Use measured crown shapes”To draw each crown’s outline and top from a CHM, set crown_geometry_source
to a crown Feature. You need two things:
- A completed inventory with a
tree_idcolumn, plusdbh,crown_ratioandfia_species_code. A CHM inventory completed with GDAM has all four: your-inventory-id. - A crown Feature made from the trees that inventory came from:
your-crown-feature-id. See
Create a crown Feature from a CHM.
Trees are matched to crowns by
tree_id, and GDAM keeps thetree_idof every tree it completes.
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 grid, measured crown shape", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 1, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "tree_id"], "crown_geometry_source": { "type": "crown_feature", "feature_id": "your-crown-feature-id" }, "seed": 42}'import fastfuels_sdk.v2 as ff
ff.set_api_key("my-api-key")
# A GDAM-completed inventory, and the crown Feature built from the# tree-detection inventory it was completed from.inventory = ff.get_inventory("your-domain-id", "your-inventory-id")crowns = ff.get_feature("your-domain-id", "your-crown-feature-id")
grid = inventory.voxelize( horizontal_resolution_m=1, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "tree_id"], crown_geometry_source=crowns, seed=42, name="Canopy fuel grid, measured crown shape",)grid.wait()print(grid.source["crown_geometry_counts"]){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid, measured crown shape", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-10-13T02:19:25.544966Z", "modified_on": "2026-10-13T02:19:43.287979Z", "checksum": "77d4afb5865a442f9589c5e6baa9a1e3", "source": { "crown_geometry_counts": { "parametric_no_crown": 0, "parametric_stem_outside_crown": 0, "measured": 3346, "parametric_no_volume": 0 }, "seed": 42, "source_chm_grid_checksum": "5e71cdcbf707472089062914b0263013", "crown_profile_model": "purves", "operation": "voxelize", "source_chm_grid_id": "your-chm-grid-id", "input": "inventory", "source_inventory_id": "your-inventory-id", "bands": ["bulk_density.foliage.live", "tree_id"], "max_crown_radius_source": { "type": "allometry" }, "crown_geometry_source": { "feature_id": "your-crown-feature-id", "type": "crown_feature" }, "resolution": { "vertical": 1.0, "horizontal": 1.0 }, "biomass_source": { "type": "allometry", "components": ["foliage"], "component_states": { "foliage": { "dead": 0.0, "live": 1.0 } }, "equations": "nsvb" }, "entity": "tree", "source_inventory_checksum": "4874a36dc9f1416bb59ffb1635d404c3" }, "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": "tree_id", "name": "Tree ID", "description": "The source inventory's `tree_id` of the dominant tree contributing to the voxel; 0 where no tree.", "type": "categorical", "unit": null, "index": 1, "nodata": null, "summary": null } ], "georeference": { "crs": "urn:ogc:def:crs:EPSG::32612", "transform": [1.0, 0.0, 314296.0, 0.0, -1.0, 5194931.0], "shape": [32, 500, 500], "z_resolution": 1.0, "z_origin": 0.0 }, "error": null, "chunks": { "shape": [32, 500, 500], "count": 1, "count_by_axis": { "x": 1, "y": 1, "z": 1 } }, "tags": []}Check source.crown_geometry_counts on the completed grid. measured is the
number of trees drawn with a measured crown. The other three count trees drawn
from crown_profile_model instead:
parametric_no_crown: the tree has no crown in the Feature.parametric_stem_outside_crown: the tree’s crown does not contain its stem.parametric_no_volume: the CHM is not above the tree’s crown base anywhere in its crown.
Here all 3,346 trees got a measured crown. The grid also records the Feature’s
CHM grid in source.source_chm_grid_id, with its checksum.
max_crown_radius_source does not apply to measured crowns.
Choose the biomass equations
Section titled “Choose the biomass equations”Set biomass_source.equations to jenkins to estimate foliage mass with the
Jenkins et al. (2003) equations instead of the default nsvb:
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 grid, Jenkins foliage", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "biomass_source": { "type": "allometry", "equations": "jenkins" }, "seed": 42}'from fastfuels_sdk.v2.client_library.models import AllometryBiomassSource, BiomassEquations
# Continues from the create step: the same completed inventory.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, biomass_source=AllometryBiomassSource(equations=BiomassEquations.JENKINS), seed=42, name="Canopy fuel grid, Jenkins foliage",){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid, Jenkins foliage", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-10-13T05:50:02.583919Z", "modified_on": "2026-10-13T05:50:18.023554Z", "checksum": "253bdba9a3f748129a9a8495b0106ae5", "source": { "source_inventory_id": "your-inventory-id", "seed": 42, "crown_profile_model": "purves", "entity": "tree", "crown_geometry_source": { "type": "parametric" }, "resolution": { "horizontal": 2.0, "vertical": 1.0 }, "bands": ["bulk_density.foliage.live"], "source_inventory_checksum": "a33c36af60ed4821af1d0bc8c356fef4", "operation": "voxelize", "biomass_source": { "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } }, "equations": "jenkins", "type": "allometry", "components": ["foliage"] }, "max_crown_radius_source": { "type": "allometry" }, "input": "inventory" }, "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 } ], "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": { "y": 1, "x": 2, "z": 1 } }, "tags": []}For how the two sets of equations differ, see Foliage mass.
Split foliage into live and dead
Section titled “Split foliage into live and dead”To count part of each tree’s foliage as dead, set
biomass_source.component_states.foliage to the live and dead shares, which
must sum to 1, and request bulk_density.foliage.dead alongside the live
band. This request counts 10 % of the foliage as dead:
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 grid, 10 % dead foliage", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "bulk_density.foliage.dead"], "biomass_source": { "type": "allometry", "equations": "nsvb", "component_states": { "foliage": { "live": 0.9, "dead": 0.1 } } }, "seed": 42}'from fastfuels_sdk.v2.client_library.models import ( AllometryBiomassSource, AllometryBiomassSourceComponentStates, BiomassEquations,)
# Continues from the create step: the same completed inventory.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "bulk_density.foliage.dead"], biomass_source=AllometryBiomassSource( equations=BiomassEquations.NSVB, component_states=AllometryBiomassSourceComponentStates.from_dict( {"foliage": {"live": 0.9, "dead": 0.1}} ), ), seed=42, name="Canopy fuel grid, 10 % dead foliage",){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid, 10 % dead foliage", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-10-13T05:50:34.619501Z", "modified_on": "2026-10-13T05:50:49.177047Z", "checksum": "ec81333384274202984552b6593d2ba2", "source": { "source_inventory_id": "your-inventory-id", "seed": 42, "crown_profile_model": "purves", "entity": "tree", "crown_geometry_source": { "type": "parametric" }, "resolution": { "horizontal": 2.0, "vertical": 1.0 }, "bands": ["bulk_density.foliage.live", "bulk_density.foliage.dead"], "source_inventory_checksum": "a33c36af60ed4821af1d0bc8c356fef4", "operation": "voxelize", "biomass_source": { "component_states": { "foliage": { "live": 0.9, "dead": 0.1 } }, "equations": "nsvb", "type": "allometry", "components": ["foliage"] }, "max_crown_radius_source": { "type": "allometry" }, "input": "inventory" }, "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": "bulk_density.foliage.dead", "name": "Dead Foliage Bulk Density", "description": "Per-voxel bulk density of dead foliage.", "type": "continuous", "unit": "kg/m**3", "index": 1, "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": { "y": 1, "z": 1, "x": 2 } }, "tags": []}The split is the same for every tree. In every voxel, the dead band holds one ninth of the live band’s value.
Cap bulk density
Section titled “Cap bulk density”To limit the densest voxels, set max_bulk_density in kg/m3. Any voxel
above the limit is lowered to it, and the foliage above the limit is removed
from the grid. This request repeats the measured-shape request with a limit of
2 kg/m3:
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 grid, measured crown shape, capped", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 1, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "tree_id"], "crown_geometry_source": { "type": "crown_feature", "feature_id": "your-crown-feature-id" }, "max_bulk_density": 2.0, "seed": 42}'# Continues from the measured-shape step: the same inventory and crown Feature.grid = inventory.voxelize( horizontal_resolution_m=1, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "tree_id"], crown_geometry_source=crowns, max_bulk_density=2.0, seed=42, name="Canopy fuel grid, measured crown shape, capped",){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid, measured crown shape, capped", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-10-13T02:19:46.840707Z", "modified_on": "2026-10-13T02:20:05.845897Z", "checksum": "6b6296d128dc43589cbdcfe970aefce8", "source": { "crown_geometry_counts": { "parametric_no_crown": 0, "parametric_stem_outside_crown": 0, "measured": 3346, "parametric_no_volume": 0 }, "seed": 42, "source_chm_grid_checksum": "5e71cdcbf707472089062914b0263013", "crown_profile_model": "purves", "max_bulk_density": 2.0, "source_inventory_id": "your-inventory-id", "input": "inventory", "source_chm_grid_id": "your-chm-grid-id", "operation": "voxelize", "bands": ["bulk_density.foliage.live", "tree_id"], "max_crown_radius_source": { "type": "allometry" }, "crown_geometry_source": { "feature_id": "your-crown-feature-id", "type": "crown_feature" }, "resolution": { "vertical": 1.0, "horizontal": 1.0 }, "biomass_source": { "type": "allometry", "components": ["foliage"], "component_states": { "foliage": { "dead": 0.0, "live": 1.0 } }, "equations": "nsvb" }, "entity": "tree", "source_inventory_checksum": "4874a36dc9f1416bb59ffb1635d404c3" }, "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": "tree_id", "name": "Tree ID", "description": "The source inventory's `tree_id` of the dominant tree contributing to the voxel; 0 where no tree.", "type": "categorical", "unit": null, "index": 1, "nodata": null, "summary": null } ], "georeference": { "crs": "urn:ogc:def:crs:EPSG::32612", "transform": [1.0, 0.0, 314296.0, 0.0, -1.0, 5194931.0], "shape": [32, 500, 500], "z_resolution": 1.0, "z_origin": 0.0 }, "error": null, "chunks": { "shape": [32, 500, 500], "count": 1, "count_by_axis": { "x": 1, "y": 1, "z": 1 } }, "tags": []}Omit the field for no limit. For how much foliage a limit removes, see Capping bulk density.
Request leaf area density and tree IDs
Section titled “Request leaf area density and tree IDs”List the bands you need in bands. This request adds leaf_area_density, the
leaf area per voxel volume, and tree_id, the inventory tree_id of the
tallest tree in each voxel:
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 grid with leaf area and tree IDs", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "leaf_area_density", "tree_id"], "seed": 42}'# Continues from the create step: the same completed inventory.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "leaf_area_density", "tree_id"], seed=42, name="Canopy fuel grid with leaf area and tree IDs",){ "id": "your-tree-grid-id", "domain_id": "your-domain-id", "name": "Canopy fuel grid with leaf area and tree IDs", "description": "", "status": "completed", "progress": { "percent": 100, "message": "Complete" }, "created_on": "2026-10-13T05:50:56.171147Z", "modified_on": "2026-10-13T05:51:11.693730Z", "checksum": "9bd8ed43564541a6b749a84e666b5348", "source": { "source_inventory_id": "your-inventory-id", "seed": 42, "crown_profile_model": "purves", "entity": "tree", "crown_geometry_source": { "type": "parametric" }, "resolution": { "horizontal": 2.0, "vertical": 1.0 }, "bands": ["bulk_density.foliage.live", "leaf_area_density", "tree_id"], "source_inventory_checksum": "a33c36af60ed4821af1d0bc8c356fef4", "operation": "voxelize", "biomass_source": { "component_states": { "foliage": { "live": 1.0, "dead": 0.0 } }, "equations": "nsvb", "type": "allometry", "components": ["foliage"] }, "max_crown_radius_source": { "type": "allometry" }, "input": "inventory" }, "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": "leaf_area_density", "name": "Leaf Area Density", "description": "Per-voxel one-sided leaf area per unit canopy volume (m**2 of leaf area per m**3), which reduces to the canonical 1/m.", "type": "continuous", "unit": "1/m", "index": 1, "nodata": null, "summary": null }, { "key": "tree_id", "name": "Tree ID", "description": "The source inventory's `tree_id` of the dominant tree contributing to the voxel; 0 where no tree.", "type": "categorical", "unit": null, "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": { "y": 1, "z": 1, "x": 2 } }, "tags": []}Each band is stored on the same lattice, with index in the order you listed
them. For every band and what it holds, see
Bands.
Request the bands a QUIC-Fire export needs
Section titled “Request the bands a QUIC-Fire export needs”A QUIC-Fire export takes the canopy’s bulk density and moisture from the tree
grid. Surface-area-to-volume ratio is optional. To produce all three on one
grid, list them in bands. Also set the live fuel moisture, which
defaults to a uniform 100 % when moisture_model is omitted.
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 grid for QUIC-Fire", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "fuel_moisture.live", "savr.foliage"], "moisture_model": { "live": { "method": "uniform", "value": 120 } }, "seed": 42}'from fastfuels_sdk.v2.client_library.models import MoistureModel, UniformMoistureValue
# Continues from the create step: the same completed inventory.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "fuel_moisture.live", "savr.foliage"], moisture_model=MoistureModel(live=UniformMoistureValue(value=120)), seed=42, name="Canopy fuel grid for QUIC-Fire",)The
QUIC-Fire tutorial wires
them into the export’s canopy_bulk_density, canopy_moisture, and
canopy_savr roles.
Request the bands DUET needs
Section titled “Request the bands DUET needs”To use the canopy as a DUET surface-fuel source, request
bulk_density.foliage.live, spcd, and fuel_moisture.live. DUET requires
all three.
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 grid for DUET", "source_inventory_id": "your-inventory-id", "resolution": { "horizontal": 2, "vertical": 1 }, "bands": ["bulk_density.foliage.live", "spcd", "fuel_moisture.live"], "seed": 42}'# Continues from the create step: the same completed inventory.grid = inventory.voxelize( horizontal_resolution_m=2, vertical_resolution_m=1, bands=["bulk_density.foliage.live", "spcd", "fuel_moisture.live"], seed=42, name="Canopy grid for DUET",)Continue with Build surface fuels from a tree grid with DUET.
Next steps
Section titled “Next steps”- Understand the options. See How tree voxelization works for what each option on this page does to the canopy.
- Export to QUIC-Fire. Stack this canopy grid with surface fuel, terrain,
and moisture into the
.datbundle. The QUIC-Fire tutorial walks through the whole pipeline. - Derive a canopy-conditioned surface pattern. Build the DUET band set above, then run DUET and optionally calibrate its output.
- Summarize it for a 2D fire model. To get CBD, CBH, canopy height, and cover instead of voxels, derive canopy fuel from the inventory directly.
Common pitfalls
Section titled “Common pitfalls”-
Inventory missing morphology columns. Some inventories lack
dbh,crown_ratio, orfia_species_code. For example, a CHM inventory carries onlytree_id,x,y,heightand, with crown segmentation,crown_radius. Voxelizing one returns a422. The error names the missing columns and points you at the allometry endpoint:{"detail": "Inventory 'your-inventory-id' is missing column(s) ['crown_ratio', 'dbh', 'fia_species_code'] required for voxelization. Impute ['crown_ratio', 'dbh', 'fia_species_code'] with the allometry endpoint (POST /domains/your-domain-id/inventories/tree/allometry/gdam with source_tree_inventory_id='your-inventory-id'), then voxelize the resulting inventory."}Complete the inventory with GDAM allometry, then voxelize the resulting inventory.
-
Requesting branchwood or fine-fuel bands. The API accepts
bulk_density.branchwood.*andbulk_density.fine.*with a matchingbiomass_source, and returns201. Processing then fails, because only foliage biomass can be voxelized today:{"id": "your-tree-grid-id","domain_id": "your-domain-id","name": "Branchwood attempt","description": "","status": "failed","progress": {"percent": 100,"message": "Failed"},"created_on": "2026-09-22T17:18:10.821896Z","modified_on": "2026-09-22T17:18:16.649739Z","checksum": "dec8534ff66d46e59f81d7a99667fd43","source": {"resolution": {"vertical": 1.0,"horizontal": 2.0},"biomass_source": {"equations": "nsvb","type": "allometry","components": ["branchwood"],"component_states": {"branchwood": {"live": 1.0,"dead": 0.0}}},"input": "inventory","max_crown_radius_source": {"type": "allometry"},"crown_profile_model": "purves","operation": "voxelize","entity": "tree","source_inventory_checksum": "8b8f79eeb1ce4d1492e6096292f488e9","bands": ["bulk_density.branchwood.live"],"source_inventory_id": "your-inventory-id","seed": 42},"modifications": [],"bands": [{"key": "bulk_density.branchwood.live","type": "continuous","unit": "kg/m**3","index": 0,"nodata": null}],"georeference": null,"error": {"code": "BIOMASS_COMPONENT_NOT_IMPLEMENTED","message": "Treevox does not yet support branchwood biomass distribution.","suggestion": "Request foliage biomass outputs for now, or wait for fastfuels-core branchwood/fine component support."},"chunks": null,"tags": []}Request foliage bands only.
-
Dead trees. Only live trees are voxelized. A tree whose
fia_status_codeis anything other than 1 (live) is left out; a tree with no status counts as live. To represent dead foliage, use the live and dead split above. -
Source inventory not
completed. Poll the inventory tocompletedbefore you voxelize it. -
Modifying the voxel grid. 3D grids don’t support modifications or resampling. To remove trees, apply the change to the inventory, then voxelize the result.
-
Measured crowns after the CHM grid is deleted. A
crown_featuresource reads the crowns’ tops from the CHM grid the Feature was made from. If that grid no longer exists, the request returns a422. Create the crown Feature again from a current CHM grid. -
Expecting identical voxels without a seed. Which voxels each crown fills is drawn at random. If you omit
seed, each run draws a new realization. The seed used is always recorded insource.seed.