Skip to content

Solving Problems With Trees

Ben Talbot edited this page Oct 20, 2021 · 2 revisions

Solving problems with behaviour trees can be challenging at first, but can give rise to much more elegant and maintainable systems for complex problems. We think of solving a problem in behaviour trees as a task we want to complete.

A task refers to something distinct we want the robot to do, which often requires the purposeful interweaving of many distinct components. Producing software to perform complex tasks has a number of pain points like:

  • Components can often come from many different internal and external authors
  • Bringing software together from different research areas, with different authors, and different conventions is non-trivial

What does it mean to define a solution to a task

We consider defining solutions to a robotic task in terms of three distinct types of components:

  • capabilities: are what functionalities required from the robot to complete the task (e.g. moving the arm to a pose, driving to a pose, detecting objects, saying a string, etc.),
  • sub-behaviours: define how a group of capabilities combine in a way that is reusable for a wide variety of tasks (examples: picking up an object, asking a question, going through a door, etc.), and
  • behaviours: are the cumulative definition for how a robot can apply its available capabilities to complete a given task (e.g. voice-controlled manipulation demo on the tabletop panda arm, going and making a coffee for a user, emptying all of the garbage bins, etc.).

The distinction between sub-behaviour and behaviour can often be confusing, and is somewhat arbitrary. A good rule of thumb is if the behaviour you are creating is too general to be locked down to a single tree (i.e. can be parameterised and would apply to a number of different tasks), and it requires the combination of too many distinct re-usable parts to be a single robot capability, then it should be a sub-behaviour.

Translating the solution into behaviour tree components

In ROS Trees we use behaviour trees to define a behaviour required for a robot to complete a task. Unsurprisingly given the name, behaviour trees define an entire agent behaviour using a tree structure consisting of nodes and edges:

  • Child-less nodes correspond to one of the agent's defined capabilities,
  • Nodes with children typically control flow throughout the tree, and
  • edges define a parent that controls when & how the node is called.

More information on how behaviour trees work can be found in the resources section at the bottom of the page.

Behaviour trees have a number of advantages over Finite State Machines (FSM), which are what is typically employed in robot systems. The main advantage applicable for our robots is behaviour trees allow us to detach behaviour (how bits of functionality combine together to do something meaningful) from functionality (things a robot can do). Detaching behaviour from functionality means that robot capabilities can be re-used across any task, and complex behaviours can be created without worrying about implementation details. This allows developers to focus on one part of the system at a time (i.e. giving a robot a useful capability, or making the robot solve a meaningful task). For example, the functionality to detect whether a door is open or closed can be implemented once, then re-used in other behaviours (waiting for a door to be opened, waiting for a door to be closed, checking if a door is closed, only opening the door if it is closed, etc.) simply by changing the structure of the tree.

We implement a solution for task with a behaviour tree by mapping the three components above to distinct parts of a tree:

  • a leaf wraps a functionality of the robot, defining how the tree executes the capability and receives the result once complete,
  • a branch is a re-usable tree sub-section encapsulating a parameterisable behaviour which could be reused across multiple different tasks, and
  • a tree is the definition of how a robot can interleave its capabilities to solve a task.

Solving a task boils down to three tightly coupled steps which can be done in any order:

  • Writing leaves for each of the required robot capabilities (this step requires you to break down the task into what capabilities the robot requires to complete the task)
  • Defining re-usable branches to solve repeated / reusable sections of the task (sometimes your task may not require this, but it always helps to define branches if you think they would be useful for another task)
  • Writing a tree for the behaviour that solves the task (often when completing this step, you will end up having to go back and add parts to the previous steps)

All this seems pretty abstract and "hand-wavey", but there is a full example in the getting started tutorial of how the above principles can be applied to solve a non-trivial task.

Quick & Easy: Solving a task with a tree

Here's the 1,2,3 TL;DR for creating a tree to solve a task:

  1. Create some leaves:

    from ros_trees.leaves_ros import ActionLeaf, ServiceLeaf
    
    class DeriveSolution(ServiceLeaf):
        def __init__(self, *args, **kwargs):
            super(DeriveSolution, self).__init__(service_name="/derive_solution", *args, **kwargs)
    
    class ApplySolution(ActionLeaf):
        def __init__(self, *args, **kwargs):
            super(DeriveSolution, self).__init__(action_namespace="/apply_solution", *args, **kwargs)
  2. Join the leaves up in a tree that solves a task:

    from py_trees.composite import Sequence
    from ros_trees.trees import BehaviourTree
    
    my_tree = BehaviourTree("Solve Task", Sequence("Solve", [DeriveSolution(), ApplySolution()]))
  3. Run the tree:

    my_tree.run(hz=30)

Other helpful commands:

  • Showing an interactive tree with rqt:

    rqt_py_trees
    
  • Printing trees interactively in the terminal:

    rosrun py_trees_ros py-trees-tree-watcher
    
  • Printing blackboard variable values interactively:

    rosrun py_trees_ros py-trees-blackboard-watcher
    
  • Getting a static GraphViz graphic for your tree:

    from ros_trees.trees import BehaviourTree
    # Create your tree in a variable called 'my_tree'
    my_tree.visualise()

Conventions & best practices

Below are some conventions & best practices we encourage everyone to use. These conventions help you create solutions that work on everyone's robots, maximising the benefit your work can provide in the future for yourself and others:

  • Use existing leaves and branches where possible (maximising reuse promotes greater consistency & robustness of our systems)
  • A leaf should be as input-agnostic as possible (e.g. writing a leaf to only work with a 3x1 numpy array of Float64s when in reality it could work with any iterable of 3 numbers only hinders future use of your leaf)
  • Extend functionality in your own class rather than messing with the base Leaf class or classes in the ros_trees.leaves* modules

Useful links and information