Source code for py_trees.composites

#!/usr/bin/env python
#
# License: BSD
#   https://raw.githubusercontent.com/stonier/py_trees/devel/LICENSE
#
##############################################################################
# Documentation
##############################################################################

"""
Composites are the **factories** and **decision makers** of a
behaviour tree. They are responsible for shaping the branches.

.. graphviz:: dot/composites.dot

.. tip:: You should never need to subclass or create new composites.

Most patterns can be achieved with a combination of the above. Adding to this
set exponentially increases the complexity and subsequently
making it more difficult to design, introspect, visualise and debug the trees. Always try
to find the combination you need to achieve your result before contemplating adding
to this set. Actually, scratch that...just don't contemplate it!

Composite behaviours typically manage children and apply some logic to the way
they execute and return a result, but generally don't do anything themselves.
Perform the checks or actions you need to do in the non-composite behaviours.

* :class:`~py_trees.composites.Sequence`: execute children sequentially
* :class:`~py_trees.composites.Selector`: select a path through the tree, interruptible by higher priorities
* :class:`~py_trees.composites.Chooser`: like a selector, but commits to a path once started until it finishes
* :class:`~py_trees.composites.Parallel`: manage children concurrently
"""

##############################################################################
# Imports
##############################################################################

import itertools

from . import common
from .behaviour import Behaviour
from .common import Status

##############################################################################
# Composites
##############################################################################


[docs]class Composite(Behaviour): """ The parent class to all composite behaviours, i.e. those that have children. Args: name (:obj:`str`): the composite behaviour name children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add *args: variable length argument list **kwargs: arbitrary keyword arguments """
[docs] def __init__(self, name="", children=None, *args, **kwargs): super(Composite, self).__init__(name, *args, **kwargs) if children is not None: for child in children: self.add_child(child) else: self.children = []
############################################ # Worker Overrides ############################################
[docs] def setup(self, timeout): """ Relays to each child's :meth:`~py_trees.behaviour.Behaviuor.setup` method in turn. Args: timeout (:obj:`float`): time to wait (0.0 is blocking forever) Return: :obj:`bool`: suceess or failure of the operation """ self.logger.debug("%s.setup()" % (self.__class__.__name__)) result = True for child in self.children: new_result = child.setup(timeout) if new_result is None: # replace with py_trees exception! self.logger.error("%s.setup()['%s'.setup() returned None (must be True||False)]" % (self.__class__.__name__, child.name)) result = result and new_result if not result: break return result
[docs] def stop(self, new_status=Status.INVALID): """ There is generally two use cases that must be supported here. 1) Whenever the composite has gone to a recognised state (i.e. :data:`~py_trees.common.Status.FAILURE` or SUCCESS), or 2) when a higher level parent calls on it to truly stop (INVALID). In only the latter case will children need to be forcibly stopped as well. In the first case, they will have stopped themselves appropriately already. Args: new_status (:class:`~py_trees.common.Status`): behaviour will transition to this new status """ self.logger.debug("%s.stop()[%s]" % (self.__class__.__name__, "%s->%s" % (self.status, new_status) if self.status != new_status else "%s" % new_status)) if new_status == Status.INVALID: for child in self.children: child.stop(new_status) # This part just replicates the Behaviour.stop function. We replicate it here so that # the Behaviour logging doesn't duplicate the composite logging here, just a bit cleaner this way. self.terminate(new_status) self.status = new_status self.iterator = self.tick()
[docs] def tip(self): """ Recursive function to extract the last running node of the tree. Returns: :class::`~py_trees.behaviour.Behaviour`: the tip function of the current child of this composite or None """ return self.current_child.tip() if self.current_child is not None else None
############################################ # Children ############################################
[docs] def add_child(self, child): """ Adds a child. Args: child (:class:`~py_trees.behaviour.Behaviour`): child to add Returns: uuid.UUID: unique id of the child """ assert isinstance(child, Behaviour), "children must be behaviours, but you passed in %s" % type(child) self.children.append(child) child.parent = self return child.id
[docs] def add_children(self, children): """ Append a list of children to the current list. Args: children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add """ for child in children: self.add_child(child)
[docs] def remove_child(self, child): """ Remove the child behaviour from this composite. Args: child (:class:`~py_trees.behaviour.Behaviour`): child to delete Returns: :obj:`int`: index of the child that was removed .. todo:: Error handling for when child is not in this list """ if child.status == Status.RUNNING: child.stop(Status.INVALID) child_index = self.children.index(child) self.children.remove(child) return child_index
[docs] def remove_all_children(self): """ Remove all children. Makes sure to stop each child if necessary. """ for child in self.children: if child.status == Status.RUNNING: child.stop(Status.INVALID) # makes sure to delete it for this class and all references to it # http://stackoverflow.com/questions/850795/clearing-python-lists del self.children[:]
[docs] def replace_child(self, child, replacement): """ Replace the child behaviour with another. Args: child (:class:`~py_trees.behaviour.Behaviour`): child to delete replacement (:class:`~py_trees.behaviour.Behaviour`): child to insert """ if child.status == Status.RUNNING: child.stop(Status.INVALID) child_index = self.children.index(child) self.logger.debug("%s.replace_child()[%s->%s]" % (self.__class__.__name__, child.name, replacement.name)) self.children[child_index] = replacement
[docs] def remove_child_by_id(self, child_id): """ Remove the child with the specified id. Args: child_id (uuid.UUID): unique id of the child Raises: IndexError: if the child was not found """ child = next((c for c in self.children if c.id == child_id), None) if child is not None: if child.status == Status.RUNNING: child.stop(Status.INVALID) self.children.remove(child) else: raise IndexError('child was not found with the specified id [%s]' % child_id)
[docs] def prepend_child(self, child): """ Prepend the child before all other children. Args: child (:class:`~py_trees.behaviour.Behaviour`): child to insert Returns: uuid.UUID: unique id of the child """ self.children.insert(0, child) child.parent = self return child.id
[docs] def insert_child(self, child, index): """ Insert child at the specified index. This simply directly calls the python list's :obj:`insert` method using the child and index arguments. Args: child (:class:`~py_trees.behaviour.Behaviour`): child to insert index (:obj:`int`): index to insert it at Returns: uuid.UUID: unique id of the child """ self.children.insert(index, child) child.parent = self return child.id
############################################################################## # Selector ##############################################################################
[docs]class Selector(Composite): """ Selectors are the Decision Makers .. graphviz:: dot/selector.dot A selector executes each of its child behaviours in turn until one of them succeeds (at which point it itself returns :data:`~py_trees.common.Status.RUNNING` or :data:`~py_trees.common.Status.SUCCESS`, or it runs out of children at which point it itself returns :data:`~py_trees.common.Status.FAILURE`. We usually refer to selecting children as a means of *choosing between priorities*. Each child and its subtree represent a decreasingly lower priority path. .. note:: Switching from a low -> high priority branch causes a `stop(INVALID)` signal to be sent to the previously executing low priority branch. This signal will percolate down that child's own subtree. Behaviours should make sure that they catch this and *destruct* appropriately. Make sure you do your appropriate cleanup in the :meth:`terminate()` methods! e.g. cancelling a running goal, or restoring a context. .. seealso:: The :ref:`py-trees-demo-selector-program` program demos higher priority switching under a selector. Args: name (:obj:`str`): the composite behaviour name children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add *args: variable length argument list **kwargs: arbitrary keyword arguments """
[docs] def __init__(self, name="Selector", children=None, *args, **kwargs): super(Selector, self).__init__(name, children, *args, **kwargs) self.current_child = None
[docs] def tick(self): """ Run the tick behaviour for this selector. Note that the status of the tick is always determined by its children, not by the user customised update function. Yields: :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children """ self.logger.debug("%s.tick()" % self.__class__.__name__) # Required behaviour for *all* behaviours and composites is # for tick() to check if it isn't running and initialise if self.status != Status.RUNNING: # selectors dont do anything specific on initialisation # - the current child is managed by the update, never needs to be 'initialised' # run subclass (user) handles self.initialise() # run any work designated by a customised instance of this class self.update() previous = self.current_child for child in self.children: for node in child.tick(): yield node if node is child: if node.status == Status.RUNNING or node.status == Status.SUCCESS: self.current_child = child self.status = node.status if previous is None or previous != self.current_child: # we interrupted, invalidate everything at a lower priority passed = False for child in self.children: if passed: if child.status != Status.INVALID: child.stop(Status.INVALID) passed = True if child == self.current_child else passed yield self return # all children failed, set failure ourselves and current child to the last bugger who failed us self.status = Status.FAILURE try: self.current_child = self.children[-1] except IndexError: self.current_child = None yield self
[docs] def stop(self, new_status=Status.INVALID): """ Stopping a selector requires setting the current child to none. Note that it is important to implement this here instead of terminate, so users are free to subclass this easily with their own terminate and not have to remember that they need to call this function manually. Args: new_status (:class:`~py_trees.common.Status`): the composite is transitioning to this new status """ # retain information about the last running child if the new status is # SUCCESS or FAILURE if new_status == Status.INVALID: self.current_child = None Composite.stop(self, new_status)
[docs] def __repr__(self): """ Simple string representation of the object. Returns: :obj:`str`: string representation """ s = "Name : %s\n" % self.name s += " Status : %s\n" % self.status s += " Current : %s\n" % (self.current_child.name if self.current_child is not None else "none") s += " Children: %s\n" % [child.name for child in self.children] return s
############################################################################## # Chooser ##############################################################################
[docs]class Chooser(Selector): """ Choosers are Selectors with Commitment .. graphviz:: dot/chooser.dot A variant of the selector class. Once a child is selected, it cannot be interrupted by higher priority siblings. As soon as the chosen child itself has finished it frees the chooser for an alternative selection. i.e. priorities only come into effect if the chooser wasn't running in the previous tick. .. note:: This is the only composite in py_trees that is not a core composite in most behaviour tree implementations. Nonetheless, this is useful in fields like robotics, where you have to ensure that your manipulator doesn't drop it's payload mid-motion as soon as a higher interrupt arrives. Use this composite sparingly and only if you can't find another way to easily create an elegant tree composition for your task. Args: name (:obj:`str`): the composite behaviour name children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add *args: variable length argument list **kwargs: arbitrary keyword arguments """
[docs] def __init__(self, name="Chooser", children=None, *args, **kwargs): super(Chooser, self).__init__(name, children, *args, **kwargs)
[docs] def tick(self): """ Run the tick behaviour for this chooser. Note that the status of the tick is (for now) always determined by its children, not by the user customised update function. Yields: :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children """ self.logger.debug("%s.tick()" % self.__class__.__name__) # Required behaviour for *all* behaviours and composites is # for tick() to check if it isn't running and initialise if self.status != Status.RUNNING: # chooser specific initialisation # invalidate everything for child in self.children: child.stop(Status.INVALID) self.current_child = None # run subclass (user) initialisation self.initialise() # run any work designated by a customised instance of this class self.update() if self.current_child is not None: # run our child, and invalidate anyone else who may have been ticked last run # (bit wasteful always checking for the latter) for child in self.children: if child is self.current_child: for node in self.current_child.tick(): yield node elif child.status != Status.INVALID: child.stop(Status.INVALID) else: for child in self.children: for node in child.tick(): yield node if child.status == Status.RUNNING or child.status == Status.SUCCESS: self.current_child = child break new_status = self.current_child.status if self.current_child is not None else Status.FAILURE self.stop(new_status) yield self
############################################################################## # Sequence ##############################################################################
[docs]class Sequence(Composite): """ Sequences are the factory lines of Behaviour Trees .. graphviz:: dot/sequence.dot A sequence will progressively tick over each of its children so long as each child returns :data:`~py_trees.common.Status.SUCCESS`. If any child returns :data:`~py_trees.common.Status.FAILURE` or :data:`~py_trees.common.Status.RUNNING` the sequence will halt and the parent will adopt the result of this child. If it reaches the last child, it returns with that result regardless. .. note:: The sequence halts once it sees a child is RUNNING and then returns the result. *It does not get stuck in the running behaviour*. .. seealso:: The :ref:`py-trees-demo-sequence-program` program demos a simple sequence in action. Args: name (:obj:`str`): the composite behaviour name children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add *args: variable length argument list **kwargs: arbitrary keyword arguments """
[docs] def __init__(self, name="Sequence", children=None, *args, **kwargs): super(Sequence, self).__init__(name, children, *args, **kwargs) self.current_index = -1 # -1 indicates uninitialised
[docs] def tick(self): """ Tick over the children. Yields: :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children """ self.logger.debug("%s.tick()" % self.__class__.__name__) if self.status != Status.RUNNING: self.logger.debug("%s.tick() [!RUNNING->resetting child index]" % self.__class__.__name__) # sequence specific handling self.current_index = 0 for child in self.children: # reset the children, this helps when introspecting the tree if child.status != Status.INVALID: child.stop(Status.INVALID) # subclass (user) handling self.initialise() # run any work designated by a customised instance of this class self.update() for child in itertools.islice(self.children, self.current_index, None): for node in child.tick(): yield node if node is child and node.status != Status.SUCCESS: self.status = node.status yield self return self.current_index += 1 # At this point, all children are happy with their SUCCESS, so we should be happy too self.current_index -= 1 # went off the end of the list if we got to here self.stop(Status.SUCCESS) yield self
@property def current_child(self): """ Have to check if there's anything actually running first. Returns: :class:`~py_trees.behaviour.Behaviour`: the child that is currently running, or None """ if self.current_index == -1: return None return self.children[self.current_index] if self.children else None
[docs] def stop(self, new_status=Status.INVALID): """ Stopping a sequence requires taking care of the current index. Note that is important to implement this here intead of terminate, so users are free to subclass this easily with their own terminate and not have to remember that they need to call this function manually. Args: new_status (:class:`~py_trees.common.Status`): the composite is transitioning to this new status """ # retain information about the last running child if the new status is # SUCCESS or FAILURE if new_status == Status.INVALID: self.current_index = -1 Composite.stop(self, new_status)
############################################################################## # Parallel ##############################################################################
[docs]class Parallel(Composite): """ Parallels enable a kind of concurrency .. graphviz:: dot/parallel.dot Ticks every child every time the parallel is run (a poor man's form of paralellism). * Parallels will return :data:`~py_trees.common.Status.FAILURE` if any child returns :py:data:`~py_trees.common.Status.FAILURE` * Parallels with policy :data:`~py_trees.common.ParallelPolicy.SUCCESS_ON_ONE` return :py:data:`~py_trees.common.Status.SUCCESS` if **at least one** child returns :py:data:`~py_trees.common.Status.SUCCESS` and others are :py:data:`~py_trees.common.Status.RUNNING`. * Parallels with policy :data:`~py_trees.common.ParallelPolicy.SUCCESS_ON_ALL` only returns :py:data:`~py_trees.common.Status.SUCCESS` if **all** children return :py:data:`~py_trees.common.Status.SUCCESS` .. seealso:: The :ref:`py-trees-demo-context-switching-program` program demos a parallel used to assist in a context switching scenario. Args: name (:obj:`str`): the composite behaviour name policy (:class:`~py_trees.common.ParallelPolicy`): policy to use for deciding success or otherwise children ([:class:`~py_trees.behaviour.Behaviour`]): list of children to add *args: variable length argument list **kwargs: arbitrary keyword arguments """
[docs] def __init__(self, name="Parallel", policy=common.ParallelPolicy.SUCCESS_ON_ALL, children=None, *args, **kwargs): super(Parallel, self).__init__(name, children, *args, **kwargs) self.policy = policy
[docs] def tick(self): """ Tick over the children. Yields: :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children """ if self.status != Status.RUNNING: # subclass (user) handling self.initialise() self.logger.debug("%s.tick()" % self.__class__.__name__) # process them all first for child in self.children: for node in child.tick(): yield node # new_status = Status.SUCCESS if self.policy == common.ParallelPolicy.SUCCESS_ON_ALL else Status.RUNNING new_status = Status.RUNNING if any([c.status == Status.FAILURE for c in self.children]): new_status = Status.FAILURE else: if self.policy == common.ParallelPolicy.SUCCESS_ON_ALL: if all([c.status == Status.SUCCESS for c in self.children]): new_status = Status.SUCCESS elif self.policy == common.ParallelPolicy.SUCCESS_ON_ONE: if any([c.status == Status.SUCCESS for c in self.children]): new_status = Status.SUCCESS # special case composite - this parallel may have children that are still running # so if the parallel itself has reached a final status, then these running children # need to be made aware of it too if new_status != Status.RUNNING: for child in self.children: if child.status == Status.RUNNING: # interrupt it (exactly as if it was interrupted by a higher priority) child.stop(Status.INVALID) self.stop(new_status) self.status = new_status yield self
@property def current_child(self): """ Have to check if there's anything actually running first. Returns: :class:`~py_trees.behaviour.Behaviour`: the child that is currently running, or None """ if self.status == Status.INVALID: return None if self.status == Status.FAILURE: for child in self.children: if child.status == Status.FAILURE: return child # shouldn't get here elif self.status == Status.SUCCESS and self.policy == common.ParallelPolicy.SUCCESS_ON_ONE: for child in self.children: if child.status == Status.SUCCESS: return child else: return self.children[-1]