Understanding contingency-handlers

Overview

This tutorial demonstrates how contingency-handlers can be used to ‘react’ to failures which occur during execution. In the following examples custom SequenceNodes are used.

Create an ActionNode with failures

Create a file named sequence_with_contingencies.py with following content. Or use the provided file: sequence_with_contingencies.py

First of all, an ActionNode which ‘can’ fail is required. Therefore, the AddTwoNumbersAction is extended that it can complete with FAILURE. This extended node is called AddTwoNumbersActionWithFailures. It is implemented that a failure can be provoked in case that one or both input parameters are missing or in case the result of the sum of the two parameters is greater than ten. These are, of course, just some ‘artificial’ failures to beeing able to demonstrate the technics of contingency-handlers.

 1from carebt import ActionNode
 2from carebt import ContingencyHistoryEntry
 3from carebt import NodeStatus
 4from carebt import SequenceNode
 5from carebt.examples.simple_sequence import CreateRandomNumberAction
 6from carebt.examples.simple_sequence import PrintNumberAction
 7
 8
 9class AddTwoNumbersActionWithFailures(ActionNode):
10    """The `AddTwoNumbersActionWithFailures` example node.
11
12    The `AddTwoNumbersActionWithFailures` demonstrates a careBT `ActionNode`
13    with two input parameters and one output parameter. It takes the two inputs,
14    adds them and returns the result. Furthermore, this node can complete with
15    `FAILURE`. This happend in case that one or both input parameters are missing
16    or that the result of the sum is greater than ten.
17
18    Input Parameters
19    ----------------
20    ?x : int
21        The first value
22    ?y : int
23        The second value
24
25    Output Parameters
26    -----------------
27    ?z : int
28        The sum of ?x and ?y
29
30    Contingencies
31    -------------
32    FAILURE:
33        ONE_PARAM_MISSING
34            One input parameter is missing.
35
36        BOTH_PARAMS_MISSING
37            Both input parameters are missing.
38
39        RESULT_TOO_LARGE
40            The result is greater than 10.
41
42    """
43
44    def __init__(self, bt_runner):
45        super().__init__(bt_runner, '?x ?y => ?z')
46
47    def on_init(self) -> None:
48        if(self._x is None and self._y is None):
49            self.set_status(NodeStatus.FAILURE)
50            self.set_contingency_message('BOTH_PARAMS_MISSING')
51        elif(self._y is None):
52            self.set_status(NodeStatus.FAILURE)
53            self.set_contingency_message('ONE_PARAM_MISSING')
54
55    def on_tick(self) -> None:
56        self._z = self._x + self._y
57        if(self._z > 10):
58            print('AddTwoNumbersActionWithFailures: calculating: '
59                  + f'{self._x} + {self._y} = {self._z} -> RESULT_TOO_LARGE')
60            self.set_status(NodeStatus.FAILURE)
61            self.set_contingency_message('RESULT_TOO_LARGE')
62        else:
63            print('AddTwoNumbersActionWithFailures: calculating: '
64                  + f'{self._x} + {self._y} = {self._z}')
65            self.set_status(NodeStatus.SUCCESS)

The code explained

The AddTwoNumbersAction was already introduced in Writing a node with parameters and is now extended to provide some failures. This extended node is called AddTwoNumbersActionWithFailures.

In the on_init function two different contingency situations are implemented. The first one if both input parameters are missing. In this case the node completes with FAILURE and provides the contingency-message ‘BOTH_PARAMS_MISSING’. And the second one if one input parameter is missing. In this case the node completes with FAILURE and provides the contingency-message ‘ONE_PARAM_MISSING’.

    def on_init(self) -> None:
        if(self._x is None and self._y is None):
            self.set_status(NodeStatus.FAILURE)
            self.set_contingency_message('BOTH_PARAMS_MISSING')
        elif(self._y is None):
            self.set_status(NodeStatus.FAILURE)
            self.set_contingency_message('ONE_PARAM_MISSING')

In the on_tick function the result of the calculation is checked whether it is greater than ten. This is, of course, just for demo purposes. In case _z is greater than ten the node fails and provides the contingency-message ‘RESULT_TOO_LARGE’.

    def on_tick(self) -> None:
        self._z = self._x + self._y
        if(self._z > 10):
            print('AddTwoNumbersActionWithFailures: calculating: '
                  + f'{self._x} + {self._y} = {self._z} -> RESULT_TOO_LARGE')
            self.set_status(NodeStatus.FAILURE)
            self.set_contingency_message('RESULT_TOO_LARGE')
        else:
            print('AddTwoNumbersActionWithFailures: calculating: '
                  + f'{self._x} + {self._y} = {self._z}')
            self.set_status(NodeStatus.SUCCESS)

Run the example

Start the Python interpreter and run the AddTwoNumbersActionWithFailures node. The log-level is set to INFO to see some more details on the execution:

>>> import carebt
>>> from carebt import LogLevel
>>> from carebt.examples.sequence_with_contingencies import AddTwoNumbersActionWithFailures
>>> bt_runner = carebt.BehaviorTreeRunner()
>>> bt_runner.get_logger().set_log_level(LogLevel.INFO)
>>> bt_runner.run(AddTwoNumbersActionWithFailures, '3 5 => ?x')
2022-03-05 17:09:01 INFO creating AddTwoNumbersActionWithFailures
AddTwoNumbersActionWithFailures: calculating: 3 + 5 = 8
2022-03-05 17:09:01 INFO ---------------------------------------------------
2022-03-05 17:09:01 INFO bt execution finished
2022-03-05 17:09:01 INFO status:  NodeStatus.SUCCESS
2022-03-05 17:09:01 INFO contingency-message:
2022-03-05 17:09:01 INFO ---------------------------------------------------
>>> bt_runner.run(AddTwoNumbersActionWithFailures, '3 => ?x')
2022-03-05 17:11:53 INFO creating AddTwoNumbersActionWithFailures
2022-03-05 17:11:53 WARN AddTwoNumbersActionWithFailures takes 2 argument(s), but 1 was/were provided
2022-03-05 17:11:53 WARN ---------------------------------------------------
2022-03-05 17:11:53 WARN bt execution finished
2022-03-05 17:11:53 WARN status:  NodeStatus.FAILURE
2022-03-05 17:11:53 WARN contingency-message: ONE_PARAM_MISSING
2022-03-05 17:11:53 WARN ---------------------------------------------------
>>> bt_runner.run(AddTwoNumbersActionWithFailures, '=> ?x')
2022-03-05 17:12:35 INFO creating AddTwoNumbersActionWithFailures
2022-03-05 17:12:35 WARN AddTwoNumbersActionWithFailures takes 2 argument(s), but 0 was/were provided
2022-03-05 17:12:35 WARN ---------------------------------------------------
2022-03-05 17:12:35 WARN bt execution finished
2022-03-05 17:12:35 WARN status:  NodeStatus.FAILURE
2022-03-05 17:12:35 WARN contingency-message: BOTH_PARAMS_MISSING
2022-03-05 17:12:35 WARN ---------------------------------------------------
>>> bt_runner.run(AddTwoNumbersActionWithFailures, '7 5 => ?x')
2022-03-05 17:12:59 INFO creating AddTwoNumbersActionWithFailures
AddTwoNumbersActionWithFailures: calculating: 7 + 5 = 12 -> RESULT_TOO_LARGE
2022-03-05 17:12:59 WARN ---------------------------------------------------
2022-03-05 17:12:59 WARN bt execution finished
2022-03-05 17:12:59 WARN status:  NodeStatus.FAILURE
2022-03-05 17:12:59 WARN contingency-message: RESULT_TOO_LARGE
2022-03-05 17:12:59 WARN ---------------------------------------------------

Create a sequence without contingency-handling

In this example the SimpleSequence node is implemented which has the two children AddTwoNumbersActionWithFailures and PrintNumberAction. The AddTwoNumbersActionWithFailures is the one from above which can complete with failures.

Add the following content to sequence_with_contingencies.py. Or use the provided file: sequence_with_contingencies.py

 1class SimpleSequence(SequenceNode):
 2    """The `SimpleSequence` example node.
 3
 4    The `SimpleSequence` shows what happens if a child node in the sequence fails.
 5    The used `AddTwoNumbersActionWithFailures` can fail in case that one or both
 6    input parameters are missing or that the result of the sum is greater than ten.
 7    These contingencies are not handled, and thus forwarded to the `SequenceNode`
 8    and finally to the `bt_runner`.
 9
10    Input Parameters
11    ----------------
12    ?a : int
13        The first value
14    ?b : int
15        The second value
16
17    Output Parameters
18    -----------------
19    ?c : int
20        The result
21
22    """
23
24    def __init__(self, bt_runner):
25        super().__init__(bt_runner, '?a ?b')
26
27    def on_init(self) -> None:
28        self.append_child(AddTwoNumbersActionWithFailures, '?a ?b => ?c')
29        self.append_child(PrintNumberAction, '?c')

The code explained

The SimpleSequence has two input parameters which are directly used as inputs for the AddTwoNumbersActionWithFailures.

Run the example

Start the Python interpreter and run the SimpleSequence node:

>>> import carebt
>>> from carebt import LogLevel
>>> from carebt.examples.sequence_with_contingencies import SimpleSequence
>>> bt_runner = carebt.BehaviorTreeRunner()
>>> bt_runner.get_logger().set_log_level(LogLevel.INFO)
>>> bt_runner.run(SimpleSequence, '2 5')
2022-03-05 17:14:21 INFO creating SimpleSequence
2022-03-05 17:14:21 INFO creating AddTwoNumbersActionWithFailures
AddTwoNumbersActionWithFailures: calculating: 2 + 5 = 7
2022-03-05 17:14:21 INFO creating PrintNumberAction
PrintNumberAction: number = 7
2022-03-05 17:14:21 INFO finished SimpleSequence
2022-03-05 17:14:21 INFO ---------------------------------------------------
2022-03-05 17:14:21 INFO bt execution finished
2022-03-05 17:14:21 INFO status:  NodeStatus.SUCCESS
2022-03-05 17:14:21 INFO contingency-message:
2022-03-05 17:14:21 INFO ---------------------------------------------------
>>> bt_runner.run(SimpleSequence, '2')
2022-03-05 17:14:56 INFO creating SimpleSequence
2022-03-05 17:14:56 WARN SimpleSequence takes 2 argument(s), but 1 was/were provided
2022-03-05 17:14:56 INFO creating AddTwoNumbersActionWithFailures
2022-03-05 17:14:56 INFO finished SimpleSequence
2022-03-05 17:14:56 WARN ---------------------------------------------------
2022-03-05 17:14:56 WARN bt execution finished
2022-03-05 17:14:56 WARN status:  NodeStatus.FAILURE
2022-03-05 17:14:56 WARN contingency-message: ONE_PARAM_MISSING
2022-03-05 17:14:56 WARN ---------------------------------------------------
>>> bt_runner.run(SimpleSequence, '')
2022-03-05 17:15:21 INFO creating SimpleSequence
2022-03-05 17:15:21 WARN SimpleSequence takes 2 argument(s), but 0 was/were provided
2022-03-05 17:15:21 INFO creating AddTwoNumbersActionWithFailures
2022-03-05 17:15:21 INFO finished SimpleSequence
2022-03-05 17:15:21 WARN ---------------------------------------------------
2022-03-05 17:15:21 WARN bt execution finished
2022-03-05 17:15:21 WARN status:  NodeStatus.FAILURE
2022-03-05 17:15:21 WARN contingency-message: BOTH_PARAMS_MISSING
2022-03-05 17:15:21 WARN ---------------------------------------------------
>>> bt_runner.run(SimpleSequence, '6 8')
2022-03-05 17:15:43 INFO creating SimpleSequence
2022-03-05 17:15:43 INFO creating AddTwoNumbersActionWithFailures
AddTwoNumbersActionWithFailures: calculating: 6 + 8 = 14 -> RESULT_TOO_LARGE
2022-03-05 17:15:43 INFO finished SimpleSequence
2022-03-05 17:15:43 WARN ---------------------------------------------------
2022-03-05 17:15:43 WARN bt execution finished
2022-03-05 17:15:43 WARN status:  NodeStatus.FAILURE
2022-03-05 17:15:43 WARN contingency-message: RESULT_TOO_LARGE
2022-03-05 17:15:43 WARN ---------------------------------------------------

The first execution shows the standard case where two input parameters are provided which have a sum smaller than ten. As in this case the AddTwoNumbersActionWithFailures node completes with SUCCESS the PrintNumberAction is also executed. The subsequent three executions have in common, that the AddTwoNumbersActionWithFailures completes with FAILURE, and thus the sequence directly completes also with FAILURE - without executing the PrintNumberAction node. The only difference is that the contingency-message differs in the three cases.

Create a sequence with contingency-handling

In this example the SimpleSequence node is extended by two contingency-handlers. The new sequence is called ContingencySequence.

Add the following content to sequence_with_contingencies.py. Or use the provided file: sequence_with_contingencies.py

 1class ContingencySequence(SequenceNode):
 2    """The `ContingencySequence` example node.
 3
 4    The `ContingencySequence` extends the `Sequence` by showing how contingency
 5    handlers can be registered. In case of the contingency `RESULT_TOO_LARGE` all
 6    child nodes are removed and the two nodes `CreateRandomNumberAction` and
 7    `PrintNumberAction` are added. For the two contingencies `ONE_PARAM_MISSING`
 8    and `BOTH_PARAMS_MISSING` one contingency-handler using a wildcard `*_MISSING`
 9    is registered. In this case the output parameter *?c* is set to 0 and the
10    current child is set to fixed. Setting a chiuld to fixed deletes the current
11    contingency message and sets the status to `FIXED`. As `FIXED` is handled
12    in the same way as `SUCCESS`, the sequence continues as everythig was normal.
13
14    Input Parameters
15    ----------------
16    ?a : int
17        The first value
18    ?b : int
19        The second value
20
21    Output Parameters
22    -----------------
23    ?c : int
24        The result
25
26    """
27
28    def __init__(self, bt_runner):
29        super().__init__(bt_runner, '?a ?b')
30
31    def on_init(self) -> None:
32        self.append_child(AddTwoNumbersActionWithFailures, '?a ?b => ?c')
33        self.append_child(PrintNumberAction, '?c')
34
35        self.register_contingency_handler(AddTwoNumbersActionWithFailures,
36                                          [NodeStatus.FAILURE],
37                                          'RESULT_TOO_LARGE',
38                                          self.fix_large_result)
39
40        self.register_contingency_handler(AddTwoNumbersActionWithFailures,
41                                          [NodeStatus.FAILURE],
42                                          # r'ONE_PARAM_MISSING|BOTH_PARAMS_MISSING',
43                                          # r'.*_MISSING',
44                                          r'.*_PARAM(S)?_MISSING',
45                                          self.fix_missing_input)
46
47    def fix_large_result(self) -> None:
48        print('fix_large_result')
49        self.remove_all_children()
50        self.append_child(CreateRandomNumberAction, '=> ?c')
51        self.append_child(PrintNumberAction, '?c')
52
53    def fix_missing_input(self) -> None:
54        print('fix_missing_input: set ?c = 0')
55        self._c = 0
56        self.fix_current_child()
57
58    def on_delete(self) -> None:
59        if len(self.get_contingency_history()) > 0:
60            entry: ContingencyHistoryEntry = self.get_contingency_history()[-1]
61            if(entry.contingency_message == 'ONE_PARAM_MISSING'
62               or entry.contingency_message == 'BOTH_PARAMS_MISSING'):
63                self.set_contingency_message('MISSING_PARAM_FIXED')
64            elif entry.contingency_message == 'RESULT_TOO_LARGE':
65                self.set_contingency_message('TOO_LARGE_RESULT_FIXED')

The code explained

In the on_init function the two child nodes are added. Afterwards two contingency-handlers are registered. The first one is attached to the AddTwoNumbersActionWithFailures child and triggers in case the child node completes with FAILURE and with the contingency-message RESULT_TOO_LARGE. In this case the fix_large_result function is called.

        self.register_contingency_handler(AddTwoNumbersActionWithFailures,
                                          [NodeStatus.FAILURE],
                                          'RESULT_TOO_LARGE',
                                          self.fix_large_result)

The fix_large_result function removes all children from the sequence and adds the two new child nodes CreateRandomNumberAction and PrintNumberAction.

    def fix_large_result(self) -> None:
        print('fix_large_result')
        self.remove_all_children()
        self.append_child(CreateRandomNumberAction, '=> ?c')
        self.append_child(PrintNumberAction, '?c')

The second contingency-handler is also attached to the AddTwoNumbersActionWithFailures child and triggers in case the child node completes with FAILURE and the contingency-message matches the regular expression r'.*_PARAM(S)?_MISSING'. Thus, the fix_missing_input function is triggered for both contingency-messages the AddTwoNumbersActionWithFailures node can provide. Alternatively the regular expression could be formulated, for example, as follows: r'ONE_PARAM_MISSING|BOTH_PARAMS_MISSING' or r'.*_MISSING'.

        self.register_contingency_handler(AddTwoNumbersActionWithFailures,
                                          [NodeStatus.FAILURE],
                                          # r'ONE_PARAM_MISSING|BOTH_PARAMS_MISSING',
                                          # r'.*_MISSING',
                                          r'.*_PARAM(S)?_MISSING',
                                          self.fix_missing_input)

The fix_missing_input function sets the output parameter _c resp. ?c to zero and the currently executing child - which is the AddTwoNumbersActionWithFailures - to FIXED. As FIXED is handled in the same way as SUCCESS the execution continues with the PrintNumberAction with input parameter ?c set to zero.

    def fix_missing_input(self) -> None:
        print('fix_missing_input: set ?c = 0')
        self._c = 0
        self.fix_current_child()

The on_delete callback is called right after all child nodes have finished their execution. In this example, it is checked whether a contingency-handler was executed. If that was the case the contingency-message is set accordingly to indicate that to the parent node. Thus, the parent node will be able to analyse the contingency-message, as well as the contingency-history to figure out how the task was completed with SUCCESS.

    def on_delete(self) -> None:
        if len(self.get_contingency_history()) > 0:
            entry: ContingencyHistoryEntry = self.get_contingency_history()[-1]
            if(entry.contingency_message == 'ONE_PARAM_MISSING'
               or entry.contingency_message == 'BOTH_PARAMS_MISSING'):
                self.set_contingency_message('MISSING_PARAM_FIXED')
            elif entry.contingency_message == 'RESULT_TOO_LARGE':
                self.set_contingency_message('TOO_LARGE_RESULT_FIXED')

Run the example

Start the Python interpreter and run the ContingencySequence node:

>>> import carebt
>>> from carebt import LogLevel
>>> from carebt.examples.sequence_with_contingencies import ContingencySequence
>>> bt_runner = carebt.BehaviorTreeRunner()
>>> bt_runner.get_logger().set_log_level(LogLevel.INFO)
>>> bt_runner.run(ContingencySequence, '6 4')
2022-03-05 17:16:26 INFO creating ContingencySequence
2022-03-05 17:16:26 INFO creating AddTwoNumbersActionWithFailures
AddTwoNumbersActionWithFailures: calculating: 6 + 4 = 10
2022-03-05 17:16:26 INFO creating PrintNumberAction
PrintNumberAction: number = 10
2022-03-05 17:16:26 INFO finished ContingencySequence
2022-03-05 17:16:26 INFO ---------------------------------------------------
2022-03-05 17:16:26 INFO bt execution finished
2022-03-05 17:16:26 INFO status:  NodeStatus.SUCCESS
2022-03-05 17:16:26 INFO contingency-message:
2022-03-05 17:16:26 INFO ---------------------------------------------------
>>> bt_runner.run(ContingencySequence, '6')
2022-03-05 17:42:54 INFO creating ContingencySequence
2022-03-05 17:42:54 WARN ContingencySequence takes 2 argument(s), but 1 was/were provided
2022-03-05 17:42:54 INFO creating AddTwoNumbersActionWithFailures
fix_missing_input: set ?c = 0
2022-03-05 17:42:54 INFO creating PrintNumberAction
PrintNumberAction: number = 0
2022-03-05 17:42:54 INFO finished ContingencySequence
2022-03-05 17:42:54 INFO ---------------------------------------------------
2022-03-05 17:42:54 INFO bt execution finished
2022-03-05 17:42:54 INFO status:  NodeStatus.SUCCESS
2022-03-05 17:42:54 INFO contingency-message: MISSING_PARAM_FIXED
2022-03-05 17:42:54 INFO contingency-history: [0] AddTwoNumbersActionWithFailures
2022-03-05 17:42:54 INFO                          NodeStatus.FAILURE
2022-03-05 17:42:54 INFO                          ONE_PARAM_MISSING
2022-03-05 17:42:54 INFO                          fix_missing_input
2022-03-05 17:42:54 INFO ---------------------------------------------------
>>> bt_runner.run(ContingencySequence, '')
2022-03-05 17:43:23 INFO creating ContingencySequence
2022-03-05 17:43:23 WARN ContingencySequence takes 2 argument(s), but 0 was/were provided
2022-03-05 17:43:23 INFO creating AddTwoNumbersActionWithFailures
fix_missing_input: set ?c = 0
2022-03-05 17:43:23 INFO creating PrintNumberAction
PrintNumberAction: number = 0
2022-03-05 17:43:23 INFO finished ContingencySequence
2022-03-05 17:43:23 INFO ---------------------------------------------------
2022-03-05 17:43:23 INFO bt execution finished
2022-03-05 17:43:23 INFO status:  NodeStatus.SUCCESS
2022-03-05 17:43:23 INFO contingency-message: MISSING_PARAM_FIXED
2022-03-05 17:43:23 INFO contingency-history: [0] AddTwoNumbersActionWithFailures
2022-03-05 17:43:23 INFO                          NodeStatus.FAILURE
2022-03-05 17:43:23 INFO                          BOTH_PARAMS_MISSING
2022-03-05 17:43:23 INFO                          fix_missing_input
2022-03-05 17:43:23 INFO ---------------------------------------------------
>>> bt_runner.run(ContingencySequence, '6 9')
2022-03-05 17:43:48 INFO creating ContingencySequence
2022-03-05 17:43:48 INFO creating AddTwoNumbersActionWithFailures
AddTwoNumbersActionWithFailures: calculating: 6 + 9 = 15 -> RESULT_TOO_LARGE
fix_large_result
2022-03-05 17:43:48 INFO creating CreateRandomNumberAction
CreateRandomNumberAction: number = 5
2022-03-05 17:43:48 INFO creating PrintNumberAction
PrintNumberAction: number = 5
2022-03-05 17:43:48 INFO finished ContingencySequence
2022-03-05 17:43:48 INFO ---------------------------------------------------
2022-03-05 17:43:48 INFO bt execution finished
2022-03-05 17:43:48 INFO status:  NodeStatus.SUCCESS
2022-03-05 17:43:48 INFO contingency-message: TOO_LARGE_RESULT_FIXED
2022-03-05 17:43:48 INFO contingency-history: [0] AddTwoNumbersActionWithFailures
2022-03-05 17:43:48 INFO                          NodeStatus.FAILURE
2022-03-05 17:43:48 INFO                          RESULT_TOO_LARGE
2022-03-05 17:43:48 INFO                          fix_large_result
2022-03-05 17:43:48 INFO ---------------------------------------------------

Again, the first execution demonstrates the ‘good’ case where two input parameters with a sum smaller than ten are provided. Thus, the AddTwoNumbersActionWithFailures completes with SUCCESS and the PrintNumberAction is executed. The next two execution show the cases where one or both input parameters are missing. The fix_missing_input contingency-handler-function is executed which sets ?c to zero and fixes the AddTwoNumbersActionWithFailures. Thus, the sequence execution conntinues with PrintNumberAction. The next execution demonstrates the case when the sum of the two input parameters is greater than ten. In this case the fix_large_result contingency-handler-function is executed which removes all child nodes and add two new ones. The last three runs show, how the execution of the contingency handlers is documented by the contingency-history.