Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions docs/v2/guides/creating-grids.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm40(
)
```

To pin a LANDFIRE vintage, pass `version` (`"2024"` or `"2025"`); it defaults
to the API's current version. A completed grid reports the calendar year its
fuels represent on `grid.represented_year`:

```python
grid = ff.grids.create_fuel_model_grid_from_landfire_fbfm40(
domain, version="2024", output_resolution_m=30
)
```

```python
>>> grid.represented_year
2024
```

### Look up fuel parameters from FBFM40 codes

The `fbfm` band holds codes, not the quantities a fire model consumes. To
Expand Down Expand Up @@ -239,11 +254,60 @@ Use `output_resolution_m` or `align_to` to choose a different lattice. See the
[Point clouds guide](point-clouds.md#create-a-point-cloud-from-usgs-3dep) to
create an airborne point cloud from USGS 3DEP.

To control how each cell reduces its above-ground returns to a height, set
`aggregation` to `"max"` (the default), `"mean"`, `"median"`, or
`"percentile"`; `"percentile"` requires a `percentile` (0-100). The
`spike_filter` argument removes lone spurious returns: leave it unset for the
API's default filter, pass `False` to keep every return, or pass a mapping of
thresholds to tune it:

```python
chm = ff.grids.create_canopy_height_grid_from_point_cloud(
point_cloud,
output_resolution_m=2,
aggregation="percentile",
percentile=95,
spike_filter={"min_canopy_footprint_m": 5, "min_prominence_m": 30},
)
chm.wait()
```

!!! tip "NAIP-CHM is a surface model"
NAIP-CHM is a digital surface model and retains buildings and other
infrastructure. To keep only vegetation, mask out built-up areas — see
[Mask out features](#mask-out-features).

### Derive canopy fuels from a tree inventory

To compute the crown-fire bands directly from a completed
[tree inventory](inventories.md) rather than from LANDFIRE, pass the inventory
to `create_canopy_fuel_grid_from_inventory`. It derives canopy bulk density
(`cbd`), canopy base height (`cbh`), canopy height (`chm`), canopy cover
(`cc`), and canopy fuel load (`cfl`) from the inventory's trees; only live
trees contribute. The bands share keys and units with the LANDFIRE canopy
source, and the output defaults to 30 m cells:

```python
canopy = ff.grids.create_canopy_fuel_grid_from_inventory(
inventory,
bands=["cbd", "cbh", "chm", "cc", "cfl"],
output_resolution_m=30,
)
canopy.wait()
```

```python
>>> {band.key for band in canopy.bands}
{'cbd', 'cbh', 'chm', 'cc', 'cfl'}
```

The default four bands (`cbd`, `cbh`, `chm`, `cc`) drop `cfl`; pass `bands`
to choose. Method keyword arguments tune how each band is derived — the
biomass source (`biomass_equations` or `biomass_column`), and per-band
reduction methods (`cbd`, `cbh`, `chm`, `cc`), among others. See
`create_canopy_fuel_grid_from_inventory` in the [Reference](../reference.md)
for the full surface. Pass `align_to` to match another grid's lattice exactly.

## 3D tree fuel grids (voxelization)

The 3D canopy fuel grid — per-voxel bulk density, the input 3D fire models
Expand Down Expand Up @@ -347,6 +411,120 @@ Use a `value` target to set every occupied cell to a constant, `max` with an
optional `min` to scale by extrema, or `mean` and `sd` to scale by moments.
Omit `calibration` only when you want the raw DUET values.

## Solar irradiance grids

!!! warning "Beta"
LeafLux irradiance grids are a new v2 grid family and still stabilizing.

To compute solar irradiance through the canopy at a single instant, pass a
completed 3D grid carrying a `leaf_area_density` band — a voxel grid from
[voxelization](#3d-tree-fuel-grids-voxelization) — to
`create_irradiance_grid_from_leaflux` with a UTC `date_time`. Light is
attenuated through the leaf area density with a Beer-Lambert
`extinction_coefficient` (default 0.5). For what solar irradiance means, see
the [FastFuels documentation](https://docs.fastfuels.silvxlabs.com).

The `irradiance.canopy.relative` band needs no terrain:

```python
import datetime

irradiance = ff.grids.create_irradiance_grid_from_leaflux(
voxels,
date_time=datetime.datetime(2024, 7, 1, 18, 0, tzinfo=datetime.timezone.utc),
bands=["irradiance.canopy.relative"],
)
irradiance.wait()
```

The `irradiance.surface.relative` band (the default) drapes light onto the
ground, so it needs a terrain grid sharing the source grid's **exact
horizontal lattice** and carrying an `elevation` band. Build that terrain grid
with `align_to=<source grid>` and pass it as `source_terrain_grid`; without a
lattice-matched terrain grid the API returns a 422:

```python
topo = ff.grids.create_topography_grid_from_3dep(
domain, align_to=voxels, bands=["elevation"]
)
topo.wait()

irradiance = ff.grids.create_irradiance_grid_from_leaflux(
voxels,
date_time=datetime.datetime(2024, 7, 1, 18, 0, tzinfo=datetime.timezone.utc),
source_terrain_grid=topo,
bands=["irradiance.surface.relative"],
)
irradiance.wait()
```

See [Align grids to each other](#align-grids-to-each-other) for how `align_to`
matches a lattice exactly.

## Fuel moisture grids

!!! warning "Beta"
Fosberg dead fuel moisture grids are a new v2 grid family and still
stabilizing.

To estimate 1-hour dead fuel moisture with the Fosberg model, pass a
topography grid and a surface-irradiance grid to
`create_dead_fuel_moisture_grid_from_fosberg` along with the weather inputs.
The result carries a single `fuel_moisture.dead.1hr` band (percent). For the
Fosberg 1-hr dead fuel moisture model itself, see the
[FastFuels documentation](https://docs.fastfuels.silvxlabs.com).

The topography, leaf-area-density, and surface-irradiance grids all feed the
same Fosberg cells, so they must land on **one shared lattice** — build the
topography and irradiance grids against the voxel grid with `align_to=`. The
topography grid supplies `slope` and `aspect` to Fosberg and `elevation` to
the irradiance surface draping:

```python
import datetime
import fastfuels_sdk.v2 as ff

# A voxel grid carrying leaf area density (see 3D tree fuel grids, above).
voxels = inventory.voxelize(
horizontal_resolution_m=2, vertical_resolution_m=1, bands=["leaf_area_density"]
)
voxels.wait()

topo = ff.grids.create_topography_grid_from_3dep(
domain, align_to=voxels, bands=["elevation", "slope", "aspect"]
)
topo.wait()

irradiance = ff.grids.create_irradiance_grid_from_leaflux(
voxels,
date_time=datetime.datetime(2020, 7, 1, 18, 0, tzinfo=datetime.timezone.utc),
source_terrain_grid=topo,
bands=["irradiance.surface.relative"],
)
irradiance.wait()

moisture = ff.grids.create_dead_fuel_moisture_grid_from_fosberg(
topo, irradiance,
dry_bulb_temp=75.0, # degrees Fahrenheit, >= 10
relative_humidity=20.0, # percent, 0-100
time=1200, # local HHMM, 0800-1959
month="July",
elevation="near", # Fosberg correction category: below/near/above
)
moisture.wait()
```

```python
>>> [band.key for band in moisture.bands]
['fuel_moisture.dead.1hr']
```

!!! warning "Grids must share one lattice"
Passing topography, leaf-area-density, and irradiance grids that sit on
different lattices is the most common failure. Build the topography and
irradiance grids against the voxel grid with `align_to=` — see
[Align grids to each other](#align-grids-to-each-other).

## Uniform grids

To fill the whole domain with constant band values at a chosen resolution —
Expand Down
66 changes: 66 additions & 0 deletions docs/v2/guides/point-clouds.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,72 @@ Point clouds are addressed by their domain and id:
pc = ff.get_point_cloud(domain, "4a56bae0cd5e481aa1617cb894a9a7f3")
```

## Read point cloud data

To read a completed point cloud's points into memory, call `to_numpy()` or
`to_dataframe()`; both page over every occupied tile and stack them into one
array or frame. `to_numpy()` returns an `(N, k)` `float64` array of the
requested `columns`, defaulting to the `X`/`Y`/`Z` coordinates:

```python
pc = ff.get_point_cloud(domain, "4a56bae0cd5e481aa1617cb894a9a7f3").wait()

points = pc.to_numpy()
```

```python
>>> points.shape
(48213, 3)
```

`to_dataframe()` returns one row per point and one column per stored attribute
(`X`, `Y`, `Z`, `classification`, and any source columns such as `intensity`):

```python
df = pc.to_dataframe()
```

```python
>>> df.columns.tolist()
['X', 'Y', 'Z', 'classification']
```

To read fewer points, narrow the output with these arguments (shared by both
methods):

- **`lod`** — an inclusive level-of-detail ceiling. `0` is the coarsest sample
and each higher value adds finer points; omit it to read every point. Valid
values are `0` through `lod_levels - 1` (see `metadata()` below).
- **`classes`** — ASPRS classification codes to keep, e.g. `[2, 5]` for ground
and high vegetation.
- **`columns`** — the stored columns to read, in the returned order.
- **`decode_coordinates`** — `True` (default) decodes `X`/`Y`/`Z` to CRS
coordinates; `False` keeps them as the stored scaled integers.

```python
# Ground returns only, as CRS coordinates.
ground = pc.to_dataframe(classes=[2], columns=["X", "Y", "Z", "classification"])
```

To inspect the tile index — occupied tiles, stored columns and dtypes, the
coordinate encoding, and the point count at each level of detail — without
downloading any points, call `metadata()`:

```python
meta = pc.metadata()
```

```python
>>> meta.lod_levels
5
>>> len(meta.tiles)
4
```

!!! tip "Wait before reading"
`metadata()`, `to_numpy()`, and `to_dataframe()` require a completed point
cloud; call `wait()` first or they raise `ValueError`.

## List point clouds

To list your point clouds, optionally narrowed to a domain, a scan type, a
Expand Down
Loading