Use apply and check method to create action graph #206
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Current Approach for Computing the Action Graph
The BTBuilder class in plansys2_executor is responsible for transforming a PDDL plan into a behavior tree. As a first step, the get_graph function transforms the plan (also referred to as the action sequence) into an action graph that encodes the causal relationships between the actions.
To construct the action graph, the roots of the graph are first computed. The roots represent the initial set of actions that can be run in parallel. Once the root set is is obtained, it is removed from the action sequence. A loop is than used to process the remainder of the action sequence. At each iteration, an action is taken from the action sequence and added to the graph. The loop progresses until there are no more actions in the action sequence.
Adding a new action node to the graph involves finding causal links from existing graph nodes to the new node. There are two types of causal links. A Type 1 causal link occurs when a requirement of the new node is satisfied by an effect of an existing node. In this case, the existing node is said to be a satisfying node of the new node. A Type 2 causal link occurs when a contradiction would prevent the new node from running in parallel with an existing node. Suppose n_new represents the action node to be added and n_existing represents an existing node in the graph. n_new cannot run in parallel with n_existing if the effects of n_new contradict the requirements of n_existing.
In the current approach, the get_node_satisfy function is used to find both types of causal links. A depth first search (DFS) is performed for each requirement of the new node to establish links to existing nodes. At each existing node, the function looks for a match between the requirement of the new node and an effect or requirement of the existing node. Note that in this approach matching the new node requirement to an existing node effect defines a Type 1 causal link, while matching the new node requirement to an existing node requirement defines a Type 2 causal link. Matching the requirement of the new node to an existing node effect or requirement involves deconstructing the PDDL trees and looking for matching predicates or functions.
Problems with the Current Approach
There are two problems with the current approach. The first problem is that the system searches for Type 1 and Type 2 causal links in the same DFS and returns at most one link for each requirement of the new node. While it is correct to assume that each requirement of the new node will have at most one Type 1 causal link, it is not correct to assume that the new node will have a number of Type 2 causal links less than or equal to the number of requirements. For example, suppose the new node has one requirement but two Type 2 causal links. In the current approach, one of these causal links will be ignored.
The second problem is that deconstructing the PDDL trees to look for matching predicates or functions is somewhat fragile, because it assumes a certain structure on the PDDL expressions that is not enforced by the language. In a function expression it is assumed that the first child of the PDDL tree, i.e, the left hand side of the expression, is a function and that this function represents the match query. However, in reality the PDDL function governing the causal relationship could be on the right hand side of the expression. Rather than trying to deconstruct the PDDL trees to look for matching predicates or functions, a better approach is to directly test for contradictions.
Proposed Modifications
The new approach addresses these problems by (1) searching for Type 2 causal links in a separate DFS from the Type 1 DFS and (2) using an “apply and check” approach to test for causal links rather than relying on the deconstructing the PDDL trees.
In the new approach, testing for Type 1 causal links is again performed using a DFS for each requirement of the new node. However, instead of deconstructing the PDDL trees to look for matching predicates or functions, we use an “apply and check” method. Starting with the state before the candidate satisfying node is applied, we check if the requirement of the new node is already satisfied. We then apply the effects of the candidate satisfying node and check if the requirement is satisfied after applying the effects. If the requirement of the new node is not satisfied before applying effects and is satisfied after applying the effects, the candidate is a satisfying node.
Testing for Type 2 causal links is performed in a second DFS after the Type 1 DFS. This time, however, we only traverse the graph once but return all potential Type 2 causal links. We again use an “apply and check” method to test for a link. Starting with the state before the candidate node is applied, we check if the new node is executable. That is, we check if all of the requirements of the new node are satisfied by the state just prior to applying the candidate node effects. If so, the new node could potentially run in parallel with the existing candidate node. To determine whether or not this is true, we apply the effects of the new node and check if the candidate existing node is still executable. That is, we check if all of the requirements of the existing node are satisfied by the state after applying the effects of the new node. If the existing node is no longer executable, we have a contradiction and thus the new node cannot be run in parallel with the existing node. This implies a causal link between the existing node and the new node, i.e., the new node must start after the existing node finishes.
Note that in testing for both types of causal links, we start from the state just before the effects of the candidate node are applied. To simplify the implementation, a snapshot of the state is stored with each node that represents the state of the system leading up to the node but not including the effects of the node. This is computed by traversing backward through the graph to find the sequence of nodes leading up to the node in question. Starting from the root, we then apply the effects of the predecessor nodes one at a time until we reach the node in question. Finally, we store the state with the node for future processing.
A Note About Pruning
After the Type 1 DFS and the Type 2 DFS we still perform the same backward pruning step as in the original approach. Before connecting the new node to it’s potential Type 1 or Type 2 parent, we traverse all the parents of the candidate node to look for previously established links to the new node. If an existing link is found, it is removed, since the new connection supersedes the old connection in determining the causal relationship.
At the end of the get_graph function there is a forward pruning step. For each root node, we traverse forward through the graph. Each output arc is evaluated to see if the pointed to node belongs to the used node set. If the pointed to node already belongs to the used node set, the output arc is removed and we continue to the next output arc or go back to the parent. If the pointed to node does not belong to the used node set, we continue the forward search until we find an output arc in the used node set or we reach a leaf node. Nodes are put into the used node set when backing up to a parent node. The forward pruning step ensures that each node within the same flow, i.e., the tree emanating from the root, will only have one parent as seen from the output arcs. Note, however, that all input arcs are preserved and thus the predecessor causal relationships for each node are maintained. This is important for the creation of wait actions when forming the behavior tree.