Skip to content

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.

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

  2. A domain: your-domain-id.

  3. A completed tree inventory that carries dbh, crown_ratio, and fia_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.

POST to grids/voxelize/inventory/tree. Set these fields:

  • source_inventory_id: the completed inventory.
  • resolution: the voxel size in meters, horizontal for x/y and vertical for z.
  • bands: the outputs. This request asks for bulk_density.foliage.live, which is also the default.
  • biomass_source: estimates foliage mass from the nsvb allometric equations and counts all of it as live.
  • seed: fixes which voxels each crown fills, so the grid is reproducible.
POST grids/voxelize/inventory/tree
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/voxelize/inventory/tree' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Canopy fuel 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
}'

Record the grid id: your-tree-grid-id. Then poll until the status is completed:

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

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.

A 3D render of the whole Blue Mountain domain seen obliquely from the southwest. A tan hillshaded terrain surface slopes gently across the frame. Green voxel cubes stand on it wherever the canopy grid holds foliage: a dense stand covering most of the western half, a curved stand in the northeast, and open ground in the southeast. A horizontal color bar below reads live foliage bulk density in kilograms per cubic meter, from 0.00 to 0.99.

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.

Two panels. The top panel is a plan-view map of the domain, 1,308 metres wide and 884 metres tall, shaded green by foliage load per column in kilograms per square metre from 0 to about 4.5. Canopy fills most of the west and a curved band in the northeast, with bare ground in the southeast. A short pink line crosses the western stand at about 552 metres north, from x 310 to 410 metres. The bottom panel is a vertical section along that line: height from 0 to 37 metres against x from 310 to 410 metres, with blocky green crowns of different heights, the tallest reaching about 33 metres, and gaps inside and between crowns.

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 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 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:

Set crown_profile_model
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
}'

The completed grid records the model in source.crown_profile_model. For the shape of each model, see Crown profile models.

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.

Measured crown radius
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
}'

A tree with no value in the column gets its allometric radius.

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_id column, plus dbh, crown_ratio and fia_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 the tree_id of every tree it completes.
Measured crown shape
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
}'

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.

Set biomass_source.equations to jenkins to estimate foliage mass with the Jenkins et al. (2003) equations instead of the default nsvb:

Jenkins foliage 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 grid, Jenkins foliage",
"source_inventory_id": "your-inventory-id",
"resolution": { "horizontal": 2, "vertical": 1 },
"biomass_source": { "type": "allometry", "equations": "jenkins" },
"seed": 42
}'

For how the two sets of equations differ, see Foliage mass.

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:

10 % dead foliage
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
}'

The split is the same for every tree. In every voxel, the dead band holds one ninth of the live band’s value.

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:

Measured crown shape, capped at 2 kg/m**3
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
}'

Omit the field for no limit. For how much foliage a limit removes, see Capping bulk density.

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:

Leaf area density and tree IDs
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
}'

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.

Canopy bands for a QUIC-Fire export
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
}'

The QUIC-Fire tutorial wires them into the export’s canopy_bulk_density, canopy_moisture, and canopy_savr roles.

To use the canopy as a DUET surface-fuel source, request bulk_density.foliage.live, spcd, and fuel_moisture.live. DUET requires all three.

Canopy bands for DUET
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
}'

Continue with Build surface fuels from a tree grid with DUET.

  • Inventory missing morphology columns. Some inventories lack dbh, crown_ratio, or fia_species_code. For example, a CHM inventory carries only tree_id, x, y, height and, with crown segmentation, crown_radius. Voxelizing one returns a 422. 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.* and bulk_density.fine.* with a matching biomass_source, and returns 201. 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_code is 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 to completed before 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_feature source reads the crowns’ tops from the CHM grid the Feature was made from. If that grid no longer exists, the request returns a 422. 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 in source.seed.