Task Trees Manager

The task trees manager supports efficient building of task-based and conventional behaviour trees, as well as provides a programming interface for managing execution of the trees. It enables robotic manipulator applications building behaviour trees by adding custom branches of behaviours to a tree skeleton. The effort is significantly less than building the tree and its behaviours from scratch.

The task trees manager has implemented the concept of task, which is defined as a branch of behaviours designed to achieve an outcome. It supports the definition of task branches in the tree, submission and cancellation of tasks, and tracking their execution.

The current version of the task trees framework provides two concrete task tree manager classes, TaskTreesManager and GuardedTaskTreesManager.

Tree Building with the TaskTreesManager Class

The TaskTreesManager class offers the following three ways of building customized behaviour trees.

  • One initialization branch: the branch of behaviours is to be executed once when the tree is first executed.

  • One or more task branches: a task branch is to be executed when its associated task is submitted to the tree.

  • One or more priority branches: a priority branch is always executed first and any task branch is executed only when all the priority branches return FAILURE status.

The following table summarizes the functions for adding the various branches.

Functions

Parameters

Remarks

_set_initialize_branch(branch)

a branch of behaviour tree

_add_priority_branch(branch)

a branch of behaviour tree of type Composite

the earlier the addition the higher is priority

_add_task_branch(branch, task_class)

a branch of behaviour tree of type Composite and the task class

the task class is a subclass of BasicTask

THe following steps are recommended for building a behaviour tree or task tree for a new application:

  1. Create a subclass of TaskTreesManager.

  2. In the constructor, call the above functions to add branches to the tree. Use one or more class functions to create the branches.

  3. Call _install_bt_and_spin to execute the tree.

Basics of Building and Executing Behaviour Trees

The following is an example of building a behaviour tree with one initialization branch and one priority branch.

class SimpleMoveTaskManager(TaskTreesManager):
    def __init__(self, arm_commander:GeneralCommander, spin_period_ms:int=10):
        super(SimpleMoveTaskManager, self).__init__(arm_commander)

        self._set_initialize_branch(self.create_init_branch())
        self._add_priority_branch(self.create_move_branch())

        self._install_bt_and_spin(self.bt, spin_period_ms)
    
    def create_move_branch(self) -> Composite:
        move_branch = Sequence('move_branch', memory=True,
                children=[
                    DoMoveDisplaceXYZ('move_dy', True, arm_commander=self.arm_commander, dxyz=[0.0, 0.05, 0]), 
                    DoMoveDisplaceXYZ('move_dz', True, arm_commander=self.arm_commander, dxyz=[0, 0, 0.05]), 
                    DoMoveDisplaceXYZ('move_ndy', True, arm_commander=self.arm_commander, dxyz=[0, -0.05, 0]), 
                    DoMoveDisplaceXYZ('move_dz', True, arm_commander=self.arm_commander, dxyz=[0, 0, -0.05]),])
        return move_branch
    
    def create_init_branch(self) -> Composite:
        # - the branch that executes the task MoveNamedPoseTask
        init_branch = Sequence('init_branch', memory=True,
                children=[
                    DoMoveXYZRPY('reset_pose', True, arm_commander=self.arm_commander, target_xyz=[0.3, -0.2, 0.3], target_rpy=[3.139, 0.0, -0.785]), ],)
        return init_branch 

Notes:

  • The initialization branch moves the end-effector to a position and orientation as defined in the function create_init_branch.

  • The only priority branch executes a rectangular move pattern as defined in the function create_move_branch.

  • The priority branch would be repeated indefintely.

Demo Animation

An arm_commander object is required for the instant-use move behaviour. The following example main program first initializes an arm_commander object. Then create a new SimpleMoveTaskManager using the arm commander and finally call the inherited spin method

if __name__=='__main__':
    try:
        arm_commander = GeneralCommander('panda_arm')
        arm_commander.abort_move(wait=True)
        arm_commander.reset_world()
        the_task_manager = SimpleMoveTaskManager(arm_commander)
        the_task_manager.spin()
    except Exception as e:
        logger.exception(e)

Example: A Timeout Priority Branch

A conditional or guarded priority branch is common. The following shows the use of conditional function is_timeout that returns True after 15 seconds since the start. The condition function is applied to the DoMoveJointPose behaviour so that it is activated after 15 seconds. Refer to timeout_move.py under simple_moves (source code).

    def __init__(self, arm_commander:GeneralCommander, spin_period_ms:int=10):
        ...
        self._set_initialize_branch(self.create_init_branch())
        self._add_priority_branch(self.create_timeout_branch())   

        self.start_time = time.time()   

        self._install_bt_and_spin(self.bt, spin_period_ms)
    ...

    def is_timeout(self, duration=30):
        return time.time() - self.start_time > duration

    def create_timeout_branch(self) -> Composite:
        timeout_branch = Sequence('move_branch', memory=True,
                children=[
                    DoMoveJointPose('reset_pose', {'_fn': self.is_timeout, 'duration': 15},
                                    arm_commander=self.arm_commander, 
                                    target_joint_pose=[0.00, -1.243, 0.00, -2.949, 0.00, 1.704, 0.785],), 
                    ],)
        return timeout_branch
    
    def create_init_branch(self) -> Composite:
        init_branch = Sequence('init_branch', memory=True,
                children=[
                    DoMoveXYZ('move_xyz', True, arm_commander=self.arm_commander, target_xyz=
                        lambda: [random.uniform(0.1, 0.5), random.uniform(-0.3, 0.3), random.uniform(0.2, 0.6)]), 
                    ],)
        return init_branch 

Demo Animation

In the above animation for this example, the initialization branch extends the arm. At 15 seconds since the start of the program, the DoMoveJointPose behaviour in the priority branch is activated by the is_timeout condition.

Tasks Definition and Adding Task Branches

The task notion supported by the task trees manager enables more control over the execution of behaviours.

A task tree has one or more task branches plus one or more priority branches and the initialization branches. The task branches are guarded and will not get executed until the task tree manager receives a submitted task. During the task execution, the task manager entertains queries on the execution state and the option to cancel the task. After the task execution, the task manager can be consulted on whether the execution was successful or details of the error occurred.

The following shows how to define a task MoveRectTask, define the associated task branch and add the branch to the task tree. Refer to the program task_move_1.py under task_moves (source code).

The branch of the task executes a rectangular motion once.

A task definition is a subclass of BasicTask.

class MoveRectTask(BasicTask):
    def __init__(self):
        super(MoveRectTask, self).__init__()

class SimpleTaskMoveManager(TaskTreesManager):
    def __init__(self, arm_commander:GeneralCommander, spin_period_ms:int=10):
        super(SimpleTaskMoveManager, self).__init__(arm_commander)
        ...
        self._add_task_branch(self.create_move_rect_branch(), MoveRectTask)

        self._install_bt_and_spin(self.bt, spin_period_ms)
        
    def create_move_rect_branch(self) -> Composite:

        move_branch = Sequence( 'move_branch', memory=True,
                children=[
                    DoMoveDisplaceXYZ('move_dy', True, arm_commander=self.arm_commander, dxyz=[0.0, 0.3, 0]), 
                    DoMoveDisplaceXYZ('move_dz', True, arm_commander=self.arm_commander, dxyz=[0, 0, 0.3]), 
                    DoMoveDisplaceXYZ('move_ndy', True, arm_commander=self.arm_commander, dxyz=[0, -0.3, 0]), 
                    DoMoveDisplaceXYZ('move_random_ndz', True, arm_commander=self.arm_commander, 
                                      dxyz=lambda: [0.0, 0.0, random.uniform(-0.25, -0.35)]),               
                    ],)
        return move_branch

In the function call to _add_task_branch the task branch created in create_move_rect_branch is passed together with class reference MoveRectTask.

Example: Task Execution

The task tree manager then awaits order to execute the MoveRectTask. To execute the task, call submit_task of the task trees manager.

The following class creates the task manager defined above, and then executes the MoveRectTask ten times in a for loop.

class TaskDemoApplication():
    """ The application program for the simple Task Demo
    """
    def __init__(self):
        ...
        self.the_task_manager = SimpleTaskMoveManager(self.arm_commander)
        self._run_demo()
    ...
        
    def _run_demo(self):
        task_manager = self.the_task_manager
        the_task = None
        logger.info(f'=== Task Demo Started') 
        for i in range(10):
            logger.info(f'=== Submit a MoveRectTask #{i + 1}')
            task_manager.submit_task(the_task:=MoveRectTask())
            the_task.wait_for_completion()    

Notes:

  • The call to submit_task is asynchronous. The MoveRectTask task object is the handle for tracking the task execution.

  • The function call to wait_for_completion is blocking until the task has entered into one of the completion states. The list of completion states can be found in states.py

The following loop could replace the call to wait_for_completion.

from task_trees.states import COMPLETION_STATES
...
        while True:
            if the_task.get_state() in COMPLETION_STATES:
                break
            time.sleep(0.1)    

Example: Task Cancellation

Call the cancel_task function on the task object to cancel the task.

self.the_task.cancel(wait=True)

If the parameter wait is True, the function call will not return until the cancellation is completed. Usually, a loop to monitor the task state is needed. Note that although the eventual state is usually TaskStates.ABORTED, in a situation which the task is just completed (successful or otherwise) when the cancel function is called, the task may end up in other states.

the_task.cancel(wait=True)
while True:
    if the_task.get_state() in COMPLETION_STATES:
        break
    time.sleep(0.1)   

Refer to the example task_move_4.py under task_moves (source code). The example cancels the executing task when the GUI receives the command to execute another task.

List of task states

States

Is Completion

Remarks

TaskStates.SUCCEEDED

Yes

TaskStates.ABORTED

Yes

the task has been cancelled

TaskStates.GUARD_ABORTED

Yes

the task has been aborted by the task tree guard

TaskStates.FAILED

Yes

the task experienced a failure

TaskStates.INVALID

Yes

the task submission was invalid

TaskStates.SUBMITTED

No

the task has just been submitted but not executed yet

TaskStates.WORKING

No

the associated task branch is being executed

TaskStates.STANDBY

No

the task has not yet been submitted

Task Parameters

The task trees manager supports parameters in task definition.

The following task, which moves to a random y position, has two parameters min_y and max_y that confine the range.

class MoveRandomTask(BasicTask):
    def __init__(self, min_y:float, max_y:float):
        super(MoveRandomTask, self).__init__()
        self.min_y = min_y
        self.max_y = max_y

The task branch has a behaviour that obtains the move target from a function (i.e. dynamic binding).

    def create_move_random_branch(self) -> Composite:
        move_branch = Sequence('move_branch', memory=True,
                children=[
                        DoMoveXYZ('move_random_y', True, arm_commander=self.arm_commander, target_xyz=self.generate_random_xyz),                                      
                    ],)
        return move_branch 

The definition of the function generate_random_xyz is shown below, which accesses the task object and the two parameters through the blackboard of py-trees. The framework provides access to the task object under the key ‘task’ on the py-trees blackboard.

    def generate_random_xyz(self) -> list:
        if not self.the_blackboard.exists('task'):
            raise TypeError(f'unable to generate random pose due to no task has been submitted')
        min_y = self.the_blackboard.task.min_y
        max_y = self.the_blackboard.task.max_y           
        xyz = [None, random.uniform(min_y, max_y), None]
        return xyz

List of TaskTreesManager Functions

Functions

Parameters

Remarks

shutdown

Shutdown the task manager properly

_define_named_poses

scene configuration object

extract the named poses and add to the arm commander

_define_objects

scene configuration object

extract the collision object definitions and add to the arm commander

_set_initialize_branch

a tree branch

setup the initialization branch

_add_priority_branch

a tree branch

add the branch as a priority branch

_add_task_branch

a tree branch and the task class definition

add a task branch

display_tree

the branch to display (optional)

output the graphical representation of the tree or a subtree

List of BasicTask Functions

Functions

Parameters

Remarks

get_state

return the state of the task

cancel

wait

cancel the task and return immediately if wait is False

wait_for_completion

block until the task is completed

wait_for_working

block until the task is in the WORKING state

get_time_since_submit

return the time lapsed since task submission

get_commander_feedback

return the final message from the arm commander on this task

TreesManager Figure

Tree Building with the GuardedTaskTreesManager Class

Another manager included in the task trees framework is GuardedTaskTreesManager. This is a subclass of TaskTreesManager and the additional features are two guard conditions that can inactivate the whole behaviour tree and inactivate the task branches respectively.

The class provides two functions for setting up the guard conditions.

  • set_global_guard_condition_fn(global_guard_condition_fn): the whole behaviour tree is inactivated if the function global_guard_condition_fn returns False.

  • set_task_guard_condition_fn(task_guard_condition_fn): the task branches are inactivated if the function task_guard_condition_fn returns False.

The current task will be aborted and its state changed to TaskStates.GUARD_ABORTED.

By default, the GuardedTaskTreesManager requires a call to reset_guard after inactivation. The constructor offers a parameter to change the guard_reset to False so that the behaviour tree is reactivated once the guard condition returns True again.

TreesManager Figure

Examples

A simple example is given in the program estop_move.py under task_moves (source code).

A more advanced example can be found in the demo program PickNDrop.