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_HEIGHT
  • TreeFields.DBH
  • TreeFields.CROWN_DIAMETER
  • TreeFields.CROWN_AREA
  • TreeFields.CROWN_HEIGHT_RATIO
  • TreeFields.CROWN_ASYMMETRY
  • TreeFields.HEIGHT_DIAMETER_RATIO
  • TreeFields.STRAIGHTNESS
  • TreeFields.IS_FORKED
  • TreeFields.TREESPECIES

Common stand fields include:

  • StandFields.AREA
  • StandFields.NUM_TREES
  • StandFields.AVERAGE_DBH_CM
  • StandFields.AVERAGE_HEIGHT
  • StandFields.DOMINANT_HEIGHT
  • StandFields.SUM_BASAL_AREA
  • StandFields.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:

  • key cannot be empty.
  • key cannot contain ..
  • alias is used for display in the interface.
  • key is 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_PERCENT
  • METRIC_BASAL_AREA_REMOVAL_PERCENT
  • METRIC_CROWN_CLOSURE_AFTER
  • METRIC_CROWN_CLOSURE_CHANGE
  • METRIC_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:

  • HookInput
  • HookOutput
  • TreeFields
  • StandFields
  • Tree
  • StandInfo

If auto-completion is unavailable, check:

  • Whether the interpreter selected in VSCode is bin/python310/python.exe.
  • Whether forest_tending_sdk is in the search path of that interpreter.
  • Whether the generated _native.pyi and the package __init__.pyi are consistent with the current binding.
  • Whether TreeFields is accessed through static fields, such as TreeFields.DBH.

Debugging Suggestions

It is recommended to perform three checks first:

  1. Compilation check:
python310/python.exe -m py_compile bin/PythonScript/your_script.py
  1. Schema check:
python310/python.exe bin/PythonScript/your_script.py --lidar360-schema
  1. 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.* and StandFields.* 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 ..

results matching ""

    No results matching ""