BehaviorTree.CPP icon indicating copy to clipboard operation
BehaviorTree.CPP copied to clipboard

Should nodes loop internally or not?

Open maxpfingsthorn opened this issue 6 years ago • 12 comments
trafficstars

I noticed that all nodes with multiple children (sequence, fallback, reactive*, retry, repeat, etc.) loop over children internally with while's and for's in their implementation of tick(). I wonder if this is a design decision, and if yes, why is this the case? Due to perceived efficiency?

I would argue that for each executeTick() on the root node, only one leaf should have their tick() method called. This would imply descending into the tree from the root every executeTick(), but I believe this cost would be negligible. However, tick()-ing only one leaf per executeTick() iteration would be more in line with the conceptual behavior tree model and it would help scheduling/preempting a lot.

What do you think?

maxpfingsthorn avatar Mar 12 '19 17:03 maxpfingsthorn

Hi @maxpfingsthorn

this is a very good question, and I am struggling to find the right answer myself, because all of of my users have a different opinion.

I agree with you that semantic correctness MUST be preferable over perceived efficiency; people usually believe that tree traversal is computationally expensive, even when it is not.

I agree with you the "internal loop" precludes the ability of a reactive tree to preempt a branch.

Unfortunately, this is a quite important change that might break someone else code in some subtle way...

Summarizing, I do see your point and I actually share it, but such a change needs a debate among users of these library; in such a debate, efficiency MUST not be considered, in my opinion, only correctness and predictability.

facontidavide avatar Mar 12 '19 17:03 facontidavide

Hi @facontidavide,

Ok, I see your point on this being a potentially breaking change. I would be happy to discuss.

What is a good forum to do this?

maxpfingsthorn avatar Mar 12 '19 19:03 maxpfingsthorn

No forum, unfortunately. Take a loop to this branch: https://github.com/BehaviorTree/BehaviorTree.CPP/tree/preemptable_loop

Is this what you had in mind?

I realized that the Reactive ControlNodes cannot be modified. Take a look at the commit for more details: 2dc84b1886bdade079c9beab931ef4f93256b7a6

facontidavide avatar Mar 13 '19 14:03 facontidavide

That looks very good to me! Thank you.

For the reactive controls: Why not? The implementation would be the same as for the ones with memory, but would reset the index on RUNNING as well. Or did I miss something?

maxpfingsthorn avatar Mar 14 '19 13:03 maxpfingsthorn

@v-lopez @mjeronimo @miccol @Masadow @Thordreck

Would you like to join this discussion?

facontidavide avatar Mar 20 '19 14:03 facontidavide

I am also still struggling to find the right answer. I guess here we are talking about asynchronous action nodes. I think in some practical cases we need to have actions that loop internally (e.g. a navigation action what waits in a loop until the goal is reached). The workaround was to check, at each loop, if the action is halted or not.

miccol avatar Mar 20 '19 15:03 miccol

I guess here we are talking about asynchronous action nodes.

Quite the opposite, I am worry about synchronous nodes.

Consider for instance this tree

<ReactiveSequence>
    <CheckBattery>
    <Repeat num_cycles="10">
          <DoSomething>
    </Repeat>
</ReactiveSequence>

Let's suppose that DoSomething is synchronous and requires 1 second to be completed. The condition CheckBattery becomes false after 5 seconds.

The entire sequence will always take 10 seconds; this goes against the "Minimum WTF principle". image

Unfortunately, since Repeat loops internally, CheckBatteryis invoked only once, not 10 times.

Allow me to use the word "atomic" as synonym of "synchronous" to make my point.

If Sequence, Fallback, Repeat or RetryUntilSuccesfull contains only atomic children, that ControlNode / DecoratorNode itself becomes atomic, i.e impossible to halt.

This is probably not what we want: in the previous example, a user would expect CheckBattery to be called many times.

The problem with the new semantic proposed by @maxpfingsthorn is that all those Conditions inside Reactive ControlNodes might be called MUCH more often, but this is the point of reactive trees, isn't it?

facontidavide avatar Mar 20 '19 15:03 facontidavide

I agree that the example you provided should call check battery several times.

In my mind, the only "blocking" nodes should be the the sync action nodes. Any intermediate decorator that needs to loop should return RUNNING.

v-lopez avatar Mar 20 '19 18:03 v-lopez

What confused me the most in the beginning was that we constantly call executeTick() from the root node when we use asynchronous nodes. I thought that because of these internal loops we could call executeTick() only once in the beginning and let it propagate through all nodes of the tree with Sequences ticking asynchronous children internally while they run. As this is not the case, I would agree with @maxpfingsthorn and prefer to tick only one leaf per executeTick(). However, as @miccol said, this would require to make an exception for ReactiveSequences with asynchronous nodes.

Regarding your example, I don't see why you should use a ReactiveSequence if you're not using an asynchronous node. I would expect an implementation like this to achieve your desired behavior

<Repeat num_cycles="10">
    <Sequence>
        <CheckBattery>
        <DoSomething>
    </Sequence>
</Repeat>

One thing I think should also be mentioned here are the semantics of two asynchronous nodes inside a ReactiveSequence. This will currently result in an infinite loop. However, I think it would be an interesting tool to have, let's call it a ParallelAsynchronousNode, which allows to execute two or more ansynchronous nodes (only once) at the same time. However, I couldn't find any theory on this kind of behavior and its semantics would have to be discussed.

hlzl avatar Mar 20 '19 19:03 hlzl

hi @hlzl , thanks for joining the discussion.

About the need to executeTick() repeatedly, that is "by design", otherwise a tree is not preemptable. But I need to make it less confusing in the documentation.

The alternative tree you suggested would work. what worries me is how intuitive a solution is compared to another for a newbie.

The problem with reactive nodes as you pointed out, is indeed that a ReactiveSequence or ReactiveFallback must not have more than an asynchronous child. This is also a potential source of bugs for many people.

In the past, I used NodeStatus::IDLE to avoid ticking twice already ticked Actions, but @v-lopez complained about that too.

I am now thinking which is the least of two evils....

facontidavide avatar Mar 20 '19 20:03 facontidavide

As a relative newcomer to BehaviorTrees, the semantic difference between

<ReactiveSequence>
    <CheckBattery>
    <Repeat num_cycles="10">
          <DoSomething>
    </Repeat>
</ReactiveSequence>

and

<Repeat num_cycles="10">
    <Sequence>
        <CheckBattery>
        <DoSomething>
    </Sequence>
</Repeat>

is clear to me.

However, I find "ReactiveSequence or ReactiveFallback must not have more than one asynchronous child" confusing. I think of an action node simply returning its current state (RUNNING, etc.) and don't see why a parent node needs to know the internals details of an action node, such as whether it asynchronously communicating with another thread or process.

mjeronimo avatar Mar 20 '19 22:03 mjeronimo

In my mind, the only "blocking" nodes should be the the sync action nodes. Any intermediate decorator that needs to loop should return RUNNING.

I agree.

The problem with reactive nodes as you pointed out, is indeed that a ReactiveSequence or ReactiveFallback must not have more than an asynchronous child. This is also a potential source of bugs for many people

I have a solution to this in mind. I will discuss it in another thread.

In the past, I used NodeStatus::IDLE to avoid ticking twice already ticked Actions, but @v-lopez complained about that too.

Is the discussion about somewhere? I used to like the IDLE status

miccol avatar Mar 21 '19 07:03 miccol

changed in version 4. I decided to allow some Control/Decorators to return RUNNING between one child and the next

facontidavide avatar Nov 22 '22 22:11 facontidavide