Behaviours

A Behaviour is the smallest element in a behaviour tree, i.e. it is the leaf. Behaviours are usually representative of either a check (am I hungry?), or an action (buy some chocolate cookies).

Skeleton

Behaviours in py_trees are created by subclassing the Behaviour class. A skeleton with informative comments is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# doc/examples/skeleton_behaviour.py

import py_trees
import random


class Foo(py_trees.Behaviour):
    def __init__(self, name):
        """
        Minimal one-time initialisation. A good rule of thumb is
        to only include the initialisation relevant for being able
        to insert this behaviour in a tree for offline rendering to
        dot graphs.

        Other one-time initialisation requirements should be met via
        the setup() method.
        """
        super(Foo, self).__init__(name)

    def setup(self, timeout):
        """
        When is this called?
          This function should be either manually called by your program
          or indirectly called by a parent behaviour when it's own setup
          method has been called.

          If you have vital initialisation here, a useful design pattern
          is to put a guard in your initialise() function to barf the
          first time your behaviour is ticked if setup has not been
          called/succeeded.

        What to do here?
          Delayed one-time initialisation that would otherwise interfere
          with offline rendering of this behaviour in a tree to dot graph.
          Good examples include:
          - Hardware or driver initialisation
          - Middleware initialisation (e.g. ROS pubs/subs/services)
        """
        self.logger.debug("  %s [Foo::setup()]" % self.name)

    def initialise(self):
        """
        When is this called?
          The first time your behaviour is ticked and anytime the
          status is not RUNNING thereafter.

        What to do here?
          Any initialisation you need before putting your behaviour
          to work.
        """
        self.logger.debug("  %s [Foo::initialise()]" % self.name)

    def update(self):
        """
        When is this called?
          Every time your behaviour is ticked.

        What to do here?
          - Triggering, checking, monitoring. Anything...but do not block!
          - Set a feedback message
          - return a py_trees.Status.[RUNNING, SUCCESS, FAILURE]
        """
        self.logger.debug("  %s [Foo::update()]" % self.name)
        ready_to_make_a_decision = random.choice([True, False])
        decision = random.choice([True, False])
        if not ready_to_make_a_decision:
            return py_trees.Status.RUNNING
        elif decision:
            self.feedback_message = "We are not bar!"
            return py_trees.Status.SUCCESS
        else:
            self.feedback_message = "Uh oh"
            return py_trees.Status.FAILURE

    def terminate(self, new_status):
        """
        When is this called?
           Whenever your behaviour switches to a non-running state.
            - SUCCESS || FAILURE : your behaviour's work cycle has finished
            - INVALID : a higher priority branch has interrupted, or shutting down
        """
        self.logger.debug("  %s [Foo::terminate().terminate()][%s->%s]" % (self.name, self.status, new_status))

Lifecycle

Getting a feel for how this works in action can be seen by running the py-trees-demo-behaviour-lifecycle program (click the link for more detail and access to the sources):

_images/lifecycle.gif

Important points to focus on:

  • The initialise() method kicks in only when the behaviour is not already running
  • The parent tick() method is responsible for determining when to call initialise(), stop() and terminate() methods.
  • The parent tick() method always calls update()
  • The update() method is responsible for deciding the behaviour Status.

Initialisation

With no less than three methods used for initialisation, it can be difficult to identify where your initialisation code needs to lurk.

Note

__init__ should instantiate the behaviour sufficiently for offline dot graph generation

Later we’ll see how we can render trees of behaviours in dot graphs. For now, it is sufficient to understand that you need to keep this minimal enough so that you can generate dot graphs for your trees from something like a CI server (e.g. Jenkins). This is a very useful thing to be able to do.

  • No hardware connections that may not be there, e.g. usb lidars
  • No middleware connections to other software that may not be there, e.g. ROS pubs/subs/services
  • No need to fire up other needlessly heavy resources, e.g. heavy threads in the background

Note

setup handles all other one-time initialisations of resources that are required for execution

Essentially, all the things that the constructor doesn’t handle - hardware connections, middleware and other heavy resources.

Note

initialise configures and resets the behaviour ready for (repeated) execution

Initialisation here is about getting things ready for immediate execution of a task. Some examples:

  • Initialising/resetting/clearing variables
  • Starting timers
  • Just-in-time discovery and establishment of middleware connections
  • Sending a goal to start a controller running elsewhere on the system

Status

The most important part of a behaviour is the determination of the behaviour’s status in the update() method. The status gets used to affect which direction of travel is subsequently pursued through the remainder of a behaviour tree. We haven’t gotten to trees yet, but it is this which drives the decision making in a behaviour tree.

class py_trees.common.Status[source]

An enumerator representing the status of a behaviour

FAILURE = 'FAILURE'

Behaviour check has failed, or execution of its action finished with a failed result.

INVALID = 'INVALID'

Behaviour is uninitialised and inactive, i.e. this is the status before first entry, and after a higher priority switch has occurred.

RUNNING = 'RUNNING'

Behaviour is in the middle of executing some action, result still pending.

SUCCESS = 'SUCCESS'

Behaviour check has passed, or execution of its action has finished with a successful result.

The update() method must return one of RUNNING. SUCCESS or FAILURE. A status of INVALID is the initial default and ordinarily automatically set by other mechansims (e.g. when a higher priority behaviour cancels the currently selected one).

Feedback Message

1
2
3
4
    def initialise(self):
        """
        Reset a counter variable.
        """

A behaviour has a naturally built in feedback message that can be cleared in the initialise() or terminate() methods and updated in the update() method.

Tip

Alter a feedback message when significant events occur.

The feedback message is designed to assist in notifying humans when a significant event happens or for deciding when to log the state of a tree. If you notify or log every tick, then you end up with alot of noise sorting through an abundance of data in which nothing much is happening to find the one point where something significant occurred that led to surprising or catostrophic behaviour.

Setting the feedback message is usually important when something significant happens in the RUNNING state or to provide information associated with the result (e.g. failure reason).

Example - a behaviour responsible for planning motions of a character is in the RUNNING state for a long period of time. Avoid updating it with a feedback message at every tick with updated plan details. Instead, update the message whenever a significant change occurs - e.g. when the previous plan is re-planned or pre-empted.

Loggers

These are used throughout the demo programs. They are not intended to be for anything heavier than debugging simple examples. This kind of logging tends to get rather heavy and requires alot of filtering to find the points of change that you are interested in (see comments about the feedback messages above).

Complex Example

The py-trees-demo-action-behaviour program demonstrates a more complicated behaviour that illustrates a few concepts discussed above, but not present in the very simple lifecycle Counter behaviour.

  • Mocks an external process and connects to it in the setup method
  • Kickstarts new goals with the external process in the initialise method
  • Monitors the ongoing goal status in the update method
  • Determines RUNNING/SUCCESS pending feedback from the external process

Note

A behaviour’s update() method never blocks, at most it just monitors the progress and holds up any decision making required by a tree that is ticking the behaviour by setting it’s status to RUNNING. At the risk of being confusing, this is what is generally referred to as a blocking behaviour.

_images/action.gif

Meta Behaviours

Attention

This module is the least likely to remain stable in this package. It has only received cursory attention so far and a more thoughtful design for handling behaviour ‘hats’ might be needful at some point in the future.

Meta behaviours are created by utilising various programming techniques pulled from a magic bag of tricks. Some of these minimise the effort to generate a new behaviour while others provide mechanisms that greatly expand your library of usable behaviours without having to increase the number of explicit behaviours contained therein. The latter is achieved by providing a means for behaviours to wear different ‘hats’ via python decorators.

_images/many-hats.png

Each function or decorator listed below includes its own example code demonstrating its use.

Factories

Decorators (Hats)