Skip to content

Derive canopy fuel grids from high-resolution CHM / point cloud #453

Description

@amarcozzi

Summary

Derive canopy fuel grid bands directly from high-resolution canopy data — a NAIP or Meta CHM grid, or a point cloud — summarized to a landscape-scale cell.

This is the second of two non-LANDFIRE paths to canopy fuels. The other is #452 (summarize a voxelized inventory profile). They are not interchangeable, and the difference is the whole design question here.

What each source can actually produce

Source CH CC CBH CBD
CHM raster (NAIP ~0.6 m, Meta ~1 m) yes yes no no
Point cloud (LAZ) yes yes maybe maybe
Voxelized inventory (#452) yes yes yes yes

A CHM is a surface, not a volume. It records the top of the canopy and nothing beneath it, so it cannot yield canopy base height or bulk density — both are properties of the vertical profile (Keane §4.3.1, p. 62). Any claim otherwise would be fabricating structure.

This bounds the issue honestly:

  • CHM -> CH + CC is a real, useful shortcut: no tree detection, no allometry, no voxelization. Fast, and defensible for the two variables it covers.
  • CHM -> CBH/CBD does not exist and must not be faked. That path is CHM -> ITD (/inventories/tree/chm, exists) -> allometry (exists) -> voxelize (exists) -> Derive canopy fuel grids (cbd/cbh/chm/cc) directly from a tree inventory — reproduce the FuelCalc/LANDFIRE method #452. Every piece but the last already ships.
  • Point cloud -> CBD/CBH is genuinely different: the returns carry real vertical structure, so a profile could be binned directly without inventing trees. But point clouds are stored as plain LAZ in the domain CRS and the API cannot read them (it's GDAL-free by design), so this needs a worker and is materially more work.

Proposed scope

Recommend splitting, and doing only the first now:

Phase 1 — CHM -> CC + CH (partial canopy grid)

POST /v2/domains/{domain_id}/grids/canopy/chm
{
  "source_grid_id": "<NAIP or Meta CHM grid>",
  "band": "chm",
  "cover_height_threshold": 2.0,
  "height_statistic": "p95",
  "alignment": {"target": "domain", "resolution": 30.0}
}
  • CC = fraction of source pixels within the output cell whose height exceeds cover_height_threshold (2 m is the common convention). This is the standard remote-sensing canopy cover and it's exactly what a fine CHM is good for.
  • CH = a per-cell statistic of the source heights. LANDFIRE CH is nominally "average height of dominant and codominant trees", which has no clean CHM analogue — hence height_statistic as an explicit parameter (p95 / mean / max / mean_top_decile) rather than a silent choice. Needs a decision on the default; p95 is a defensible starting point (max is noise-sensitive, mean is biased low by gaps).
  • Emits a canopy grid with only cc and chm bands — a legitimate partial canopy grid. The landscape export requires all four canopy roles, so users compose: CHM-derived cc/chm + Derive canopy fuel grids (cbd/cbh/chm/cc) directly from a tree inventory — reproduce the FuelCalc/LANDFIRE method #452-derived cbh/cbd, or fall back to LANDFIRE for the crown pair. Roles are per-{grid_id, band}, so mixing sources across bands already works with no export change.

Phase 2 — point cloud -> full profile (separate issue when wanted)

Bin returns into a vertical profile per column, then reuse #452's summarization. Needs a LAZ-reading worker; see the point-cloud storage notes (plain cloud.laz, domain CRS, 1 GiB cap).

On the 10 m / 30 m / 270 m question

Multi-resolution landscapes do not need new machinery. POST /grids/{grid_id}/resample already aggregates onto a target lattice with per-band method_overrides. What's missing is guidance, not code:

  • cc (%) — average
  • cbdaverage (a density; mass-conserving-ish)
  • chmaverage vs max is a real modeling choice
  • cbhaverage vs min; min is conservative for initiation

Worth deciding whether the resample endpoint's band-type-aware defaults should special-case canopy bands, or whether this is purely a docs matter. Coarsening to 270 m is then just a resample, and 10 m is only meaningful when the source is finer than 30 m — which is precisely what this issue and #452 unlock.

Work (phase 1)

  • fastfuels-core — cover fraction + height statistic over a window.
  • APIgrids/canopy/chm/ constructor, reusing the existing canopy cc / chm band definitions so the output is indistinguishable from a LANDFIRE canopy grid's equivalent bands.
  • griddle — handler; a windowed aggregation over the source zarr.
  • Validation — source grid 2D + completed; band exists with unit m; output resolution must be coarser than the source (a fractional-cover statistic over <1 source pixel per output cell is meaningless) -> 422.
  • Tests — core unit tests on synthetic CHMs with known cover fractions; griddle integration; API schema/router.

Open questions

  1. Default height_statistic (recommend p95).
  2. Is a two-band canopy grid (cc + chm) acceptable as a first-class resource, or should the endpoint require composition into a four-band grid? (Recommend allowing it — bands are already per-role in the export.)
  3. Should this and Derive canopy fuel grids (cbd/cbh/chm/cc) directly from a tree inventory — reproduce the FuelCalc/LANDFIRE method #452 be one /grids/canopy/... family with a discriminated source, or separate constructors? (Recommend separate, mirroring /meta, /naip, /landfire.)

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions