sylva.voxels¶
sylva.voxels ¶
Ray-traced voxel grids with Beer-Lambert statistics.
A port of rayvoxel (J. Rivory, unpublished), built on raycloudtools.
Every pulse is traced through the grid, and each voxel accumulates beam
counts, potential and free path lengths, beam sections and mean beam angles.
From these the attenuation coefficient is estimated by free path length
(fpl, bias corrected after Pimont et al. 2018), exact potential path length
(ppl), transmittance or Bailey & Mahaffee's (2017) eq. 10
(bailey), and divided by a projection function G to give plant, leaf
and wood area density. G comes from an analytic leaf angle distribution or,
with inclination=True, from per-tree inclination angle distributions
estimated on the echoes (Vicari et al. 2019).
sylva.canopy.density_grid is the lightweight alternative when only
hits and path lengths are needed.
RayVoxelGrid ¶
Voxel statistics from ray_voxelize.
The grid lives in the Rust core; arrays are copied out on first use and
cached, shaped (nz, ny, nx) like sylva.canopy.DensityGrid.
Raw accumulators (fields) and derived quantities (metrics,
plus attenuation_<m>, pad_<m>, lad_<m>, wad_<m> for any
attenuation method m) are available by name or as attributes::
grid["num_hits"]; grid.path_length; grid.pad_fpl; grid.transmittance
Notes
Main raw fields (sums over the pulses crossing each voxel):
| Field | Meaning |
|---|---|
num_beams |
pulses entering the voxel |
num_hits |
echoes in the voxel (num_hit_leaf, _wood, _plant by class) |
num_hits_weighted |
echoes weighted by their share of the pulse (weighting) |
num_beams_occluded |
pulses reaching it only after their last echo (occlusion=True) |
path_length |
potential path length: full chords of the entering pulses (m) |
free_path_length |
path actually travelled inside the voxel (m) |
bs_entering, bs_intercepted |
beam section entering and stopped (m², needs beam) |
wood_volume |
QSM wood (m³) after add_wood_volume |
Derived metrics:
| Metric | Meaning |
|---|---|
state |
0 unobserved, 1 occluded, 2 empty, 3 filled (STATES) |
attenuation_<m> |
attenuation λ (m⁻¹) by method m (see attenuation) |
pad_<m>, lad_<m>, wad_<m> |
plant, leaf, wood area density λ / G (m² m⁻³) |
pad_g0_5 |
plant area density with G = 0.5 |
pad_g_corrected |
PAD with G at the voxel's mean beam zenith, first attenuation method |
surface_area |
pad_g0_5 × voxel volume (m²) |
transmittance |
beam-section transmittance (0-1) |
mean_zenith_angle, mean_azimuth_angle |
mean beam direction (degrees) |
azimuth_concentration |
0 (all azimuths) to 1 (one azimuth) |
mean_laser_dist |
mean distance from the scanner (m) |
sd_path_length |
spread of the chord lengths (m) |
distance_from_ground |
voxel centre above the DTM (m, needs dtm) |
exploration_rate |
share of sub-voxels crossed (subvoxel_split) |
g_plant, g_leaf, g_wood |
G used per voxel |
wood_volume_density |
QSM wood volume per voxel volume (m³ m⁻³) |
fields
property
¶
fields: list[str]
Names of the raw accumulators this grid holds (depends on the options).
observed
property
¶
observed: ndarray
Boolean (nz, ny, nx): voxels crossed by at least one pulse before its last echo.
tree_iad
property
¶
tree_iad: dict[int, dict]
Per-tree inclination angle distributions: normalised liad /
wiad / piad histograms over bin_centres (rad), the
angle-integrated g_leaf / g_wood / g_plant and the closest
de Wit type of each. Empty unless inclination=True.
Returns:
| Type | Description |
|---|---|
dict
|
|
occlusion_profile ¶
occlusion_profile(min_height: float = 0.0, max_height: float | None = None) -> dict
What the scan saw of the canopy space, layer by layer.
The canopy space is every voxel from min_height above the ground
(distance_from_ground, needs a DTM; else above the grid floor) up
to max_height -- by default the highest layer holding a filled
voxel. Returns per layer the height of its centre, the voxel
count and the shares observed (a pulse went through or ended in
it), occluded (only pulses already stopped reached it; needs
occlusion=True) and unobserved, the mean pulses entering a
voxel (mean_beams), and plot totals under "total".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_height
|
float
|
Bottom of the canopy space (m above ground). |
0.0
|
max_height
|
float | None
|
Top of the canopy space; the highest filled voxel if None. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Arrays |
observed_map ¶
observed_map(min_height: float = 0.0, max_height: float | None = None) -> np.ndarray
Map of how much of each column's canopy space was observed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_height
|
float
|
Canopy space, as for |
0.0
|
max_height
|
float
|
Canopy space, as for |
0.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
z_levels ¶
z_levels() -> np.ndarray
Bottom z of each voxel layer.
Returns:
| Type | Description |
|---|---|
ndarray
|
Length |
centers ¶
centers() -> tuple[np.ndarray, np.ndarray, np.ndarray]
Voxel-centre coordinates.
Returns:
| Type | Description |
|---|---|
X, Y, Z : numpy.ndarray
|
Each |
profile ¶
profile(name: str = 'pad_fpl', min_beams: int = 1) -> np.ndarray
Mean of a metric per vertical layer over voxels crossed by at least
min_beams pulses (unobserved voxels do not count as empty).
Multiply by the voxel size and sum for a plant area index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Field or metric, e.g. |
'pad_fpl'
|
min_beams
|
int
|
Minimum pulses for a voxel to count. Raise it (5-10) to keep poorly sampled voxels out of the mean. |
1
|
Returns:
| Type | Description |
|---|---|
ndarray
|
One value per layer, bottom first; NaN where no voxel qualifies. Layers are in grid z, not height above ground. |
add_wood_volume ¶
add_wood_volume(qsms: QSM | Iterable[QSM]) -> None
to_dict ¶
to_dict(names: Iterable[str] | None = None) -> dict[str, np.ndarray]
Copy arrays out by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names
|
Iterable[str] | None
|
Fields and metrics to include; every raw field if None. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
write ¶
write(path: str | Path, format: str | None = None, include_unobserved: bool = False, filled_only: bool = False) -> int
Write a .vox voxel-space file, or a space-delimited .txt table
with voxel centres (format "vox" / "text", by default from
the extension). Observed and occluded voxels are written unless
filled_only or include_unobserved.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file; overwritten. |
required |
format
|
('vox', 'text')
|
From the extension if None ( |
"vox"
|
include_unobserved
|
bool
|
Also write voxels no pulse reached. |
False
|
filled_only
|
bool
|
Only write voxels holding echoes. |
False
|
Returns:
| Type | Description |
|---|---|
int
|
Number of voxels written. |
write_iad_csv ¶
write_iad_csv(path: str | Path) -> None
Write per-tree inclination distributions as CSV.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file: one row per tree with de Wit types and inclination
histograms. Needs |
required |
laser_spec ¶
laser_spec(name: str) -> tuple[float, float]
Beam geometry of a known scanner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Scanner name, e.g. |
required |
Returns:
| Type | Description |
|---|---|
diameter, divergence : float
|
Beam diameter at exit (m) and full divergence (rad). |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown scanner; pass |
leaf_projection ¶
leaf_projection(theta, lad: str = 'spherical', lad_params: Sequence[float] = ()) -> np.ndarray
Leaf projection function G(θ) of an analytic leaf angle distribution.
G is the mean projected leaf area per unit leaf area in the beam direction; area density is attenuation over G.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta
|
Beam zenith angle(s) in radians. |
required | |
lad
|
str
|
|
'spherical'
|
lad_params
|
Sequence[float]
|
Parameters of the ellipsoidal or beta distribution. The beta
distribution follows Goel and Strebel: on |
()
|
Returns:
| Type | Description |
|---|---|
ndarray
|
G for each angle. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown distribution or wrong parameters. |
See Also
sylva.leaves.LeafAngleDistribution.g : G from a measured distribution.
ray_voxelize ¶
ray_voxelize(shots: Shots | str | Path, voxel_size: float = 0.1, bounds=None, *, dtm: Raster | None = None, ground: ndarray | None = None, ground_class: int | None = None, ground_distance: float = 0.2, foliage: ndarray | None = None, leaf_classes: Sequence[int] = (), wood_classes: Sequence[int] = (), class_attr: str = 'classification', tree_attr: str | None = 'tree_id', intensity_attr: str = 'intensity', weighting: str = 'equal', attenuation: str | Sequence[str] = 'fpl', laser: str | None = None, beam: tuple[float, float] | None = None, lad: str = 'spherical', lad_params: Sequence[float] = (), inclination: bool = False, n_iad_bins: int = 18, knn_normal: int = 10, triangle_lmax: float = 0.05, occlusion: bool = False, flat_top: bool = False, neighbour_prior_min_rays: int = 0, subvoxel_split: int = 0, subvoxel_min_beams: int = 10, average_leaf_area: float = 0.005, unbounded_range: float = np.inf, block_size: int | Sequence[int] | None = None, out: str | Path | None = None, max_memory: float | None = None, workers: int | None = None)
Trace shots through a voxel grid (see the module docstring).
shots may be the path of a shots file (sylva.Shots.save). It
is then streamed a few row groups at a time, so memory stays at the size
of the grid plus a few million pulses; the grid defaults to the echo
bounds recorded in the file, and echo labels must come from attributes
(ground_class / dtm, leaf_classes / wood_classes,
tree_attr) rather than from ground / foliage arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shots
|
Shots | str | Path
|
Pulses in one frame (every scan of a plot together), or the path of
a shots file. Must include the misses: from
|
required |
voxel_size
|
float
|
Voxel edge (m). 0.1-0.5 m is usual; memory is about 0.25 kB per
voxel while tracing, so a 50 x 50 x 40 m plot at 0.1 m needs ~25 GB
unless it is traced in blocks ( |
0.1
|
bounds
|
|
None
|
|
dtm
|
Raster | None
|
Ground echoes are traced up to but never count as hits. Give them as a
boolean |
None
|
ground
|
Raster | None
|
Ground echoes are traced up to but never count as hits. Give them as a
boolean |
None
|
ground_class
|
Raster | None
|
Ground echoes are traced up to but never count as hits. Give them as a
boolean |
None
|
ground_distance
|
Raster | None
|
Ground echoes are traced up to but never count as hits. Give them as a
boolean |
None
|
foliage
|
ndarray | None
|
Per-echo codes ( |
None
|
leaf_classes
|
ndarray | None
|
Per-echo codes ( |
None
|
wood_classes
|
ndarray | None
|
Per-echo codes ( |
None
|
class_attr
|
ndarray | None
|
Per-echo codes ( |
None
|
tree_attr
|
str | None
|
Echo attribute grouping echoes into trees for the inclination distributions; all echoes are pooled as tree 0 when it is missing. |
'tree_id'
|
weighting
|
str
|
Share of a pulse carried by each echo: |
'equal'
|
attenuation
|
str | Sequence[str]
|
One or more of |
'fpl'
|
laser
|
str | None
|
A scanner name for |
None
|
beam
|
str | None
|
A scanner name for |
None
|
lad
|
str
|
Analytic leaf angle distribution, see |
'spherical'
|
lad_params
|
str
|
Analytic leaf angle distribution, see |
'spherical'
|
inclination
|
bool
|
Estimate inclination angle distributions from echo normals (PCA over
|
False
|
n_iad_bins
|
bool
|
Estimate inclination angle distributions from echo normals (PCA over
|
False
|
knn_normal
|
bool
|
Estimate inclination angle distributions from echo normals (PCA over
|
False
|
triangle_lmax
|
bool
|
Estimate inclination angle distributions from echo normals (PCA over
|
False
|
occlusion
|
bool
|
Also trace beyond each pulse's last echo ( |
False
|
flat_top
|
bool
|
Start the path in each column's top voxel at the highest echo. |
False
|
neighbour_prior_min_rays
|
int
|
Top up voxels crossed by fewer weighted beams from their 26 neighbours. |
0
|
subvoxel_split
|
int
|
|
0
|
subvoxel_min_beams
|
int
|
|
0
|
average_leaf_area
|
float
|
Mean leaf area (m²) of the effective free path correction for leaves of finite size (Pimont et al. 2018, 2019); 0 disables. |
0.005
|
unbounded_range
|
float
|
How far pulses without an echo are traced (default: to the grid edge). |
inf
|
block_size
|
int | Sequence[int] | None
|
Trace the grid block by block, a few blocks at a time: voxels per
block edge, or |
None
|
out
|
str | Path | None
|
Directory to write the blocks to as they finish; the grid is then
returned as a |
None
|
max_memory
|
float | None
|
GB the tracing accumulators of one pass may take (default: half of
|
None
|
workers
|
int | None
|
Threads for a blocked trace (default: all). Blocked results do not depend on it. |
None
|
Returns:
| Type | Description |
|---|---|
RayVoxelGrid or BlockedVoxelGrid
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
For inconsistent options ( |
KeyError
|
If a named echo attribute is missing. |
tree_sampling ¶
tree_sampling(grid: RayVoxelGrid, cloud, labels, min_beams: float = 10.0, above: float = 2.0) -> dict[str, np.ndarray]
How well each tree was seen, from a ray-traced grid built with
occlusion=True (so occluded voxels are told from unreached ones).
A tree's crown envelope is, voxel layer by voxel layer, the convex hull
of its points (labels per point, negative ignored). That envelope is
observed almost by construction -- the parts of a crown nobody saw left
no points -- so the informative columns are the pulses that reached it
and whether its top is real:
above_observed_fraction
share of the voxels up to above metres over the tree's highest
point, within the footprint of its top metre, that were observed
(empty, or filled by a neighbour). Low means the top may be hidden
and the tree taller than its points.
median_beams, p10_beams, beams_by_quarter
pulses entering an envelope voxel (unseen voxels count 0): overall,
the 10th percentile, and the median per quarter of the envelope's
height, bottom first.
well_sampled_fraction
observed envelope voxels with at least min_beams pulses.
Also tree_id, n_voxels and volume of the envelope and its
observed_fraction, occluded_fraction, unobserved_fraction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
RayVoxelGrid
|
Grid from |
required |
cloud
|
A |
required | |
labels
|
Tree id per point (e.g. from |
required | |
min_beams
|
float
|
Pulses for a voxel to count as well sampled. |
10.0
|
above
|
float
|
Height (m) above each tree's top that is checked. |
2.0
|
Returns:
| Type | Description |
|---|---|
dict
|
The columns above as arrays, one row per tree; ready for
|