Skip to content

sylva.synthetic

sylva.synthetic

Small synthetic scenes for examples, tutorials and tests.

Nothing here is a forest model; the shapes are simple enough that the right answer is known (stem positions, diameters, heights, leaf area), which is what an example needs.

ALSFlight dataclass

A simulated airborne survey, from als_flight.

Parameters:

Name Type Description Default
points PointCloud

The returns, in firing order, with the LAS attributes gps_time, return_number, number_of_returns, scan_angle, intensity, point_source_id (the flight line, from 1) and classification (what was hit: 2 ground, otherwise the class of the scene point), plus tree_id when the scene has it.

required
trajectory dict[str, ndarray]

The platform's path while the laser is on, as a table of equal length arrays: time (s, the same clock as gps_time), x, y, z (m), roll, pitch, heading (degrees) and line. Turns between lines are not included.

required
n_pulses int

Pulses fired, including those clipped away or with no return.

required

sensor_positions

sensor_positions(gps_time) -> np.ndarray

Where the scanner was when each pulse left.

Parameters:

Name Type Description Default
gps_time

Times, e.g. points.attrs["gps_time"].

required

Returns:

Type Description
ndarray

(N, 3) positions, interpolated linearly within the flight line being flown at each time. This is exact (to rounding), since lines are straight and flown at constant speed; NaN for times outside every line. A return lies on the ray from this position through the return, so (point - position) normalised is the beam direction.

write_tiles

write_tiles(directory, size: float = 50.0, format: str = 'laz', origin=None, epsg: int | None = None)

Write the returns as square LAS/LAZ tiles.

Parameters:

Name Type Description Default
directory

Where to write <xmin>_<ymin>.<ext> files.

required
size float

Tile side (m).

50.0
format str

"las" or "laz".

'laz'
origin

(x, y) of a tile corner; by default the minimum snapped down to a multiple of size.

None
epsg int | None

EPSG code to record in the files.

None

Returns:

Type Description
Catalog

The tiles written (point format 6, 1 mm quantisation).

ForestEpochs dataclass

Two scanned epochs of one synthetic plot and their truth; see forest_epochs.

Attributes:

Name Type Description
clouds list

The echoes of all scans of each epoch, with classification (2 ground, 4 leaf, 5 wood), tree_id (0 for ground; the same tree keeps its id in both epochs), branch_id (limb index within the tree, -1 on stems and ground), scan_id and the scan attributes of sylva.Shots.to_pointcloud. Epoch 2 is in its displaced frame.

shots list

The pulses of each epoch, misses included, in the same frames.

trees list

Per epoch, a table (dict of equal-length arrays) of the trees standing: tree_id, x, y, z0 (terrain at the stem), dbh (1.3 m above z0), height, stem_volume, wood_volume (stem and limbs), leaf_area and n_limbs. Both tables are in the frame of epoch 1.

changes list

The known changes, one dict each, with kind and tree_id: growth (d_dbh, d_height, d_stem_volume, shift), death and recruit (x, y, dbh, height, wood_volume; a recruit next to a felled tree has replaces), branch_removed (branch_id, volume, leaf_area, base and tip coordinates) and foliage_thinned (per tree leaf_area and discs; for tree_id -1 the box, fraction and total leaf_area).

transform ndarray

(4, 4) transform taking epoch 2 onto epoch 1: the registration an alignment should recover.

origins list

Scanner positions of each epoch, in its delivered frame.

range_noise tuple

Range noise (m) of each epoch.

of_kind

of_kind(kind: str) -> list[dict]

The changes of one kind.

Parameters:

Name Type Description Default
kind str

growth, death, recruit, branch_removed or foliage_thinned.

required

Returns:

Type Description
list of dict

ALSEpochs dataclass

Two airborne surveys of one synthetic forest; see als_epochs.

Attributes:

Name Type Description
scenes list

The scene of each survey (~sylva.synthetic.crown_forest points and, with buildings, roof points of classification 6). Unchanged trees keep their points; a grown tree is rebuilt taller.

flights list

The ~sylva.synthetic.ALSFlight of each survey, both in the true frame.

trees dict

One row per tree of the first survey (a table of equal-length arrays): tree_id, stem x, y, true height_a and height_b (NaN once removed, from ~sylva.synthetic.forest_trees), top_x, top_y and fate: unchanged, grown, removed (felled singly) or gap (felled with its neighbours to open the gap).

offset ndarray

(dx, dy, dz) added to the second survey's points when its tiles are written: the misalignment an alignment should recover.

gap tuple

(x, y, n): the gap was opened by felling the n trees nearest to (x, y).

growth float

Height added to the grown trees (m).

buildings list

(xmin, ymin, xmax, ymax) of each building.

write_tiles

write_tiles(directory_a, directory_b, size: float = 50.0, epsg: int | None = None)

Write both surveys as tiles, the second displaced by offset.

Parameters:

Name Type Description Default
directory_a

Output directories.

required
directory_b

Output directories.

required
size float

Tile side (m).

50.0
epsg int | None

EPSG code to record.

None

Returns:

Type Description
(Catalog, Catalog)

Plot dataclass

A plot from plot with its truth.

Attributes:

Name Type Description
points PointCloud

All points, with classification (2 ground, 3 understorey, 4 leaf, 5 wood and dead wood), label (1 ground, 2 stem, 3 branch, 4 leaf, 5 understorey, 6 dead wood; LABELS), tree_id (0 off the trees, then 1.. by decreasing DBH), branch_order (-1 off the trees), cylinder and leaf (rows of cylinders and leaves, -1 elsewhere) and epicormic.

trees dict

Truth per tree, a table of equal-length arrays: tree_id, archetype, x, y, z (stem base on the terrain), x_bh, y_bh (the stem axis at 1.3 m, where a leaning stem is found), dbh, height (above the terrain at the stem), crown_base, crown_area, wood_volume, stem_volume, branch_volume (m³) and leaf_area (m²).

cylinders ndarray

(n, 12) cylinder rows of all trees (sylva.qsm.COLUMNS); parent refers to rows of the same tree. Use qsm.

cylinder_tree ndarray

Tree of each cylinder.

leaves dict

Leaf blades of all trees, as in SyntheticTree, plus tree.

dead_wood dict

Fallen logs and stumps: kind ("log" or "stump"), start, axis, length, radius; a stump starts 0.1 m below the ground.

shrubs dict

Understorey shrubs: x, y, height, leaf_area.

grass dict

Grass tufts: x, y, radius.

terrain dict

slope, aspect and modes, for ground_height.

size float

Side of the plot (m); trees stand in [0, size]².

stem_density property

stem_density: float

Stems per hectare.

basal_area property

basal_area: float

Basal area (m² ha⁻¹).

qsm

qsm(tree_id: int) -> QSM

The cylinders of one tree.

Parameters:

Name Type Description Default
tree_id int

From trees.

required

Returns:

Type Description
QSM

With parent rows relative to that tree.

Raises:

Type Description
KeyError

For a tree the plot does not have.

ground_height

ground_height(x, y) -> np.ndarray

The terrain's elevation.

Parameters:

Name Type Description Default
x

Coordinates (m).

required
y

Coordinates (m).

required

Returns:

Type Description
ndarray

Elevation of the plane and its micro-relief.

SyntheticTree dataclass

A tree from tree_model with its truth.

Attributes:

Name Type Description
points PointCloud

The tree's points, with attributes classification (4 leaf, 5 wood), label (2 stem, 3 branch, 4 leaf; see LABELS), branch_order (0 stem, 1, 2, ...; for a leaf, the order of its twig), cylinder (row of qsm, -1 for leaves), leaf (row of leaves, -1 for wood) and epicormic (1 on epicormic shoots and their leaves).

qsm QSM

The wood as cylinders, the truth for a QSM: the points were drawn on exactly these cylinders (on the shaped section for the stem, whose radius is the area-equivalent one), so volumes and lengths are exact. n_points counts the points drawn on each.

leaves dict

One entry per leaf blade (an ellipse): centre, normal (unit, pointing up), axis (unit, along the blade), length, width, area (π L W / 4), cylinder (the twig it hangs from) and epicormic.

archetype str

Name of the archetype.

base ndarray

Stem base.

stem_bh ndarray

The stem axis at breast height (1.3 m along the stem): where a leaning stem's DBH is measured.

dbh float

Area-equivalent diameter of the stem at breast height (m).

height float

Highest point of wood or leaf above the base (m).

crown_base float

Height of the lowest first-order branch that is not epicormic (m).

crown_area float

Area of the convex hull of the crown seen from above (m²).

crown_extent tuple

(xmin, ymin, xmax, ymax) of the crown.

leaf_area float

One-sided leaf area (m²), epicormic leaves included.

epicormic_leaf_area float

Leaf area on epicormic shoots (m²).

lad str

Name of the leaf angle distribution the normals were drawn from.

cylinder_epicormic ndarray

Per cylinder, whether it belongs to an epicormic shoot.

wood_volume property

wood_volume: float

Volume of the wood (m³), the sum of the cylinders.

volume_by_order

volume_by_order() -> dict

Wood volume (m³) per branch order, from the cylinders.

Returns:

Type Description
dict

{order: volume}, order 0 being the stem.

length_by_order

length_by_order() -> dict

Axis length (m) per branch order, from the cylinders.

Returns:

Type Description
dict

{order: length}.

leaf_angles

leaf_angles(n_bins: int = 18) -> LeafAngleDistribution

The leaf angle distribution of the generated leaves, weighted by their area.

Parameters:

Name Type Description Default
n_bins int

Bins over 0-90 degrees.

18

Returns:

Type Description
LeafAngleDistribution

Whose ~sylva.leaves.LeafAngleDistribution.g gives the leaf projection function G of the tree's leaves.

terrain_height

terrain_height(x, y, slope: float = 0.05) -> np.ndarray

Ground elevation of the synthetic scenes: a gentle slope with a ripple.

Parameters:

Name Type Description Default
x

Coordinates (m).

required
y

Coordinates (m).

required
slope float

Rise per metre along x.

0.05

Returns:

Type Description
ndarray

slope * x + 0.2 * sin(y / 3).

tree

tree(x: float = 0.0, y: float = 0.0, dbh: float = 0.3, height: float = 12.0, z0: float = 0.0, n_branches: int = 6, leaf_points: int = 18000, seed: int = 0) -> PointCloud

One tree: a tapered stem, n_branches limbs in the upper half and small flat leaf discs around the limb ends.

The classification attribute is 5 for wood and 4 for leaves.

Parameters:

Name Type Description Default
x float

Stem base position.

0.0
y float

Stem base position.

0.0
z0 float

Stem base position.

0.0
dbh float

Diameter at the base (m); the stem tapers to a quarter of it.

0.3
height float

Tree height (m).

12.0
n_branches int

Limbs.

6
leaf_points int

Approximate number of leaf points (12 per leaf disc).

18000
seed int

Random seed.

0

Returns:

Type Description
PointCloud

leaf_area

leaf_area(cloud: PointCloud) -> float

True one-sided leaf area of a synthetic cloud.

Parameters:

Name Type Description Default
cloud PointCloud

From tree or forest.

required

Returns:

Type Description
float

Area (m²) of the leaf discs, counted from classification == 4 points; what leaf-area estimates can be checked against.

forest

forest(trees=None, size: float = 20.0, ground_points: int = 40000, margin: float = 4.0, seed: int = 0) -> PointCloud

Sloped terrain with a few trees standing on it (z is not normalised). The ground extends margin m beyond the size m square so that no crown overhangs the edge of the terrain.

trees is a list of (x, y, dbh, height), by default DEFAULT_TREES. Attributes: classification (2 ground, 4 leaf, 5 wood) and tree_id (0 for ground, then 1.. in list order) as ground truth to compare results against.

Parameters:

Name Type Description Default
trees

(x, y, dbh, height) per tree.

None
size float

Side of the plot square (m).

20.0
ground_points int

Points on the terrain.

40000
margin float

Terrain beyond the plot edge (m).

4.0
seed int

Random seed.

0

Returns:

Type Description
PointCloud

scan

scan(cloud: PointCloud, origin=(10.0, 10.0, 1.5), resolution_deg: float = 0.25, max_zenith_deg: float = 130.0, max_echoes: int = 2, echo_separation: float = 0.5, *, scanner: str | None = None, min_zenith_deg: float | None = None, range_noise=None, beam_divergence: float | None = None, exit_diameter: float | None = None, footprint_samples: int | None = None, mixed_pixels: bool | None = None, reflectance=None, target_radius: float | None = None, oriented: bool | None = None, detection_threshold: float | None = None, max_range: float | None = None, seed: int = 0) -> Shots

A pseudo terrestrial scan of cloud from origin.

Pulses are fired on a regular zenith / azimuth grid. The points falling in a pulse's angular cell are its candidate targets: the nearest gives the first echo, and further ones at least echo_separation m apart give up to max_echoes echoes. Cells without a point are pulses with no return, which a real scanner's point stream would not show, but which carry the free-space information ray tracing needs. Echo attributes are copied from the points, so classification and tree_id survive.

Beam model. Giving any of the keyword-only options below switches to a scanner with a finite beam, cast into the points: each pulse leaves along the centre of its angular cell as a cone of full divergence beam_divergence from an aperture of exit_diameter, sampled by footprint_samples equal-energy sub-beams. Each point stands for a small patch of surface of radius target_radius: a disc facing along its normal (the normal_x, normal_y, normal_z attributes, which tree_model and plot provide, else a local PCA where the points are planar), or a sphere; a sub-beam stops at the first patch it meets. Hits closer in range than echo_separation form one echo: with mixed_pixels at their energy-weighted mean range (energy: the sub-beam's share times the target's reflectance), which is how a footprint straddling an edge puts a point in the gap between foreground and background; beyond it, separate echoes (multiple returns within a footprint). Echoes weaker than detection_threshold are lost, and Gaussian range noise is added along the beam. Echo positions lie on the pulse axis, and the echoes carry, besides the attributes of their strongest point, reflectance (dB; 0 dB is a white target filling the footprint), footprint_fraction (share of the sub-beams in the echo) and range_spread (m, the spread of the hits' ranges within the echo, before noise: large for a mixed pixel). Every pulse is kept, misses included. Leaving all of them out gives the scan above, unchanged.

Parameters:

Name Type Description Default
cloud PointCloud

Scene to scan, e.g. forest.

required
origin

Scanner position.

(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
max_echoes int

Echoes per pulse.

2
echo_separation float

Minimum range (m) between echoes of one pulse; with the beam model, the range resolution of the receiver.

0.5
scanner str | None

A scanner model from sylva.synthetic.SCANNERS ("vz400", "vz400i", "vz2000i"): its field of view, divergence, exit diameter and ranging precision (see scanner_preset) stand in for the options not given. The angular step stays resolution_deg.

None
min_zenith_deg float | None

Pulses start at this zenith (degrees); 0 by default, 30 for the scanner models.

None
range_noise

Standard deviation of the range noise (m), or (a, b) for a + b R at range R.

None
beam_divergence float | None

Full beam divergence (mrad).

None
exit_diameter float | None

Beam diameter at the scanner (m).

None
footprint_samples int | None

Sub-beams per pulse (sunflower pattern); 7 when the beam has a size, else 1.

None
mixed_pixels bool | None

Merge hits within echo_separation at their mean range (True, the default) or report the strongest hit's range (False).

None
reflectance

Reflectance of the targets: {classification: value} (other classes 0.4), a number for all, or the name of a per-point attribute. By default near-infrared (1550 nm) values: ground, understorey and leaf 0.3, wood 0.5, other 0.4.

None
target_radius float | None

Radius (m) of the surface patch each point stands for; by default 1.75 times the median point spacing, which closes a surface sampled as tree_model samples it (independent random points leave a few holes). Objects thinner than this, such as twigs, are seen thicker than they are.

None
oriented bool | None

Orient the patches as discs where a normal is known (True, the default), which keeps silhouettes sharp; False makes every patch a sphere, which widens every object by target_radius on each side.

None
detection_threshold float | None

Least echo energy detected (the reflectance a target filling the footprint would give); 0 by default.

None
max_range float | None

Longest range (m); unlimited by default.

None
seed int

Seed of the range noise (beam model only).

0

Returns:

Type Description
Shots

One pulse per angular cell, misses included.

Examples:

>>> f = synthetic.forest()
>>> s = synthetic.scan(f)                                   # the point-cell scan
>>> s = synthetic.scan(f, scanner="vz400", range_noise=0.005)  # a finite beam
>>> s.echo_attrs["range_spread"]

als_flight

als_flight(forest: PointCloud, altitude: float = 80.0, speed: float = 10.0, line_spacing: float = 40.0, heading: float = 0.0, scan_pattern: str = 'oscillating', scan_angle: float = 30.0, scan_rate: float = 80.0, pulse_rate: float = 50000.0, beam_divergence: float = 1.0, footprint_samples: int = 7, max_returns: int = 5, min_separation: float = 1.0, detection_threshold: float = 0.1, range_noise: float = 0.02, attitude=(0.5, 0.3, 0.2), target_radius: float = 0.03, terrain_slope: float = 0.05, bounds=None, clip: bool = True, margin: float = 10.0, turn_time: float = 10.0, start_time: float = 0.0, trajectory_rate: float = 100.0, seed: int = 0) -> ALSFlight

Fly a simulated airborne laser scanner over a synthetic scene.

The platform flies parallel flight lines, alternating in direction, at constant height and speed; a mirror sweeps the beam across the track and pulses leave at pulse_rate. A pulse is a cone of the beam divergence sampled by footprint_samples equal-energy sub-beams, each stopping at the first scene point it passes within target_radius of, or at the terrain. Hits closer in range than min_separation merge into one return (at their energy-weighted mean range), returns carrying less than detection_threshold of the pulse energy are lost, and at most max_returns are kept, nearest first.

The ground is the analytic surface of terrain_height (with terrain_slope), not the scene's ground points (classification == 2), which are ignored; so the true DTM is known exactly. Use forest for a scene; larger scenes are a forest with more trees over a larger size.

Geometry and timing, which the ray-based methods rely on:

  • Frames: map x east, y north, z up; body x forward, y right, z down. Heading is clockwise from north; roll positive with the right wing down; pitch positive nose up. A body vector b points along M Rz(heading) Ry(pitch) Rx(roll) b in the map, with M taking north-east-down to east-north-up.
  • The beam leaves the scanner (at the trajectory position; no lever arm or boresight) along b = (0, sin a, cos a) for mirror angle a (positive to the right). Every return of a pulse lies on that axis: point = position(gps_time) + range * direction; see ALSFlight.sensor_positions.
  • scan_angle is the LAS scan angle a - roll (degrees from the vertical, including roll); the mirror angle is scan_angle + roll.
  • Line k (from 0) starts at start_time + k * (T + turn_time), T being the time to fly a line, and fires pulses at 1 / pulse_rate intervals from its start; every return of a pulse has the pulse's gps_time. Roll, pitch and heading are sinusoids about level flight along the line (periods 7.3, 11.1 and 13.7 s, amplitudes attitude, phases from seed).
  • "oscillating": the mirror sweeps left to right and back at a constant angular rate, scan_rate sweeps per second (a zigzag on the ground). "rotating": a polygon mirror sweeps left to right only and jumps back (parallel scan lines).

Parameters:

Name Type Description Default
forest PointCloud

The scene, e.g. forest: points, with classification (2 is ignored; 4 leaf and 5 wood set the reflectance) and optionally tree_id, carried to the returns.

required
altitude float

Flying height above z = 0 (m).

80.0
speed float

Ground speed (m/s).

10.0
line_spacing float

Distance between flight lines (m). The lines are centred on the area; there are floor(width / line_spacing) + 1 of them.

40.0
heading float

Direction of the first line, degrees clockwise from north.

0.0
scan_pattern ('oscillating', 'rotating')

Mirror motion.

"oscillating"
scan_angle float

Largest mirror angle either side of nadir (degrees, at most 75).

30.0
scan_rate float

Sweeps across the track per second.

80.0
pulse_rate float

Pulses per second.

50000.0
beam_divergence float

Full divergence (mrad): the footprint radius at range R is R * beam_divergence / 2000 m.

1.0
footprint_samples (1, 7, 19)

Sub-beams per pulse.

1
max_returns int

Returns kept per pulse (1-15).

5
min_separation float

Range resolution (m): hits closer than this form one return.

1.0
detection_threshold float

Share of the pulse energy a return needs (0-1).

0.1
range_noise float

Standard deviation of Gaussian noise on each return's range (m).

0.02
attitude

Amplitudes of roll, pitch and heading (degrees, at most 10).

(0.5, 0.3, 0.2)
target_radius float

Radius (m) of the sphere each scene point stands for. The leaf points of tree are about 4 cm apart.

0.03
terrain_slope float

Slope of the terrain (at most 0.5); 0.05 is that of forest.

0.05
bounds

(xmin, ymin, xmax, ymax) to cover; the scene's extent if None.

None
clip bool

Keep only returns inside bounds.

True
margin float

Distance (m) flown beyond the area at each end of a line.

10.0
turn_time float

Seconds between lines, laser off.

10.0
start_time float

GPS time of the first pulse (s).

0.0
trajectory_rate float

Trajectory samples per second.

100.0
seed int

Random seed (NumPy's default_rng): the same seed gives the same flight, whatever the number of threads.

0

Returns:

Type Description
ALSFlight

Raises:

Type Description
ValueError

For settings out of range, or an altitude that would take the platform through the scene or the terrain.

Examples:

>>> from sylva import synthetic
>>> flight = synthetic.als_flight(synthetic.forest(), pulse_rate=20_000)
>>> cat = flight.write_tiles("tiles/", size=10.0)

forest_epochs

forest_epochs(n_trees: int = 16, size: float = 30.0, deaths: int = 2, recruits: int = 2, replaced: int = 1, small_increments: int = 2, dbh_increment: tuple[float, float] = (0.012, 0.004), height_increment: tuple[float, float] = (0.6, 0.2), branch_removals: int = 1, foliage_box=None, foliage_fraction: float = 0.5, tree_shift: float = 0.0, offset=None, range_noise: tuple[float, float] = (0.003, 0.005), scan_positions=None, scan_jitter: float = 0.5, resolution_deg: float = 0.25, max_echoes: int = 1, ground_density: float = 60.0, min_spacing: float = 2.5, seed: int = 0) -> ForestEpochs

Two epochs of one synthetic plot with known changes, each scanned.

Every tree is a fixed structure (a tapered stem, limbs at fixed heights, leaf discs at fixed offsets from the limb tips) sampled afresh in each epoch. Between the epochs survivors grow by a known DBH and height increment (a uniform layer of wood along the stem, a longer leader and proportionally longer limbs); deaths trees are gone, recruits new small trees stand (replaced of them 0.3 to 0.6 m from a dead stem, as after felling), branch_removals survivors lose their largest limb, small_increments survivors grow by only 0.5 mm in DBH and 1 cm in height, and foliage_fraction of the leaf discs in foliage_box disappear. Each epoch is scanned from scan_positions with scan (so occlusion is real) and Gaussian range noise; the epoch-2 scanners stand scan_jitter m (one sigma) from the epoch-1 ones, and epoch 2 is delivered in a frame displaced by offset, as an independently registered revisit would be.

Parameters:

Name Type Description Default
n_trees int

Trees in epoch 1, with DBH drawn from 0.15 to 0.5 m and height from an allometry (about 12 to 20 m).

16
size float

Side of the square plot (m); stems stand at least 2 m inside it.

30.0
deaths int

Trees lost, trees gained, and of those the felled-and-replaced pairs.

2
recruits int

Trees lost, trees gained, and of those the felled-and-replaced pairs.

2
replaced int

Trees lost, trees gained, and of those the felled-and-replaced pairs.

2
small_increments int

Survivors with increments far below what a scan can detect.

2
dbh_increment tuple[float, float]

Mean and standard deviation (m) of the survivors' increments.

(0.012, 0.004)
height_increment tuple[float, float]

Mean and standard deviation (m) of the survivors' increments.

(0.012, 0.004)
branch_removals int

Survivors that lose their largest limb and its leaves.

1
foliage_box

(xmin, ymin, zmin, xmax, ymax, zmax) of the thinned foliage; by default the crown of one survivor.

None
foliage_fraction float

Share of the leaf discs in the box that disappear.

0.5
tree_shift float

Horizontal displacement (m, random direction) of every survivor's stem between the epochs, to test matching; 0 keeps stems in place.

0.0
offset

(4, 4) rigid transform from the true frame to the frame epoch 2 is delivered in; by default a rotation of 1.5 degrees about z with tilts of 0.1 and -0.15 degrees, and a shift of (0.8, -0.5, 0.15) m.

None
range_noise tuple[float, float]

Range noise (m, one sigma) of epoch 1 and epoch 2.

(0.003, 0.005)
scan_positions

Scanner positions as (n, 2) fractions of the plot side (1.5 m above ground); by default the centre and four positions at 0.2 and 0.8. Positions within 1 m of a stem are moved away from it.

None
scan_jitter float

Standard deviation (m) of the epoch-2 scanners' offsets.

0.5
resolution_deg float

Angular step of the scans (degrees).

0.25
max_echoes int

Echoes recorded per pulse. scan places every echo of a pulse along the direction of its last one, so with more than one echo the first echoes at a stem's silhouette are displaced sideways and stem circles come out a few millimetres wide; one echo keeps every point on its surface.

1
ground_density float

Terrain points per m² before scanning.

60.0
min_spacing float

Least distance between stems (m).

2.5
seed int

Random seed; the same seed gives the same epochs.

0

Returns:

Type Description
ForestEpochs

Raises:

Type Description
ValueError

If the counts are inconsistent (more deaths than trees, more replaced than deaths or recruits) or the trees cannot be placed.

Examples:

>>> ep = synthetic.forest_epochs(seed=1)
>>> ref, new = ep.clouds
>>> [c["tree_id"] for c in ep.of_kind("death")]

waveforms

waveforms(shots: Shots, gps_time=None, pulse_width: float = 1.5, interval: float = 1.0, n_samples: int = 120, margin: float = 3.0, start_range: float | None = None, background: float = 10.0, noise: float = 1.0, digitise: bool = True, bits: int = 16, amplitude: float = 100.0, metres_per_ns: float = 0.299792458 / 2.0, seed: int = 0)

Full waveforms of pulses whose targets are known.

The received waveform is the system pulse, a Gaussian of standard deviation pulse_width, convolved with the targets along the beam (Wagner et al. 2006): each echo of shots is a target at its range, with the peak amplitude of the echo attribute amplitude (for a point target) and the depth of the echo attribute extent (standard deviation along the beam, m; 0 if absent). A target of depth e gives a Gaussian echo of width s = sqrt(pulse_width^2 + (e / metres_per_ns)^2) and, its energy being kept, peak amplitude * pulse_width / s. The echoes are added to a constant background with Gaussian noise and, with digitise, rounded and clipped to 0 .. 2^bits - 1 as a digitiser would.

Parameters:

Name Type Description Default
shots Shots

Pulses and their targets; a shot without an echo gives a waveform of background and noise only.

required
gps_time

Time of each shot; the shot index by default.

None
pulse_width float

Standard deviation of the system pulse (ns). A 3.5 ns full width at half maximum is 1.5 ns.

1.5
interval float

Sampling interval (ns).

1.0
n_samples int

Samples per waveform.

120
margin float

Range (m) before a shot's first echo at which its record starts (a shot without an echo starts at the origin).

3.0
start_range float | None

Range (m) at which every record starts, instead of margin.

None
background float

Background level and noise standard deviation (sample units).

10.0
noise float

Background level and noise standard deviation (sample units).

10.0
digitise bool

Round and clip the samples.

True
bits int

Digitiser resolution.

16
amplitude float

Peak of echoes without an amplitude attribute.

100.0
metres_per_ns float

Range per nanosecond of round-trip time (m/ns).

0.299792458 / 2.0
seed int

Seed of the noise.

0

Returns:

Name Type Description
waveforms Waveforms

One returning waveform per shot, anchored at its origin.

truth Echoes

The echoes they contain: time, peak amplitude and width after the convolution, position and range.

Raises:

Type Description
ValueError

For non-positive widths, intervals or sample counts, negative noise, or gps_time of the wrong length.

stand

stand(n_trees: int, size: float = 100.0, min_spacing: float = 4.0, heights=(10.0, 25.0), seed: int = 0) -> list[tuple[float, float, float, float]]

Random trees for forest: a stand of a given density.

Stems are drawn uniformly in the size m square and rejected when closer than min_spacing to one already placed; heights are uniform in heights and dbh = 0.1 + 0.015 * height.

Parameters:

Name Type Description Default
n_trees int

Trees to place.

required
size float

Side of the square (m).

100.0
min_spacing float

Smallest distance between stems (m).

4.0
heights

(low, high) tree heights (m), 1 < low <= high.

(10.0, 25.0)
seed int

Random seed (NumPy's default_rng stream).

0

Returns:

Type Description
list of (x, y, dbh, height)

Raises:

Type Description
ValueError

For bad settings, or if the trees cannot be placed that far apart.

crown_forest

crown_forest(trees, size: float = 100.0, shape: str = 'ellipsoid', crown_radius: float = 0.25, crown_length: float = 0.5, density: float = 40.0, ground_points: int = 40000, margin: float = 4.0, seed: int = 0) -> PointCloud

A scene of trees with solid crowns, for airborne lidar.

Each tree (x, y, dbh, height) (e.g. from stand) has a stem (points on a cylinder of diameter dbh, classification 5) from the terrain up to its crown, and a crown filled uniformly with density leaf points per m³ (classification 4): an ellipsoid of revolution or a cone of radius crown_radius * height and length crown_length * height, its top height above the terrain at the stem. The trees of forest carry their leaves in clusters at the ends of a few limbs, which suits terrestrial scanning; these have the closed outline that airborne tree detection assumes, and a projected crown area of pi * (crown_radius * height) ** 2.

Parameters:

Name Type Description Default
trees

(x, y, dbh, height) per tree.

required
size float

Terrain as in forest (slope 0.05).

100.0
ground_points float

Terrain as in forest (slope 0.05).

100.0
margin float

Terrain as in forest (slope 0.05).

100.0
shape ('ellipsoid', 'cone')

Crown shape.

"ellipsoid"
crown_radius float

Crown radius and length as fractions of the tree height (crown_length at most 1).

0.25
crown_length float

Crown radius and length as fractions of the tree height (crown_length at most 1).

0.25
density float

Leaf points per m³ of crown. With the default target_radius of als_flight (3 cm), 40 stops about 10 % of a beam's energy per metre of crown.

40.0
seed int

Random seed; the crown of tree i uses seed + i + 1.

0

Returns:

Type Description
PointCloud

With classification (2 ground, 4 leaf, 5 wood) and tree_id (0 for ground, then 1.. in list order); forest_trees gives the truth.

Raises:

Type Description
ValueError

For a non-positive radius or density, a crown length outside (0, 1], or trees without finite positions and positive heights.

forest_trees

forest_trees(forest: PointCloud, terrain_slope: float = 0.05) -> dict

The true trees of a scene from forest, to check tree detection and crown delineation against.

Parameters:

Name Type Description Default
forest PointCloud

The scene, from forest or crown_forest, with its tree_id (0 for ground).

required
terrain_slope float

Slope of the terrain under it (0.05 for forest, and the terrain_slope of als_flight).

0.05

Returns:

Type Description
dict

Columns in order of tree_id: tree_id; stem_x, stem_y (mean of the wood points within 1 m of the tree's lowest point); top_x, top_y, top_z (the highest point); height (of the top above the terrain beneath it); crown_area (m², convex hull of all the tree's points seen from above) and crowns (those hulls, (k, 2) each).

Raises:

Type Description
ValueError

If the scene has no tree_id.

als_epochs

als_epochs(n_trees: int = 100, size: float = 100.0, removed: int = 4, gap=(70.0, 70.0, 5), growth: float = 1.0, offset=(0.3, -0.2, 0.15), buildings: bool = True, flight_a: dict | None = None, flight_b: dict | None = None, seed: int = 0) -> ALSEpochs

Two airborne surveys of a synthetic forest with known changes.

A stand of n_trees (~sylva.synthetic.stand, 12 to 25 m, stems at least 4 m apart) in the west size m of a square of size + 20 m, with two gabled buildings east of it (one ridge running north, one east, so that their roofs have slopes of four aspects), is flown twice with ~sylva.synthetic.als_flight. Between the surveys

  • the gap[2] trees nearest to gap[:2] are felled, opening a gap;
  • removed other trees are felled singly;
  • the trees in the west half grow by growth m (their crowns scale with their height), the others are unchanged, point for point;
  • the second survey is flown by another sensor (by default 120 m above ground at 2,500 pulses/s with 40 m between lines, against 80 m at 4,000 pulses/s and 30 m: about half the pulse density and a 50 % wider footprint);
  • its tiles are delivered displaced by offset.

Parameters:

Name Type Description Default
n_trees int

Trees and side (m) of the forest.

100
size int

Trees and side (m) of the forest.

100
removed int

Trees felled singly.

4
gap

(x, y, n): centre of the gap and trees felled to open it, or None for none.

(70.0, 70.0, 5)
growth float

Height growth (m) of the trees in the west half.

1.0
offset

(dx, dy, dz) misalignment of the second survey.

(0.3, -0.2, 0.15)
buildings bool

Add the two buildings.

True
flight_a dict | None

Settings of ~sylva.synthetic.als_flight overriding the defaults of each survey. Unless given, scan_rate is set so that pulses are as far apart along the scan as along the track, as surveys are planned (the default of als_flight, 80 sweeps per second, leaves them 15 times farther apart across the track at these pulse rates).

None
flight_b dict | None

Settings of ~sylva.synthetic.als_flight overriding the defaults of each survey. Unless given, scan_rate is set so that pulses are as far apart along the scan as along the track, as surveys are planned (the default of als_flight, 80 sweeps per second, leaves them 15 times farther apart across the track at these pulse rates).

None
seed int

Random seed.

0

Returns:

Type Description
ALSEpochs

archetype

archetype(name: str) -> dict

The architectural constants of an archetype.

Parameters:

Name Type Description Default
name str

One of ARCHETYPES.

required

Returns:

Type Description
dict

crown_base and leader (fractions of the height at which the crown starts and the stem ends), crown_radius (fraction of the height, an upper bound), crown_k (crown radius crown_k (0.6 + 12 dbh) m), lai (leaf area per crown area, an upper bound) and leaf_k (leaf area leaf_k dbh²), allometry ((a, b, c) of the height curve 1.3 + a (1 - exp(-b D))^c, D in cm), leaf_size (m), lad, butt_swell, lean (degrees) and sweep.

plot

plot(size: float = 30.0, density: float = 600.0, dbh=('weibull', 1.8, 0.22), min_dbh: float = 0.07, archetypes='broadleaf', *, height_noise: float = 0.1, slope: float = 0.1, aspect: float = 90.0, roughness: float = 0.05, roughness_length: float = 2.0, shrubs: float = 400.0, grass_cover: float = 0.2, grass_height: float = 0.5, logs: float = 60.0, stumps: float = 40.0, ground_density: float = 400.0, margin: float = 2.0, point_density: float = 1000.0, max_order: int = 3, lad=None, epicormic: float = 0.0, seed: int = 0) -> Plot

A forest plot of tree_model trees, with its truth.

round(density * size² / 10⁴) diameters are drawn from the distribution and truncated below at min_dbh; each tree takes an archetype by weight, a height from the archetype's Chapman-Richards curve 1.3 + a (1 - exp(-b D))^c (D in cm) times a lognormal scatter exp(N(0, height_noise)), and the archetype's crown and leaf area allometries. Stems are placed uniformly at random, largest first, never closer than 0.75 (D_i + D_j) + 0.2 m. The terrain is a plane rising by slope per metre towards aspect plus a Gaussian random field (64 Fourier modes) of standard deviation roughness and correlation length roughness_length. Shrubs (shrub archetype, 0.5 to 2.5 m), grass tufts (15 cm radius, blades up to grass_height), fallen logs (2 to 8 m long, 5 to 25 cm radius, lying on the ground) and stumps (0.2 to 1 m tall, 10 to 30 cm radius) are scattered over the plot. Nothing is drawn below the terrain.

Parameters:

Name Type Description Default
size float

Side of the square plot (m).

30.0
density float

Stems per hectare.

600.0
dbh

Diameter distribution: ("weibull", shape, scale) (scale in m), ("reverse_j", mean) (a negative exponential above min_dbh with mean excess mean m, de Liocourt's reverse-J), or a sequence of diameters (m), one tree each (density is then ignored).

('weibull', 1.8, 0.22)
min_dbh float

Smallest diameter (m).

0.07
archetypes

An archetype name, or {name: weight}.

'broadleaf'
height_noise float

Standard deviation of log(height) about the allometry.

0.1
slope float

Rise per metre (e.g. 0.1 is 5.7 degrees).

0.1
aspect float

Direction of steepest ascent (degrees clockwise from north, +y).

90.0
roughness float

Standard deviation of the micro-relief (m).

0.05
roughness_length float

Correlation length of the micro-relief (m).

2.0
shrubs float

Shrubs per hectare.

400.0
grass_cover float

Fraction of the plot covered by grass tufts.

0.2
grass_height float

Tallest grass (m).

0.5
logs float

Fallen logs and stumps per hectare.

60.0
stumps float

Fallen logs and stumps per hectare.

60.0
ground_density float

Terrain points per m², over the plot and margin beyond it.

400.0
margin float

Terrain beyond the plot edge (m).

2.0
point_density float

Points per m² of wood, leaf, understorey and dead wood.

1000.0
max_order int

Highest branch order of the trees (1 to 4).

3
lad

Leaf angle distribution of every tree, as in tree_model; by default each archetype's.

None
epicormic float

Epicormic shoots per metre of bole on every tree.

0.0
seed int

Random seed. Trees are grown in parallel, each with its own seed, so the plot does not depend on the number of threads.

0

Returns:

Type Description
Plot

Raises:

Type Description
ValueError

For unknown names, values out of range, or more stems than fit in the plot without overlap.

Examples:

>>> p = synthetic.plot(size=20, density=800, archetypes={"eucalypt": 2, "broadleaf": 1})
>>> p.trees["dbh"], p.stem_density, p.basal_area
>>> p.points.attrs["label"]      # 1 ground ... 6 dead wood

scanner_preset

scanner_preset(name: str) -> dict

Field of view and beam of a scanner model, as synthetic.scan takes them.

Parameters:

Name Type Description Default
name str

One of SCANNERS ("VZ-400" and similar spellings work).

required

Returns:

Type Description
dict

min_zenith_deg and max_zenith_deg (the vertical field of view, +60 to -40 degrees for these), beam_divergence (mrad), exit_diameter (m, a nominal value) and range_noise (m, the ranging precision), from the manufacturer's data sheets. Check them against your instrument; the angular step is the scan's resolution_deg.

Raises:

Type Description
ValueError

For an unknown model.

tree_model

tree_model(archetype: str = 'broadleaf', x: float = 0.0, y: float = 0.0, z0: float = 0.0, dbh: float = 0.3, height: float = 15.0, *, crown_radius: float | None = None, crown_base: float | None = None, max_order: int = 3, lean: float | None = None, sweep: float | None = None, butt_swell: float | None = None, buttresses: int = 0, buttress_height: float = 1.0, buttress_extent: float = 0.5, ellipticity: float = 0.0, bark_depth: float = 0.0, leaf_area: float | None = None, leaf_size=None, lad=None, epicormic: float = 0.0, point_density: float = 2000.0, pipe_exponent: float = 2.0, min_radius: float = 0.0015, breast_height: float = 1.3, seed: int = 0) -> SyntheticTree

A tree with a realistic architecture and its truth.

The stem leans and curves smoothly, tapers, and swells at the butt; it can be elliptical, fissured and buttressed. First-order branches leave it from the crown base up (in whorls for the conifer) with a length set by the archetype's crown envelope; forking archetypes end the stem in codominant limbs. Each branch carries children up to max_order, each a fixed fraction of its parent's length (shorter towards the tip), at the archetype's branching angle, bending up or drooping. Radii follow the pipe model: above the lowest branch every segment's cross-sectional area is proportional to the leaf area it carries (Shinozaki et al. 1964), continuous with the stem taper below. Leaves are elliptical blades on the terminal twigs, with normals drawn from the leaf angle distribution. Points are drawn at point_density per m² on the wood surface and on one side of each leaf.

Archetypes (archetype lists their constants):

"broadleaf" crown from 35 % of the height, ellipsoidal, the stem forking into two limbs at 80 %; planophile leaves 10 × 5 cm. "conifer" a central leader to the top with whorls of five branches from 25 % of the height, a conical crown, drooping branches; spherical distribution of 5 × 1.2 cm blades standing in for shoots. "eucalypt" a clear bole to 55 % of the height, three ascending limbs from 65 %, a sparse crown (low leaf area) of pendulous (erectophile) 15 × 3 cm leaves, a noticeable lean. "savanna" a short bole forking at 30 % of the height into three spreading limbs, a wide flat crown, small planophile leaves. "shrub" several stems from near the ground; dbh is the stem diameter.

Parameters:

Name Type Description Default
archetype str

One of ARCHETYPES.

'broadleaf'
x float

Stem base.

0.0
y float

Stem base.

0.0
z0 float

Stem base.

0.0
dbh float

Diameter at breast_height along the stem (m), honoured exactly (as the area-equivalent diameter) unless the crown starts below it.

0.3
height float

Target height (m); the realised height is SyntheticTree.height.

15.0
crown_radius float | None

Crown radius (m); by default min(crown_radius * height, crown_k (0.6 + 12 dbh)) from archetype.

None
crown_base float | None

Height of the lowest branch (m); by default the archetype's.

None
max_order int

Highest branch order, 1 to 4.

3
lean float | None

Lean of the stem (degrees, towards a random azimuth); by default the archetype's.

None
sweep float | None

Slope amplitude of the stem's smooth curvature (0 for straight).

None
butt_swell float | None

Extra radius at the ground relative to the taper (e.g. 0.3).

None
buttresses int

Number of buttress flanges (0 for none).

0
buttress_height float

Height the flanges reach (m).

1.0
buttress_extent float

Relative extra radius at the crest of a flange at the ground.

0.5
ellipticity float

a / r - 1 of the stem section; the area stays that of the round section.

0.0
bark_depth float

Depth of bark fissures (m); the area stays that of the round section.

0.0
leaf_area float | None

One-sided leaf area of the crown (m²); by default min(lai π R², leaf_k dbh²) from archetype.

None
leaf_size

(length, width) of a leaf blade (m).

None
lad

Leaf angle distribution: a de Wit name ("spherical", "planophile", "erectophile", "plagiophile", "extremophile", "uniform"), ("beta", mu, nu) with Goel and Strebel's (1984) parameters, or ("ellipsoidal", chi) (Campbell 1990); by default the archetype's.

None
epicormic float

Epicormic shoots per metre of bole below the crown (0 for none), as on eucalypts resprouting after fire.

0.0
point_density float

Points per m² of wood surface and of leaf.

2000.0
pipe_exponent float

Exponent e of the radius rule r ∝ W^(1/e) in the leaf area W carried (2 for the pipe model).

2.0
min_radius float

Smallest twig radius (m).

0.0015
breast_height float

Distance along the stem at which dbh applies (m).

1.3
seed int

Random seed; the same seed gives the same tree on every machine.

0

Returns:

Type Description
SyntheticTree

Raises:

Type Description
ValueError

For an unknown archetype or leaf angle distribution, or a size that is not positive.

Examples:

>>> t = synthetic.tree_model("eucalypt", dbh=0.5, height=30, epicormic=4, seed=1)
>>> t.dbh, t.height, t.wood_volume, t.leaf_area
>>> t.volume_by_order(), t.leaf_angles().mean_deg

Realistic trees and plots

tree_model, plot, archetype and scanner_preset are available from sylva.synthetic; see Synthetic trees, plots and scans.

sylva.synthetic.model

Realistic synthetic trees and plots, with their truth.

These are available from sylva.synthetic as tree_model and plot. A tree is grown from an archetype with recursive branching, allometric lengths and angles, pipe-model radii, a leaning and curving stem, and leaves of known size and orientation; a plot mixes such trees at a given stem density and diameter distribution on rough, sloping terrain with understorey, grass, fallen logs and stumps. Every result carries its truth: the wood as a cylinder table (a ~sylva.qsm.QSM), the leaves as blades, DBH, height, crown and per-point labels.

SyntheticTree dataclass

A tree from tree_model with its truth.

Attributes:

Name Type Description
points PointCloud

The tree's points, with attributes classification (4 leaf, 5 wood), label (2 stem, 3 branch, 4 leaf; see LABELS), branch_order (0 stem, 1, 2, ...; for a leaf, the order of its twig), cylinder (row of qsm, -1 for leaves), leaf (row of leaves, -1 for wood) and epicormic (1 on epicormic shoots and their leaves).

qsm QSM

The wood as cylinders, the truth for a QSM: the points were drawn on exactly these cylinders (on the shaped section for the stem, whose radius is the area-equivalent one), so volumes and lengths are exact. n_points counts the points drawn on each.

leaves dict

One entry per leaf blade (an ellipse): centre, normal (unit, pointing up), axis (unit, along the blade), length, width, area (π L W / 4), cylinder (the twig it hangs from) and epicormic.

archetype str

Name of the archetype.

base ndarray

Stem base.

stem_bh ndarray

The stem axis at breast height (1.3 m along the stem): where a leaning stem's DBH is measured.

dbh float

Area-equivalent diameter of the stem at breast height (m).

height float

Highest point of wood or leaf above the base (m).

crown_base float

Height of the lowest first-order branch that is not epicormic (m).

crown_area float

Area of the convex hull of the crown seen from above (m²).

crown_extent tuple

(xmin, ymin, xmax, ymax) of the crown.

leaf_area float

One-sided leaf area (m²), epicormic leaves included.

epicormic_leaf_area float

Leaf area on epicormic shoots (m²).

lad str

Name of the leaf angle distribution the normals were drawn from.

cylinder_epicormic ndarray

Per cylinder, whether it belongs to an epicormic shoot.

wood_volume property

wood_volume: float

Volume of the wood (m³), the sum of the cylinders.

volume_by_order

volume_by_order() -> dict

Wood volume (m³) per branch order, from the cylinders.

Returns:

Type Description
dict

{order: volume}, order 0 being the stem.

length_by_order

length_by_order() -> dict

Axis length (m) per branch order, from the cylinders.

Returns:

Type Description
dict

{order: length}.

leaf_angles

leaf_angles(n_bins: int = 18) -> LeafAngleDistribution

The leaf angle distribution of the generated leaves, weighted by their area.

Parameters:

Name Type Description Default
n_bins int

Bins over 0-90 degrees.

18

Returns:

Type Description
LeafAngleDistribution

Whose ~sylva.leaves.LeafAngleDistribution.g gives the leaf projection function G of the tree's leaves.

Plot dataclass

A plot from plot with its truth.

Attributes:

Name Type Description
points PointCloud

All points, with classification (2 ground, 3 understorey, 4 leaf, 5 wood and dead wood), label (1 ground, 2 stem, 3 branch, 4 leaf, 5 understorey, 6 dead wood; LABELS), tree_id (0 off the trees, then 1.. by decreasing DBH), branch_order (-1 off the trees), cylinder and leaf (rows of cylinders and leaves, -1 elsewhere) and epicormic.

trees dict

Truth per tree, a table of equal-length arrays: tree_id, archetype, x, y, z (stem base on the terrain), x_bh, y_bh (the stem axis at 1.3 m, where a leaning stem is found), dbh, height (above the terrain at the stem), crown_base, crown_area, wood_volume, stem_volume, branch_volume (m³) and leaf_area (m²).

cylinders ndarray

(n, 12) cylinder rows of all trees (sylva.qsm.COLUMNS); parent refers to rows of the same tree. Use qsm.

cylinder_tree ndarray

Tree of each cylinder.

leaves dict

Leaf blades of all trees, as in SyntheticTree, plus tree.

dead_wood dict

Fallen logs and stumps: kind ("log" or "stump"), start, axis, length, radius; a stump starts 0.1 m below the ground.

shrubs dict

Understorey shrubs: x, y, height, leaf_area.

grass dict

Grass tufts: x, y, radius.

terrain dict

slope, aspect and modes, for ground_height.

size float

Side of the plot (m); trees stand in [0, size]².

stem_density property

stem_density: float

Stems per hectare.

basal_area property

basal_area: float

Basal area (m² ha⁻¹).

qsm

qsm(tree_id: int) -> QSM

The cylinders of one tree.

Parameters:

Name Type Description Default
tree_id int

From trees.

required

Returns:

Type Description
QSM

With parent rows relative to that tree.

Raises:

Type Description
KeyError

For a tree the plot does not have.

ground_height

ground_height(x, y) -> np.ndarray

The terrain's elevation.

Parameters:

Name Type Description Default
x

Coordinates (m).

required
y

Coordinates (m).

required

Returns:

Type Description
ndarray

Elevation of the plane and its micro-relief.

archetype

archetype(name: str) -> dict

The architectural constants of an archetype.

Parameters:

Name Type Description Default
name str

One of ARCHETYPES.

required

Returns:

Type Description
dict

crown_base and leader (fractions of the height at which the crown starts and the stem ends), crown_radius (fraction of the height, an upper bound), crown_k (crown radius crown_k (0.6 + 12 dbh) m), lai (leaf area per crown area, an upper bound) and leaf_k (leaf area leaf_k dbh²), allometry ((a, b, c) of the height curve 1.3 + a (1 - exp(-b D))^c, D in cm), leaf_size (m), lad, butt_swell, lean (degrees) and sweep.

tree_model

tree_model(archetype: str = 'broadleaf', x: float = 0.0, y: float = 0.0, z0: float = 0.0, dbh: float = 0.3, height: float = 15.0, *, crown_radius: float | None = None, crown_base: float | None = None, max_order: int = 3, lean: float | None = None, sweep: float | None = None, butt_swell: float | None = None, buttresses: int = 0, buttress_height: float = 1.0, buttress_extent: float = 0.5, ellipticity: float = 0.0, bark_depth: float = 0.0, leaf_area: float | None = None, leaf_size=None, lad=None, epicormic: float = 0.0, point_density: float = 2000.0, pipe_exponent: float = 2.0, min_radius: float = 0.0015, breast_height: float = 1.3, seed: int = 0) -> SyntheticTree

A tree with a realistic architecture and its truth.

The stem leans and curves smoothly, tapers, and swells at the butt; it can be elliptical, fissured and buttressed. First-order branches leave it from the crown base up (in whorls for the conifer) with a length set by the archetype's crown envelope; forking archetypes end the stem in codominant limbs. Each branch carries children up to max_order, each a fixed fraction of its parent's length (shorter towards the tip), at the archetype's branching angle, bending up or drooping. Radii follow the pipe model: above the lowest branch every segment's cross-sectional area is proportional to the leaf area it carries (Shinozaki et al. 1964), continuous with the stem taper below. Leaves are elliptical blades on the terminal twigs, with normals drawn from the leaf angle distribution. Points are drawn at point_density per m² on the wood surface and on one side of each leaf.

Archetypes (archetype lists their constants):

"broadleaf" crown from 35 % of the height, ellipsoidal, the stem forking into two limbs at 80 %; planophile leaves 10 × 5 cm. "conifer" a central leader to the top with whorls of five branches from 25 % of the height, a conical crown, drooping branches; spherical distribution of 5 × 1.2 cm blades standing in for shoots. "eucalypt" a clear bole to 55 % of the height, three ascending limbs from 65 %, a sparse crown (low leaf area) of pendulous (erectophile) 15 × 3 cm leaves, a noticeable lean. "savanna" a short bole forking at 30 % of the height into three spreading limbs, a wide flat crown, small planophile leaves. "shrub" several stems from near the ground; dbh is the stem diameter.

Parameters:

Name Type Description Default
archetype str

One of ARCHETYPES.

'broadleaf'
x float

Stem base.

0.0
y float

Stem base.

0.0
z0 float

Stem base.

0.0
dbh float

Diameter at breast_height along the stem (m), honoured exactly (as the area-equivalent diameter) unless the crown starts below it.

0.3
height float

Target height (m); the realised height is SyntheticTree.height.

15.0
crown_radius float | None

Crown radius (m); by default min(crown_radius * height, crown_k (0.6 + 12 dbh)) from archetype.

None
crown_base float | None

Height of the lowest branch (m); by default the archetype's.

None
max_order int

Highest branch order, 1 to 4.

3
lean float | None

Lean of the stem (degrees, towards a random azimuth); by default the archetype's.

None
sweep float | None

Slope amplitude of the stem's smooth curvature (0 for straight).

None
butt_swell float | None

Extra radius at the ground relative to the taper (e.g. 0.3).

None
buttresses int

Number of buttress flanges (0 for none).

0
buttress_height float

Height the flanges reach (m).

1.0
buttress_extent float

Relative extra radius at the crest of a flange at the ground.

0.5
ellipticity float

a / r - 1 of the stem section; the area stays that of the round section.

0.0
bark_depth float

Depth of bark fissures (m); the area stays that of the round section.

0.0
leaf_area float | None

One-sided leaf area of the crown (m²); by default min(lai π R², leaf_k dbh²) from archetype.

None
leaf_size

(length, width) of a leaf blade (m).

None
lad

Leaf angle distribution: a de Wit name ("spherical", "planophile", "erectophile", "plagiophile", "extremophile", "uniform"), ("beta", mu, nu) with Goel and Strebel's (1984) parameters, or ("ellipsoidal", chi) (Campbell 1990); by default the archetype's.

None
epicormic float

Epicormic shoots per metre of bole below the crown (0 for none), as on eucalypts resprouting after fire.

0.0
point_density float

Points per m² of wood surface and of leaf.

2000.0
pipe_exponent float

Exponent e of the radius rule r ∝ W^(1/e) in the leaf area W carried (2 for the pipe model).

2.0
min_radius float

Smallest twig radius (m).

0.0015
breast_height float

Distance along the stem at which dbh applies (m).

1.3
seed int

Random seed; the same seed gives the same tree on every machine.

0

Returns:

Type Description
SyntheticTree

Raises:

Type Description
ValueError

For an unknown archetype or leaf angle distribution, or a size that is not positive.

Examples:

>>> t = synthetic.tree_model("eucalypt", dbh=0.5, height=30, epicormic=4, seed=1)
>>> t.dbh, t.height, t.wood_volume, t.leaf_area
>>> t.volume_by_order(), t.leaf_angles().mean_deg

plot

plot(size: float = 30.0, density: float = 600.0, dbh=('weibull', 1.8, 0.22), min_dbh: float = 0.07, archetypes='broadleaf', *, height_noise: float = 0.1, slope: float = 0.1, aspect: float = 90.0, roughness: float = 0.05, roughness_length: float = 2.0, shrubs: float = 400.0, grass_cover: float = 0.2, grass_height: float = 0.5, logs: float = 60.0, stumps: float = 40.0, ground_density: float = 400.0, margin: float = 2.0, point_density: float = 1000.0, max_order: int = 3, lad=None, epicormic: float = 0.0, seed: int = 0) -> Plot

A forest plot of tree_model trees, with its truth.

round(density * size² / 10⁴) diameters are drawn from the distribution and truncated below at min_dbh; each tree takes an archetype by weight, a height from the archetype's Chapman-Richards curve 1.3 + a (1 - exp(-b D))^c (D in cm) times a lognormal scatter exp(N(0, height_noise)), and the archetype's crown and leaf area allometries. Stems are placed uniformly at random, largest first, never closer than 0.75 (D_i + D_j) + 0.2 m. The terrain is a plane rising by slope per metre towards aspect plus a Gaussian random field (64 Fourier modes) of standard deviation roughness and correlation length roughness_length. Shrubs (shrub archetype, 0.5 to 2.5 m), grass tufts (15 cm radius, blades up to grass_height), fallen logs (2 to 8 m long, 5 to 25 cm radius, lying on the ground) and stumps (0.2 to 1 m tall, 10 to 30 cm radius) are scattered over the plot. Nothing is drawn below the terrain.

Parameters:

Name Type Description Default
size float

Side of the square plot (m).

30.0
density float

Stems per hectare.

600.0
dbh

Diameter distribution: ("weibull", shape, scale) (scale in m), ("reverse_j", mean) (a negative exponential above min_dbh with mean excess mean m, de Liocourt's reverse-J), or a sequence of diameters (m), one tree each (density is then ignored).

('weibull', 1.8, 0.22)
min_dbh float

Smallest diameter (m).

0.07
archetypes

An archetype name, or {name: weight}.

'broadleaf'
height_noise float

Standard deviation of log(height) about the allometry.

0.1
slope float

Rise per metre (e.g. 0.1 is 5.7 degrees).

0.1
aspect float

Direction of steepest ascent (degrees clockwise from north, +y).

90.0
roughness float

Standard deviation of the micro-relief (m).

0.05
roughness_length float

Correlation length of the micro-relief (m).

2.0
shrubs float

Shrubs per hectare.

400.0
grass_cover float

Fraction of the plot covered by grass tufts.

0.2
grass_height float

Tallest grass (m).

0.5
logs float

Fallen logs and stumps per hectare.

60.0
stumps float

Fallen logs and stumps per hectare.

60.0
ground_density float

Terrain points per m², over the plot and margin beyond it.

400.0
margin float

Terrain beyond the plot edge (m).

2.0
point_density float

Points per m² of wood, leaf, understorey and dead wood.

1000.0
max_order int

Highest branch order of the trees (1 to 4).

3
lad

Leaf angle distribution of every tree, as in tree_model; by default each archetype's.

None
epicormic float

Epicormic shoots per metre of bole on every tree.

0.0
seed int

Random seed. Trees are grown in parallel, each with its own seed, so the plot does not depend on the number of threads.

0

Returns:

Type Description
Plot

Raises:

Type Description
ValueError

For unknown names, values out of range, or more stems than fit in the plot without overlap.

Examples:

>>> p = synthetic.plot(size=20, density=800, archetypes={"eucalypt": 2, "broadleaf": 1})
>>> p.trees["dbh"], p.stem_density, p.basal_area
>>> p.points.attrs["label"]      # 1 ground ... 6 dead wood

scanner_preset

scanner_preset(name: str) -> dict

Field of view and beam of a scanner model, as synthetic.scan takes them.

Parameters:

Name Type Description Default
name str

One of SCANNERS ("VZ-400" and similar spellings work).

required

Returns:

Type Description
dict

min_zenith_deg and max_zenith_deg (the vertical field of view, +60 to -40 degrees for these), beam_divergence (mrad), exit_diameter (m, a nominal value) and range_noise (m, the ranging precision), from the manufacturer's data sheets. Check them against your instrument; the angular step is the scan's resolution_deg.

Raises:

Type Description
ValueError

For an unknown model.