Forest Tending Python SDK Developer Guide
Development Environment
The built-in Python environment is located at:
python310/python.exe
Scripts depend on forest_tending_sdk, which is generated from C++ bindings through pybind11. The current design uses strong binding, so the Python side directly reuses the input, output, fields, parameters, and control structures defined in C++.
Recommended script location:
Example script:
bin/PythonScript/crope-tree-selection-script.py
Minimum Script Template
from __future__ import annotations
from forest_tending_sdk import ForestTendingHook, HookInput, HookOutput
class CustomTendingHook(ForestTendingHook):
fields = []
parameters = []
def attribute(self, ctx: HookInput) -> HookOutput:
return HookOutput.attribute_result("no dynamic attributes")
def select_targets(self, ctx: HookInput) -> HookOutput:
out = HookOutput.target_selection_result("target selection completed")
for candidate in ctx.target_candidates:
out.add_target(
candidate.tree_id,
mandatory=False,
priority=candidate.priority,
score=candidate.score,
reason=candidate.reason,
tags=["rule_target"],
)
return out
def select_harvest(self, ctx: HookInput) -> HookOutput:
out = HookOutput.harvest_selection_result("harvest selection completed")
for candidate in ctx.harvest_candidates:
out.add_harvest_candidate(
candidate.tree_id,
priority=candidate.priority,
score=candidate.score,
reason=candidate.reason,
tags=["rule_harvest"],
)
return out
def validate(self, ctx: HookInput) -> HookOutput:
return HookOutput.validation_result("validation completed")
if __name__ == "__main__":
raise SystemExit(CustomTendingHook().main())
The final main() entry must be retained. When C++ executes the script, it passes the input JSON path and output JSON path.
Four Stages
attribute(ctx)
Used to calculate dynamic attributes.
Input:
ctx.stand: Stand information.ctx.trees: All individual trees.ctx.parameters: Script parameters.
Output:
out = HookOutput.attribute_result("attributes completed")
out.add_attribute(tree.tree_id, {
"python_vigor_score": 0.82,
"python_is_crowded_tree": True,
})
return out
Dynamic attribute fields must be declared in the class fields in advance before they can appear in the rule configurator.
select_targets(ctx)
Used to confirm final reserve trees.
Input:
ctx.target_candidates: Candidates generated by C++ retention rules.ctx.trees: All individual trees.ctx.parameters: Script parameters.
Output:
out = HookOutput.target_selection_result("targets selected")
out.add_target(
tree_id,
mandatory=True,
priority=0,
score=0.95,
reason="high quality reserve tree",
tags=["reserve"],
)
return out
mandatory indicates that the script regards this tree as a mandatory reserve tree. Subsequent harvesting rules will not process final reserve trees.
select_harvest(ctx)
Used to filter and rank harvesting candidates.
Input:
ctx.target_tree_ids: Final reserve tree IDs.ctx.harvest_candidates: Candidates generated by C++ harvesting rules.ctx.trees: All individual trees.ctx.parameters: Script parameters.
Output:
out = HookOutput.harvest_selection_result("harvest candidates ranked")
out.add_harvest_candidate(
tree_id,
priority=rank,
score=score,
reason="competitor around target tree",
affected_target_tree_ids=[target_id],
tags=["target_release"],
)
return out
The output here is an ordered harvesting candidate list. C++ continues to accept or skip trees one by one according to the built-in control indicators.
validate(ctx)
Used to check the final scheme.
Input:
ctx.target_tree_ids: Final reserve tree IDs.ctx.plan.reserveTreeIds: Final reserve tree set.ctx.plan.harvestTreeIds: Final harvested tree set.ctx.plan.metrics: Final scheme metrics.ctx.control_rules: Control indicator rules.
Output:
out = HookOutput.validation_result("post validation completed")
out.add_issue(
"low_reserve_count",
"warning",
"Reserve tree count is lower than expected.",
tree_ids=[],
)
return out
Built-in Fields
The SDK exposes C++ built-in fields through static classes. This access method is recommended because it supports auto-completion and unified maintenance.
from forest_tending_sdk import TreeFields, StandFields
DBH = TreeFields.DBH
HEIGHT = TreeFields.TREE_HEIGHT
CROWN_HEIGHT_RATIO = TreeFields.CROWN_HEIGHT_RATIO
AREA = StandFields.AREA
Read individual tree fields:
dbh = tree.number(TreeFields.DBH, 0.0)
species = tree.string(TreeFields.TREESPECIES, "")
Read stand fields:
area = ctx.stand.number(StandFields.AREA, 0.0)
dominant_height = ctx.stand.number(StandFields.DOMINANT_HEIGHT, 0.0)
View fields:
TreeFields.names()
TreeFields.all()
TreeFields.contains("DBH")
TreeFields.get("DBH")
Common individual tree fields include:
TreeFields.TREE_HEIGHTTreeFields.DBHTreeFields.CROWN_DIAMETERTreeFields.CROWN_AREATreeFields.CROWN_HEIGHT_RATIOTreeFields.CROWN_ASYMMETRYTreeFields.HEIGHT_DIAMETER_RATIOTreeFields.STRAIGHTNESSTreeFields.IS_FORKEDTreeFields.TREESPECIES
Common stand fields include:
StandFields.AREAStandFields.NUM_TREESStandFields.AVERAGE_DBH_CMStandFields.AVERAGE_HEIGHTStandFields.DOMINANT_HEIGHTStandFields.SUM_BASAL_AREAStandFields.AVERAGE_NEAREST_NEIGHBOR_DISTANCE
Custom Dynamic Fields
If the rule configurator needs to use script calculation results, declare fields.
from forest_tending_sdk import Field, FieldType, ForestTendingHook
VIGOR_SCORE = Field("python_vigor_score", "Vigor Score", FieldType.Number)
IS_CROWDED = Field("python_is_crowded_tree", "Is Crowded Tree", FieldType.Boolean)
class CustomHook(ForestTendingHook):
fields = [VIGOR_SCORE, IS_CROWDED]
Field requirements:
keycannot be empty.keycannot contain..aliasis used for display in the interface.keyis used for rule saving and script output.- Dynamic attributes do not need units.
Script Parameters
Script parameters are declared in the class parameters, and the interface automatically generates controls.
from forest_tending_sdk import Parameter
COMPETITION_RADIUS = Parameter.number(
"competition_radius",
"Competition Radius",
8.0,
minimum=1.0,
maximum=50.0,
step=0.5,
unit="m",
group="Harvest Ranking",
description="Neighborhood radius used to identify competitors.",
)
MAX_RELEASE = Parameter.integer(
"max_release_per_target",
"Maximum Release per Target",
2,
minimum=0,
maximum=8,
step=1,
group="Harvest Ranking",
)
class CustomHook(ForestTendingHook):
parameters = [COMPETITION_RADIUS, MAX_RELEASE]
Read parameters:
radius = ctx.parameters.number(COMPETITION_RADIUS)
max_release = ctx.parameters.integer(MAX_RELEASE)
Supported types:
Parameter.number()Parameter.integer()Parameter.boolean()Parameter.string()Parameter.enumeration()
Parameter keys cannot contain ., and English underscore naming is recommended.
Target Tree Competitor Calculation
Whether a tree competes with a target tree can only be determined after final target trees have been confirmed. Therefore, it is recommended to use ctx.target_tree_ids in select_harvest().
from forest_tending_sdk import radius_neighbor_map
def select_harvest(self, ctx: HookInput) -> HookOutput:
out = HookOutput.harvest_selection_result("target competitors ranked")
trees = [tree for tree in ctx.trees if tree.tree_id > 0]
target_ids = set(ctx.target_tree_ids)
allowed_ids = {candidate.tree_id for candidate in ctx.harvest_candidates}
neighbors = radius_neighbor_map(trees, 8.0)
ranked = []
for tree in trees:
if tree.tree_id not in allowed_ids or tree.tree_id in target_ids:
continue
affected = [other.tree_id for other, _ in neighbors[tree.tree_id] if other.tree_id in target_ids]
if not affected:
continue
ranked.append((len(affected), tree.tree_id, affected))
ranked.sort(reverse=True)
for rank, (_, tree_id, affected) in enumerate(ranked):
out.add_harvest_candidate(
tree_id,
priority=rank,
score=float(len(affected)),
reason="target competitor",
affected_target_tree_ids=affected,
tags=["target_competitor"],
)
return out
Do not calculate whether a tree competes with a target tree in the earliest attribute() stage, because final target trees have not yet been determined at that time.
Custom Control Rules
General business scripts usually do not need custom control rules. Prefer the built-in C++ control indicators. Built-in control indicators include:
METRIC_HARVEST_PERCENTMETRIC_BASAL_AREA_REMOVAL_PERCENTMETRIC_CROWN_CLOSURE_AFTERMETRIC_CROWN_CLOSURE_CHANGEMETRIC_MAX_CANOPY_GAP_AREA
If the script really needs to provide additional control rules, add control rules and candidate tree metrics to the output of select_harvest(). C++ executes them in the same per-tree control process.
out.add_harvest_control_rule(
"custom_competition_release",
"Custom Competition Release",
"less_equal",
10.0,
severity="hard",
reason="limit released competitors by script metric",
)
out.add_harvest_candidate(
tree_id,
priority=rank,
score=score,
metrics={"custom_competition_release": metric_value},
)
Unless the built-in control indicators cannot express the requirement, do not duplicate general indicators such as harvesting intensity, basal area, and crown closure in Python.
Schema and Interface Loading
The interface reads the script schema with the following command:
bin/python310/python.exe your_script.py --lidar360-schema
The output includes:
fields: Dynamic attribute fields.parameters: Script parameters.schemaVersion: Field schema version.sdkAbiVersion: SDK ABI version.
You can manually check whether the script can be recognized by the interface:
bin/python310/python.exe bin/PythonScript/crope-tree-selection-script.py --lidar360-schema
Type Hints and Auto-completion
The SDK includes py.typed and .pyi type files. When VSCode uses the project Python environment, it should recognize:
HookInputHookOutputTreeFieldsStandFieldsTreeStandInfo
If auto-completion is unavailable, check:
- Whether the interpreter selected in VSCode is
bin/python310/python.exe. - Whether
forest_tending_sdkis in the search path of that interpreter. - Whether the generated
_native.pyiand the package__init__.pyiare consistent with the current binding. - Whether
TreeFieldsis accessed through static fields, such asTreeFields.DBH.
Debugging Suggestions
It is recommended to perform three checks first:
- Compilation check:
python310/python.exe -m py_compile bin/PythonScript/your_script.py
- Schema check:
python310/python.exe bin/PythonScript/your_script.py --lidar360-schema
- Workflow check:
Run the tool on a small sample plot in the interface, and check whether reserve trees, harvesting candidates, final harvested trees, and control indicator warnings meet expectations.
Development Conventions
- Prefer
TreeFields.*andStandFields.*to access built-in fields. - Use dynamic fields only for script-added attributes. Do not redeclare existing C++ fields.
- In selection stages, output only business ranking and reasons. Do not bypass built-in C++ control indicators.
select_targets()is responsible for final reserve trees.select_harvest()is responsible for harvesting candidate ranking.validate()only checks the final scheme. It is not recommended to rewrite harvesting results here.- Field and parameter keys should use English underscores and should not use
..