Skip to content

sylva.fusion

TLS and ALS together: registration of a plot on a survey, linked trees, merged clouds and profiles, and upscaling of plot values. See TLS and ALS together for the methods and their validation.

sylva.fusion

Terrestrial and airborne lidar together.

A TLS plot measures stems, diameters and the lower canopy in detail; an airborne survey measures the upper canopy and the terrain over the whole landscape. This module puts the two in one frame and combines them:

  • register places a TLS plot (in a local frame, or georeferenced with a GNSS error of a few metres) on the ALS survey, and reports the residuals and their uncertainty;
  • link_trees links TLS stems to ALS trees, reports the stems under another tree's crown, and gives one table of TLS diameters and ALS heights;
  • merge_clouds and fuse_profiles give one point cloud and one plant area density profile from both instruments;
  • plot_values, fit_model and upscale carry plot values from the TLS over the ALS survey with a regression on area-based metrics;
  • synthetic_scan is a terrestrial scanner for synthetic scenes that sees them as sylva.synthetic.als_flight does, so both instruments can be checked against one known forest.

The computations are in the Rust core (sylva_rs::fusion).

TLS trees linked to ALS trees, from link_trees.

Attributes:

Name Type Description
tls dict

The TLS trees as a table: tree_id, x, y (in the ALS frame), dbh, height (TLS), volume.

als dict

The ALS trees as a table: id, x, y, height and crowns when given.

status ndarray

Per TLS tree: matched (it is the ALS tree), suppressed (under the crown of a taller ALS tree, which the ALS cannot see past), codominant (under an ALS crown whose tree is another stem of about the same height: the ALS merged two canopy trees) or unlinked (under no ALS crown and near no top).

als_index ndarray

Index into als of the tree each TLS tree is linked to or stands under; -1 for none.

distance ndarray

Stem to that tree's top (m).

inside ndarray

Whether the stem is inside that crown.

als_height ndarray

That ALS tree's height.

height ndarray

Combined height: for a matched tree the ALS height, unless the TLS is known to have seen the top and measured it taller; the TLS height otherwise.

height_source ndarray

"als", "tls" or "none".

top_seen ndarray

Whether the TLS saw the top: 1, 0 or NaN (unknown). Given, or for a matched tree inferred from the TLS height reaching the ALS height.

flag ndarray

top_seen; als_height (TLS top not seen, ALS height used); top_not_seen (not seen and no ALS height: the height is a lower bound); top_unknown.

cost ndarray

Assignment cost of each matched tree.

als_tls ndarray

Per ALS tree, the index of its matched TLS tree (-1 for none).

als_stems list

Per ALS tree, the indices of every TLS tree linked to it or under it.

settings dict

The settings of the call.

counts

counts() -> dict

Numbers of TLS trees by status, of ALS trees with a matched stem and without, and of ALS crowns holding two stems or more.

one_to_many

one_to_many() -> dict

ALS trees with more than one TLS stem under them.

Returns:

Type Description
dict

ALS tree id to the TLS tree ids under it, the matched one first.

table

table() -> dict

One row per TLS tree: its TLS measurements, its link and the combined height.

Returns:

Type Description
dict

tree_id, x, y, dbh, height_tls, volume, status, als_id (-1 for none), als_height, distance, inside, height, height_source, top_seen, flag.

als_table

als_table() -> dict

One row per ALS tree: id, x, y, height, tls_id (its matched TLS tree, -1 for none), n_stems (TLS stems linked to it or under it) and stems (their ids).

to_csv

to_csv(path: str | Path) -> None

Write table as CSV (empty cells for NaN).

Parameters:

Name Type Description Default
path str | Path

Output file.

required

FusedProfile dataclass

A plant area density profile from both instruments, from fuse_profiles.

Attributes:

Name Type Description
height ndarray

Bottom of each height bin above ground (m).

pad ndarray

Fused plant area density (m² m⁻³); NaN where neither saw the bin.

weight_tls, weight_als

Weight of each instrument in each bin (summing to 1).

tls, als

Each instrument's bins: pad, beams (mean pulses entering a voxel of the bin, unreached voxels counting 0), observed (share of the bin's voxels with at least min_beams pulses) and n_voxels.

split_height float

Lowest height above which the ALS carries at least half the weight in every bin: where merge_clouds can switch instruments.

bin_size float

Bin height (m).

settings dict

The settings of the call.

pai

pai(source: str = 'fused') -> float

Plant area index: the sum of the PAD of the bins times their height.

Parameters:

Name Type Description Default
source ('fused', 'tls', 'als')

Which profile. Bins without a value count as 0.

"fused"

Returns:

Type Description
float

table

table() -> dict

The profile as columns: height, pad, weight_tls, weight_als, pad_tls, pad_als, beams_tls, beams_als, observed_tls, observed_als.

Registration dataclass

A TLS plot placed on an ALS survey, from register.

Attributes:

Name Type Description
transform ndarray

(4, 4) matrix taking TLS coordinates into the ALS frame (initial included).

search_transform ndarray

The same after the search, before the ICP refinement: the best peak.

pivot ndarray

Centre of the TLS ground after initial (ALS frame): the point the heading turns about, and to which shift and the uncertainties refer.

heading float

Rotation about the vertical (degrees, counter-clockwise) added to initial.

shift ndarray

Translation (m) of the pivot added to initial.

candidates dict

The peaks of the search, best first, refined: heading, dx, dy, score (the combined correlation), chm_r and dtm_r (Pearson's r of the canopy and terrain models) and overlap (TLS canopy cells on ALS data).

ambiguity float

Misfit (1 - score) of the best peak over that of the second best: 0 for a clear answer, near 1 when another pose fits about as well (NaN with one peak).

icp dict | None

Summary of the kept ICP run (fitness, rmse, n, iterations, converged, accepted, shift and turn from its peak, and start, the index of that peak in candidates), or None without refinement.

covariance ndarray | None

Formal (6, 6) covariance of [rx, ry, rz, tx, ty, tz] (radians, m) at the pivot, from the ICP's point-to-plane information. It treats every residual as independent, so it is a lower bound.

jackknife ndarray | None

Jackknife standard errors of the pivot's x, y, z (m) and the heading (degrees) over the four quadrants of the plot, or None.

residuals dict

ground_n, ground_median, ground_nmad, ground_rmse (TLS ground minus the ALS DTM, m) and canopy_n, canopy_median, canopy_p90 (distance of TLS canopy points to the nearest ALS canopy return, m).

n_tls, n_als

Points used (the ALS within reach of the plot).

resolution, coarse_resolution

Cell sizes (m) of the fine and coarse search, after widening for a sparse survey.

als_density float

ALS returns per m² near the plot.

settings dict

The settings of the call.

std property

std: dict

Standard errors of the pivot's x, y, z (m) and the heading (degrees): the larger of the formal and the jackknife ones, NaN where neither exists.

apply

apply(cloud: PointCloud) -> PointCloud

The TLS cloud moved into the ALS frame.

Parameters:

Name Type Description Default
cloud PointCloud

Points in the TLS frame given to register.

required

Returns:

Type Description
PointCloud

to_dict

to_dict() -> dict

Everything as plain lists and numbers, for JSON.

save

save(path: str | Path) -> None

Write to_dict as JSON (NaN written as null).

Parameters:

Name Type Description Default
path str | Path

Output file.

required

load classmethod

load(path: str | Path) -> Registration

Read a registration written by save.

Parameters:

Name Type Description Default
path str | Path

JSON file.

required

Returns:

Type Description
Registration

report

report() -> str

A short text summary of the pose, its checks and its uncertainty.

Model dataclass

A regression of plot values on ALS metrics, from fit_model.

Attributes:

Name Type Description
form str

"linear" (y = b0 + Σ bk xk) or "loglog" (ln y = b0 + Σ bk ln xk).

names list

Predictor names, in the order of coef[1:].

coef, se

Coefficients (intercept first) and their standard errors, on the model's scale.

sigma, df

Residual standard error (model scale) and its degrees of freedom.

n int

Plots.

r2, adj_r2

Coefficient of determination on the model's scale, and adjusted.

xtx_inv ndarray

(XᵀX)⁻¹ for prediction variances.

correction float

Back-transformation factor exp(σ²/2) of a loglog model (Baskerville 1972); 1 for linear.

y, fitted, loo

Plot values, fitted values and leave-one-out predictions (scale of y).

loo_rmse, loo_bias, loo_r2, loo_rrmse

Leave-one-out RMSE, mean error (prediction minus observation), 1 - PRESS / SS and RMSE as a percentage of the mean of y.

x_min, x_max

Range of each predictor over the plots.

equation

equation() -> str

The fitted model as text.

summary

summary() -> str

Coefficients, fit and leave-one-out statistics as text.

predict

predict(predictors, level: float = 0.95) -> dict

Predict at new predictor values.

Parameters:

Name Type Description Default
predictors

A dict of arrays or of ~sylva.Raster (one grid) named as the model's predictors, or a (n, k) array in their order.

required
level float

Coverage of the prediction interval.

0.95

Returns:

Type Description
dict

mean (for loglog, exp(ŷ + σ²/2)), se (standard error of a new observation; for loglog that of the log-normal distribution), lower, upper (the t interval, for loglog taken back from the log scale) and extrapolated (a predictor outside the plots' range). Rasters when the predictors are rasters (extrapolated as 0 and 1, NaN where there is no prediction), arrays otherwise. Missing or, for loglog, non-positive predictors give NaN.

Upscaling dataclass

A model and its wall-to-wall prediction, from upscale.

Attributes:

Name Type Description
model Model

The fitted Model.

mean, se, lower, upper

Rasters of the prediction, the standard error of a new observation and the interval at level.

extrapolated Raster

Raster: 1 where a predictor lies outside the range of the plots, 0 elsewhere, NaN without a prediction.

level float

Coverage of the interval.

to_ascii_grids

to_ascii_grids(directory: str | Path, prefix: str = '') -> list[str]

Write the rasters as ESRI ASCII grids.

Parameters:

Name Type Description Default
directory str | Path

Output directory (created if needed).

required
prefix str

Prepended to mean.asc, se.asc, lower.asc, upper.asc and extrapolated.asc.

''

Returns:

Type Description
list of str

The files written.

link_trees(tls_trees, als_trees, transform=None, *, volumes=None, top_seen=None, sampling=None, min_above_observed: float = 0.5, max_distance: float = 3.0, crown_buffer: float = 0.5, height_weight: float = 2.0, dbh_weight: float = 2.0, max_cost: float = 2.0, top_tolerance: float = 1.0) -> TreeLinks

Link TLS stems to ALS trees and combine their measurements.

A TLS tree and an ALS tree can be one tree when the stem lies inside the ALS crown (or within crown_buffer of its outline) or within max_distance of its top. Among these pairs an optimal assignment (Kuhn 1955; Munkres 1957) minimises the total cost, a stem left without an ALS tree costing max_cost. A pair costs::

(d / D)² + height_weight * dh² + dbh_weight * (1 - dbh / dbh_max)²

with d the stem-to-top distance, D the larger of max_distance and the crown's equivalent radius, dh = (h_als - h_tls) / h_als (0.3 for a stem without a TLS height) and dbh_max the largest DBH among the stems that could be that ALS tree. The stem that is tallest by the TLS and thickest under a crown is therefore its tree, as the dominant tree of a crown usually is both. Each ALS tree gets at most one stem, and the others under its crown are reported with it as suppressed (shorter by more than top_tolerance and 10 %) or codominant (about as tall: the ALS saw two canopy trees as one). TreeLinks.one_to_many lists these crowns.

The combined table keeps the TLS diameter and takes the height from the instrument that saw the top. Whether the TLS saw a tree's top comes from top_seen, from sampling (sylva.voxels.tree_sampling: seen when above_observed_fraction is at least min_above_observed), or, for a matched tree, from its TLS height reaching the ALS height within top_tolerance. A matched tree takes the ALS height unless the TLS is known (from top_seen or sampling) to have seen the top and measured it taller: both are lower bounds, the ALS for missing the apex and the TLS for occlusion, but a TLS height can also be too tall where its segmentation gave the tree part of a neighbour's crown. Other trees keep the TLS height, flagged top_not_seen when that is a lower bound.

Parameters:

Name Type Description Default
tls_trees

sylva.trees.Tree objects (after sylva.trees.tree_heights), or a table (dict of arrays) with x, y and optionally tree_id, dbh, height, volume, z, in the TLS frame.

required
als_trees

sylva.als.Trees (with crowns), or a table with x, y, height and optionally id and crowns (a list of (k, 2) outlines).

required
transform

Registration or (4, 4) matrix taking the TLS into the ALS frame; the trees are taken to share a frame if None. Stem positions are moved at z (0 if not given).

None
volumes

Wood volume (m³) per TLS tree: a sylva.qsm.PlotQSMs, a mapping of tree id to volume, or an array in the order of the trees.

None
top_seen

Per TLS tree, whether its top was seen (booleans, or 1, 0 and NaN).

None
sampling

Output of sylva.voxels.tree_sampling (matched by tree_id), in place of top_seen.

None
min_above_observed float

above_observed_fraction from which a top counts as seen.

0.5
max_distance float

Farthest a stem can be from an ALS top (m) to be its tree outside the crown outline, and the scale of the distance cost.

3.0
crown_buffer float

Distance (m) outside a crown outline that still counts as under it.

0.5
height_weight float

Weight of the relative height difference in the cost.

2.0
dbh_weight float

Weight of the DBH shortfall from the thickest candidate stem.

2.0
max_cost float

Cost of leaving a stem without an ALS tree; no pair costing more is made.

2.0
top_tolerance float

Height difference (m) within which a TLS tree reaches the ALS height.

1.0

Returns:

Type Description
TreeLinks

Raises:

Type Description
ValueError

For a non-finite position, mismatched columns or a setting out of range.

Examples:

>>> links = fusion.link_trees(stems, als_trees, transform=reg, volumes=qsms)
>>> links.one_to_many()                          # {als id: [tls ids]}
>>> links.to_csv("trees_fused.csv")

fuse_profiles

fuse_profiles(tls_grid, als_grid, dtm: Raster | None = None, area=None, *, bin_size: float | None = None, min_beams: float = 5.0, estimator: str = 'mean', g: float = 0.5, pad_field: str | None = None, mode: str = 'beams', min_observed: float = 0.0) -> FusedProfile

One plant area density profile from TLS and ALS voxels.

Each grid is summarised per bin of height above dtm over area: PAD, the mean number of pulses entering a voxel (voxels no pulse reached count 0, so occlusion lowers it) and the share of voxels crossed by at least min_beams pulses. Each bin then takes its PAD from both instruments, weighted by

  • "beams" (the default): the pulses, since the variance of a gap-fraction estimate falls in proportion to the beams that sampled it. Near the ground the TLS dominates; high in a closed canopy, where its pulses have been stopped, the ALS takes over;
  • "observed": the share of the bin each instrument saw;
  • "best": the one with more pulses alone.

An instrument whose bin is seen less than min_observed gets no weight there. The weights are reported per bin.

Both grids must be in one frame (move the TLS shots with sylva.Shots.transform and a Registration before voxelising) and should share the voxel size.

Parameters:

Name Type Description Default
tls_grid

sylva.voxels.RayVoxelGrid of the TLS.

required
als_grid

sylva.als.ALSVoxels (or a RayVoxelGrid) of the ALS.

required
dtm Raster | None

Terrain (e.g. the ALS DTM); heights above the grid floor if None.

None
area

(xmin, ymin, xmax, ymax), (x, y, radius) or a (k, 2) polygon: the columns (by their centres) to summarise; every column of each grid if None. Use the plot, so that both describe the same ground.

None
bin_size float | None

Height bin (m); the larger voxel size if None.

None
min_beams float

Pulses a voxel needs to count as observed; voxels crossed by fewer give noisy estimates that are biased upwards.

5.0
estimator ('mean', 'pooled')

"mean": the mean of pad_field over the observed voxels of the bin, each voxel's own estimate, so a crown and the gap beside it count by their volume. "pooled": Σ num_hits_weighted / (g Σ free_path_length) over the observed voxels, a ratio of sums that is not biased upwards by poorly sampled voxels but weights each voxel by the pulses that reached it; where the canopy is clumped the shadowed crowns get fewer pulses than the gaps and the density reads low (30 % on the validation stands), so use it for layers that are horizontally uniform.

"mean"
g float

Leaf projection G of the pooled estimator (0.5: spherical).

0.5
pad_field str | None

PAD field of the mean estimator; "pad_fpl" if None.

None
mode ('beams', 'observed', 'best')

Weighting.

"beams"
min_observed float

Share of a bin an instrument must have seen for its PAD to count.

0.0

Returns:

Type Description
FusedProfile

Raises:

Type Description
ValueError

For a grid without the fields asked for, or bad settings.

merge_clouds

merge_clouds(tls: PointCloud, als: PointCloud, dtm: Raster, split=5.0, transform=None, fallback: float | None = None) -> PointCloud

One cloud from both instruments: TLS below the split, ALS above.

Parameters:

Name Type Description Default
tls PointCloud

The TLS points (in the ALS frame, or moved there by transform).

required
als PointCloud

The ALS returns.

required
dtm Raster

Terrain for the heights of both, in the ALS frame.

required
split

Height above ground (m) where the ALS takes over: a number, a ~sylva.Raster of heights (one per column; NaN cells and points off the grid take fallback), or a FusedProfile, whose FusedProfile.split_height is used.

5.0
transform

Registration or (4, 4) matrix applied to tls first.

None
fallback float | None

Split height where a split raster has no value; the raster's median if None.

None

Returns:

Type Description
PointCloud

TLS points lower than the split, then ALS returns at or above it, with source (uint8: 1 TLS, 2 ALS), height above the DTM and the attributes both clouds have. The CRS is the ALS cloud's.

Raises:

Type Description
ValueError

For a bad split or transform.

register

register(tls: PointCloud, als, initial=None, *, resolution: float = 0.5, returns_per_cell: float = 10.0, coarse_resolution: float = 2.0, search_radius: float = 10.0, heading_range: float = 180.0, heading_step: float = 3.0, dtm_weight: float = 0.5, min_height: float = 2.0, min_overlap: float = 0.5, n_candidates: int = 3, refine: str = 'all', icp_voxel_sizes=(1.0, 0.5, 0.25), icp_max_distances=(2.0, 1.0, 0.5), min_planarity: float = 0.3, max_refine_shift: float = 3.0, max_refine_turn: float = 8.0, jackknife: bool = True, tls_ground=None, als_ground=None) -> Registration

Register a TLS plot onto an ALS survey.

Search. The canopy height model (CHM) and terrain model (DTM) of each cloud are made on one grid spacing, from its ground points (classification 2). The TLS cells are turned by a heading and shifted over the ALS rasters, and each pose is scored by the correlation (Pearson's r) of the two CHMs plus dtm_weight times that of the two DTMs, over the cells both measured. A correlation is blind to a vertical offset between the terrains and to a TLS canopy that reads low where the scanner saw less of the crowns. Every heading within heading_range of the initial one (heading_step apart) and every shift within search_radius (coarse_resolution apart) is scored; the n_candidates best separated peaks are searched again at resolution with bilinear sampling and a parabola through the best score in heading, x and y, and the vertical offset is the median difference of the two DTMs. For a sparse survey the fine cells are widened to hold about returns_per_cell returns each (1 m at 10 returns per m²): a CHM of cells with one or two returns is too ragged to place a plot by.

Refinement. A robust point-to-plane ICP (Huber weights, trimmed tail; the ICP of sylva.coreg) moves the TLS points onto the ALS points (refine="all": ground, stems and crowns; "ground": the ground alone; "none": no refinement), over the voxel pyramid icp_voxel_sizes, from each refined peak. A run counts only if it moves less than max_refine_shift and max_refine_turn from its peak; of those the best fit is kept (the most TLS points within the last cut-off of an ALS point; the higher peak when two fit within half a percent). The 3-D fit of ground and crowns tells apart poses that the canopy models cannot: the TLS sees the crowns from below and to the side, so the best correlation of the canopy models can lie a few degrees or metres off, most of all with one or two scan positions.

Uncertainty. The formal covariance of the ICP (sigma² H⁻¹ of its point-to-plane information: a lower bound, since the residuals are not independent) and a jackknife over the four quadrants of the plot (each left out and the ICP run again). The residuals of the TLS ground against the ALS DTM and of the TLS canopy against the nearest ALS returns check the result without any reference.

Everything is computed about a pivot near the plot, so map coordinates of any size lose no precision.

Parameters:

Name Type Description Default
tls PointCloud

The TLS plot, with ground classified (or tls_ground).

required
als

The ALS returns with ground classified (or als_ground), as a ~sylva.PointCloud, or a catalogue (anything sylva.als.catalog accepts), of which only the box around the plot is read.

required
initial

(4, 4) matrix placing the TLS roughly in the ALS frame: a GNSS position and a compass heading, or the identity (the default) for a plot already georeferenced.

None
resolution float

Smallest cell size (m) of the fine search and the residual DTM.

0.5
returns_per_cell float

ALS returns a fine cell should hold on average; 0 keeps resolution whatever the density.

10.0
coarse_resolution float

Cell size (m) of the search over the whole window.

2.0
search_radius float

Largest horizontal shift (m) searched, from the initial position. A few metres for a GNSS position; the uncertainty of a hand-held position otherwise.

10.0
heading_range float

Headings searched either side of the initial one (degrees). 180 (the default) searches the full circle, for a plot in a scanner frame; a few degrees for a georeferenced one.

180.0
heading_step float

Heading step (degrees) of the coarse search.

3.0
dtm_weight float

Weight of the terrain correlation against the canopy's. Terrain relief constrains a pose where the canopy is uniform.

0.5
min_height float

CHM heights below this (m) count as 0.

2.0
min_overlap float

Share of the TLS canopy cells that must fall on ALS data for a pose to be scored.

0.5
n_candidates int

Peaks of the coarse search refined.

3
refine ('all', 'ground', 'none')

Points the ICP uses.

"all"
icp_voxel_sizes

ICP voxel pyramid and correspondence cut-offs (m), coarse to fine.

(1.0, 0.5, 0.25)
icp_max_distances

ICP voxel pyramid and correspondence cut-offs (m), coarse to fine.

(1.0, 0.5, 0.25)
min_planarity float

Planarity an ALS point needs to be an ICP target (0 to 1).

0.3
max_refine_shift float

Largest horizontal move (m) and turn (degrees) the ICP may make from the search result.

3.0
max_refine_turn float

Largest horizontal move (m) and turn (degrees) the ICP may make from the search result.

3.0
jackknife bool

Run the quadrant jackknife (four more ICPs).

True
tls_ground

Boolean ground masks, in place of classification == 2 (als_ground only with an ALS point cloud).

None
als_ground

Boolean ground masks, in place of classification == 2 (als_ground only with an ALS point cloud).

None

Returns:

Type Description
Registration

Raises:

Type Description
ValueError

For bad settings, clouds without ground points, or no pose that puts enough of the TLS canopy on ALS data.

Examples:

>>> reg = fusion.register(tls, als_catalog, initial=gnss_pose, search_radius=5.0,
...                       heading_range=5.0)
>>> print(reg.report())
>>> tls_map = reg.apply(tls)

synthetic_scan

synthetic_scan(scene: PointCloud, origins=(10.0, 10.0, 1.5), resolution_deg: float = 0.25, max_zenith_deg: float = 130.0, target_radius: float = 0.03, terrain_slope: float = 0.05, min_range: float = 0.1, max_range: float = 200.0)

Scan a synthetic scene from the ground as sylva.synthetic.als_flight flies it.

Every scene point is a sphere of target_radius (points of class 2, the scene's ground, are ignored), and the ground is the analytic terrain of sylva.synthetic.terrain_height. Pulses leave the scanner on a regular zenith and azimuth grid as thin rays; a ray stops at the first sphere or the terrain, whichever it meets first, and gives one echo there, or none if it leaves the scene. A layer of spheres of n per m³ is then a turbid medium of plant area density 2 π r² n (spherical leaf angles) for both scanners, so their estimates can be checked against the same foliage. (sylva.synthetic.scan instead lets the points in each pulse's angular cell be hit, which suits geometry but not densities.)

Parameters:

Name Type Description Default
scene PointCloud

From sylva.synthetic.forest or sylva.synthetic.crown_forest.

required
origins

One scanner position (x, y, z) or an (n, 3) array.

(10.0, 10.0, 1.5)
resolution_deg float

Angular step in zenith and azimuth (degrees).

0.25
max_zenith_deg float

Pulses are fired from straight up to this zenith.

130.0
target_radius float

Sphere radius (m); use the target_radius of the flight.

0.03
terrain_slope float

Slope of the terrain (terrain_slope of the flight).

0.05
min_range float

Echoes nearer than min_range are ignored; rays are followed to max_range (m).

0.1
max_range float

Echoes nearer than min_range are ignored; rays are followed to max_range (m).

0.1

Returns:

Type Description
Shots or list of Shots

One per origin, every pulse fired (misses included), with the echo attributes classification (2 for the terrain) and tree_id when the scene has it.

Raises:

Type Description
ValueError

For settings out of range or an origin below the terrain.

fit_model

fit_model(y, predictors, model: str = 'loglog', names=None) -> Model

Regress plot values on ALS metrics.

Ordinary least squares, in one of two forms:

  • "loglog" (the default): ln y = b0 + Σ bk ln xk, the usual form for biomass against canopy height (a power law), with predictions taken back as exp(ŷ + σ²/2) (Baskerville 1972);
  • "linear": y = b0 + Σ bk xk.

Leave-one-out predictions come from the hat matrix in closed form (they equal refitting without each plot), and with them the RMSE, bias and R² a new plot can expect. Keep the predictors few (one or two for ten plots): each costs a degree of freedom, and correlated metrics (zq95 and zmax) add little.

Parameters:

Name Type Description Default
y

Plot values, e.g. the agb of plot_values per plot.

required
predictors

Per plot, the metrics: a dict of arrays, a sylva.als.PlotMetrics, or an (n, k) array.

required
model ('loglog', 'linear')

Model form.

"loglog"
names

The predictors to use (keys of predictors), or names for the columns of an array; every column if None.

None

Returns:

Type Description
Model

Raises:

Type Description
ValueError

With fewer than k + 2 plots for k coefficients, non-finite values, values that are not positive for loglog, or collinear predictors.

Examples:

>>> m = fusion.fit_model(agb, plot_metrics, "loglog", names=["zq95", "cover"])
>>> print(m.summary())

plot_values

plot_values(trees, area: float, wood_density: float, *, volumes=None, min_dbh: float = 0.0) -> dict

Plot totals per hectare from a plot's TLS trees.

Parameters:

Name Type Description Default
trees

sylva.trees.Tree objects, a table (dict of arrays) with dbh and optionally volume and tree_id, or a TreeLinks (its TLS trees).

required
area float

Plot area (m²). Give only the trees inside the plot.

required
wood_density float

Basic wood density (kg/m³), e.g. 500 to 700 for many hardwoods; there is no default, since it depends on the species.

required
volumes

Wood volume (m³) per tree, as for link_trees, if the trees do not carry a volume.

None
min_dbh float

Inventory threshold (m): smaller trees are left out.

0.0

Returns:

Type Description
dict

agb (Mg/ha: volume times wood density), volume (m³/ha), basal_area (m²/ha), stems (per ha), n_trees and missing_volume (trees counted without a volume).

Raises:

Type Description
ValueError

For a non-positive area or wood density.

upscale

upscale(y, plot_metrics, grid: Mapping, predictors, model: str = 'loglog', level: float = 0.95) -> Upscaling

Fit plot values on ALS metrics and predict them over the ALS grid.

fit_model on the plots, then Model.predict on every cell of grid. The grid's cells should have the plots' area (e.g. 25 m cells for 0.06 ha plots), since metrics such as the height percentiles depend on the area they are computed over.

Parameters:

Name Type Description Default
y

Plot values (e.g. agb from plot_values), one per plot.

required
plot_metrics

The plots' ALS metrics: sylva.als.PlotMetrics (from sylva.als.plot_metrics) or a dict of arrays.

required
grid Mapping

The same metrics as rasters on one grid (from sylva.als.grid_metrics), by name.

required
predictors

Names of the metrics to use.

required
model ('loglog', 'linear')

Model form.

"loglog"
level float

Coverage of the prediction interval.

0.95

Returns:

Type Description
Upscaling

Raises:

Type Description
ValueError

As fit_model, or for a grid without a predictor.

Examples:

>>> plots = als.plot_metrics(cat, centres, radius=15.0, metrics=["zq95", "cover"])
>>> grid = als.grid_metrics(cat, 25.0, ["zq95", "cover"])
>>> up = fusion.upscale(agb, plots, grid, ["zq95"], "loglog")
>>> up.mean.to_geotiff("agb.tif")