py_trees Documentation

repository·devel·Indexed 20 days ago

https://github.com/splintered-reality/py_trees

A pythonic implementation of behaviour trees designed for building decision-making engines, particularly for robotics applications. Version 2.5.0 provides primitives including behaviours, decorators, composites (Selector, Sequence, Parallel), and blackboards for data sharing. It features an abstract ports API, XML-based tree definitions, and support for serializing trees to dot graphs or ASCII/Unicode terminal rendering.

Tokens
30.6K
Snippets
82
Records
141
Agent score
70%

What's inside py_trees

  1. What is PyTrees?

    devel

    PyTrees is a Python implementation of behaviour trees designed for rapid development of medium-sized decision-making engines, commonly used in robotics.

    Core features include:

    • Tree Components: Behaviours, Decorators, Sequences, Selectors, Parallels, and the BehaviourTree itself.
    • Data Sharing: Blackboards for sharing data between nodes.
    • Declarative Definitions: An abstract ports API and XML parser for defining trees.
    • Visualization: Support for serializing to dot graphs or rendering to ASCII/Unicode in a terminal.
    • Platform Support: Tested on Linux and Mac.
  2. Understand the core PyTrees module structure

    devel

    PyTrees is organized into several specialized modules that allow you to build, manage, and visualize behaviour trees:

    • py_trees: The main entry point.
    • py_trees.behaviour: The core template from which all custom behaviours are derived.
    • py_trees.behaviours: A library of pre-built, useful behaviours.
    • py_trees.blackboard: Provides a shared data store for communication between behaviours.
    • py_trees.composites: Contains behaviours that manage child nodes (e.g., Sequences, Selectors).
    • py_trees.decorators: Provides 'hats' (wrappers) for behaviours to modify their execution logic.
    • py_trees.idioms: Contains creators for common behaviour tree patterns.
    • py_trees.meta: Provides factories for creating behaviours.
    • py_trees.trees: Contains tree managers to handle tree execution and lifecycle.
    • py_trees.display: Tools for visualizing trees via DOT graphs, strings, or stdout.
    • py_trees.timers: Timer-related behaviours.
    • py_trees.visitors: Entities that traverse the tree to inspect or modify behaviours during execution.
    • py_trees.console: Utilities for console colour and syntax highlighting.
    • py_trees.common: Common definitions, methods, and enumerations.
    • py_trees.utilities: Assorted utility functions.
  3. What is a blocking behaviour?

    devel
    In PyTrees, a behaviour is considered blocking if its progress from RUNNING to SUCCESS or FAILURE takes more than one tick. Technically, the execution of a behaviour (the tick part) should always be non-blocking, but the logical duration of the task it represents is described as blocking if it persists across multiple ticks.
  4. Design constraints of py_trees

    devel

    The py_trees implementation is designed for rapid development in Python using generators and decorators. To maintain a practical, easy-to-use, and lock-free framework, the following design constraints are assumed:

    • No data sharing: There is no interaction or sharing of data between different tree instances.
    • No parallel execution: Only one behaviour is allowed to be initialising or executing at any given time (no parallelisation of tree execution).
  5. What are Ports and why use them?

    devel

    Ports provide a structured way to wire data exchange between nodes in a tree. Instead of nodes performing ad-hoc reads and writes to the blackboard, they declare explicit input and output ports.

    Key Benefits:

    • Explicit Data Contracts: A node's input_ports() and output_ports() declarations serve as its public data-flow API.
    • Early Error Detection: Port values are type-checked at runtime (TypeError on mismatch). Missing required inputs raise py_trees.ports.NoDataAvailable instead of returning None.
    • Subtree Isolation: Sibling subtrees can reuse the same port names internally because the subtree namespace scopes them on the blackboard to prevent collisions.
    • Reusable Subtrees: Subtrees can be reconfigured from the outside by rewiring their port remappings without changing the internal Python code.
    • Refactoring Safety: Changing a blackboard key only requires updating the remapping at setup time, rather than searching and replacing key names throughout your node code.

    Note: The py_trees.ports module and the XML parser are experimental. Their API may change between releases.

  6. Understand the Behaviour lifecycle and method responsibilities

    devel

    The lifecycle of a behaviour is managed by the parent via the tick() method. The key methods and their responsibilities are:

    • __init__: Instantiate the behaviour. Keep this minimal to allow for offline dot graph generation (e.g., in CI). Do not perform hardware connections, middleware connections, or start heavy threads here.
    • setup(): Handles one-time initialisation of heavy resources required for execution (e.g., hardware connections, ROS publishers/subscribers).
    • initialise(): Configures and resets the behaviour for (repeated) execution. Use this for resetting variables, starting timers, or establishing just-in-time middleware connections.
    • update(): The core logic. It must return a py_trees.common.Status. Crucially, update() must never block. It should monitor progress and return RUNNING if the task is ongoing.
    • terminate(): Called when the behaviour is finished or cancelled to perform cleanup.
  7. Use Feedback Messages for significant events

    devel

    Behaviours have a built-in feedback message that can be updated in update() and cleared in initialise() or terminate().

    Best Practice: Only update the feedback message when significant events occur. Avoid updating it every tick (e.g., don't log every coordinate change in a moving character), as this creates excessive noise. Use it to notify humans of state changes or to provide context for a SUCCESS or FAILURE (e.g., the reason for a failure).

    # Example of updating feedback message
    def update(self):
        if self.task_is_done:
            self.feedback_message = "Task completed successfully"
            return py_trees.common.Status.SUCCESS
        elif self.error_occurred:
            self.feedback_message = "Error: Sensor timeout"
            return py_trees.common.Status.FAILURE
        return py_trees.common.Status.RUNNING
  8. How to extend the Ports framework with port-aware wrappers

    devel

    The py_trees.ports framework provides the mechanism for typed input/output ports but does not ship concrete port-aware behaviours, decorators, or composites. To make an existing upstream class (like py_trees.decorators.Retry) port-aware, you must create an adapter class that combines PortsMixin with the upstream class.

    Implementation Pattern:

    1. Inheritance: Inherit from both PortsMixin and the upstream class.
    2. Port Definition: Implement input_ports() and output_ports() class methods using PortInformation to define the schema.
    3. Initialization: In __init__, call super().__init__ with safe default values.
    4. Runtime Updates: In initialise(), use self.get_input("port_name") to read the current port value and apply it to the upstream class's attributes before calling super().initialise().
    5. Namespace: To avoid naming collisions, place your ported class in a ports submodule that mirrors the upstream layout (e.g., yourproject.ports.decorators.Retry).
    import py_trees
    from py_trees.ports import PortInformation, PortsMixin
    
    class Retry(PortsMixin, py_trees.decorators.Retry):
        """Retry that reads its failure budget from an input port."""
    
        @classmethod
        def input_ports(cls):
            return {"num_failures": PortInformation(data_type=int, required=True)}
    
        @classmethod
        def output_ports(cls):
            return {}
    
        def __init__(
            self, 
            name: str, 
            child: py_trees.behaviour.Behaviour, 
            **kwargs
        ):
            # Start with a safe default; it is overwritten on every initialise().
            super().__init__(
                name=name, child=child, num_failures=1, **kwargs
            )
    
        def initialise(self) -> None:
            # Read the port value at tick boundary and apply it before
            # the upstream Retry logic runs.
            self.num_failures = self.get_input("num_failures")
            super().initialise()
  9. What are Behaviour Trees and when to use them

    devel

    Behaviour trees are a decision-making engine used for purposeful planning with high reactivity. They are particularly suited for medium-scale decision engines (hundreds of behaviours) that do not require real-time, low-latency safety responses (latency of ~50-200ms is typically sufficient).

    Key Features:

    • Ticking: The ability to tick allows for work between executions without requiring multi-threading.
    • Priority Handling: Natural mechanisms for higher-priority interruptions (often called reactivity).
    • Simplicity: A small number of core components makes them easy for designers to use.
    • Scalability: They avoid the combinatorial explosion seen in finite state machines as complexity increases.
    • Dynamic: The tree graph can be changed on the fly between ticks or by parent behaviours.

    Use Cases: Ideal for high-level scenario/application layers in robotics, such as navigation context switching, docking/undocking processes, and interaction logic (LEDs, sound), where low-latency reactive safety is handled by a separate control layer.

  10. Understand XML attributes: Ports vs Constructor Arguments

    devel

    Attributes on an XML node tag serve two different purposes depending on whether they match a declared port:

    1. Port Remappings: If an attribute name matches a key in input_ports() or output_ports(), it is treated as a port remapping. Values can be {curly_key} references (to wire ports) or literal constants (which are type-converted).
    2. Constructor Keyword Arguments: If an attribute name does not match a declared port, it is passed as a keyword argument to the class __init__ method. Values are type-converted based on the constructor's type annotations. Note: {curly_key} references are NOT allowed for constructor arguments and will raise a ValueError.

    Example Mapping

    <Greeting name="hello_node" name_key="{target}" prefix="Howdy"/>
    • name="hello_node": Sets the behaviour name.
    • name_key="{target}": A port remap (matches input_ports).
    • prefix="Howdy": A constructor kwarg (does not match any port; passed to __init__).