Skip to content

Commit cf9c749

Browse files
race and short circuit on first completion (#51)
Thanks for your work on this crate, I've been loving using it for robotics recently! I recently was trying to make a timeout behavior and realized that: - WhenAny - races children in parallel but short-circuits on first Success (ignores failures until all fail) - WhenAll - races children in parallel but short-circuits on first Failure - No node short-circuits on first completion regardless of success/failure I think it would be helpful to have a node that runs things in parallel and take the result of whichever finishes first so you can do stuff like this: ```rust /// fails if the behavior doesnt finish before the timer is up. fn with_timeout<A: Clone>(behavior: Behavior<A>, timeout: f64) -> Behavior<A> { Race(vec![ behavior, Invert(Box::new(Wait(timeout))), ]) } ``` You can achieve a similar thing with a while loop but the caveat is that if the inner behavior suceeds before the timer is up itll restart. Of course ways to use the blackboard to do timeouts without adding a new node but I think this one might be helpful.
1 parent 27494b6 commit cf9c749

15 files changed

Lines changed: 261 additions & 10 deletions

File tree

.github/workflows/rust-pr.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ jobs:
1313
steps:
1414
- name: Install libudev
1515
run: sudo apt-get update && sudo apt-get install libudev-dev pkg-config librust-alsa-sys-dev
16-
- uses: actions/checkout@v2
17-
- uses: actions/setup-python@v2
18-
- uses: pre-commit/action@v2.0.3
16+
- uses: actions/checkout@v6.0.2
17+
- uses: actions/setup-python@v6.2.0
18+
- uses: pre-commit/action@v3.0.1
1919
build:
2020
runs-on: ubuntu-latest
2121
steps:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ For example, if you have a state `A` and a state `B`:
5555
- Do `A`, `B` repeatedly while `LoopCondition` runs: `WhileAll(LoopCondition, [A, B])`. After *All* nodes `A`, `B` are completed successfully, check the condition node.
5656
- Run `A` and `B` in parallell and wait for both to succeed: `WhenAll([A, B])`
5757
- Run `A` and `B` in parallell and wait for any to succeed: `WhenAny([A, B])`
58+
- Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])`
5859
- Run `A` and `B` in parallell, but `A` has to succeed before `B`: `After([A, B])`
5960

6061
See the `Behavior` enum for more information.

bonsai/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ name = "bonsai-bt"
1212
readme = "../README.md"
1313
repository = "https://github.com/sollimann/bonsai.git"
1414
rust-version = "1.80.0"
15-
version = "0.10.0"
15+
version = "0.11.0"
1616

1717
[lib]
1818
name = "bonsai_bt"

bonsai/src/behavior.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ pub enum Behavior<A> {
128128
/// Succeeds if all behaviors succeed, but only if succeeding in sequence.
129129
/// Fails if one behavior fails.
130130
After(Vec<Behavior<A>>),
131+
/// Runs all behaviors in parallel until one completes (succeeds or fails).
132+
///
133+
/// Returns the status of the first behavior to complete,
134+
/// whether that is `Success` or `Failure`.
135+
/// If all behaviors remain `Running`, returns `Running`.
136+
Race(Vec<Behavior<A>>),
131137
}
132138

133139
#[cfg(test)]

bonsai/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
//! - Do `A`, `B` forever: `While(WaitForever, [A, B])`
2525
//! - Run `A` and `B` in parallell and wait for both to succeed: `WhenAll([A, B])`
2626
//! - Run `A` and `B` in parallell and wait for any to succeed: `WhenAny([A, B])`
27+
//! - Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])`
2728
//! - Run `A` and `B` in parallell, but `A` has to succeed before `B`: `After([A, B])`
2829
//!
2930
//! See the `Behavior` enum for more information.
@@ -118,7 +119,7 @@
118119
//! ```
119120
120121
pub use behavior::Behavior::{
121-
self, Action, After, AlwaysSucceed, If, Invert, Select, Sequence, Wait, WaitForever, WhenAll, WhenAny, While,
122+
self, Action, After, AlwaysSucceed, If, Invert, Race, Select, Sequence, Wait, WaitForever, WhenAll, WhenAny, While,
122123
WhileAll,
123124
};
124125

bonsai/src/state.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ pub(crate) enum State<A> {
102102
/// Keeps track of a `WhenAny` behavior. As the states finish, they are set
103103
/// to [`None`].
104104
WhenAny(Vec<Option<State<A>>>),
105+
/// Keeps track of a `Race` behavior.
106+
Race(Vec<Option<State<A>>>),
105107
/// Keeps track of an `After` behavior.
106108
After {
107109
/// The index of the next state that must succeed.
@@ -166,6 +168,7 @@ impl<A: Clone> State<A> {
166168
}
167169
Behavior::WhenAll(all) => State::WhenAll(all.into_iter().map(|ev| Some(State::new(ev))).collect()),
168170
Behavior::WhenAny(any) => State::WhenAny(any.into_iter().map(|ev| Some(State::new(ev))).collect()),
171+
Behavior::Race(behaviors) => State::Race(behaviors.into_iter().map(|ev| Some(State::new(ev))).collect()),
169172
Behavior::After(after_all) => State::After {
170173
next_success_index: 0,
171174
states: after_all.into_iter().map(State::new).collect(),
@@ -404,6 +407,20 @@ impl<A: Clone> State<A> {
404407
let any = true;
405408
when_all(any, upd, cursors, e, f, blackboard)
406409
}
410+
(_, &mut Race(ref mut cursors)) => {
411+
// return the result of the first child to complete,
412+
// regardless of whether it succeeds or fails.
413+
for cur in cursors.iter_mut() {
414+
match *cur {
415+
None => {}
416+
Some(ref mut state) => match state.tick(e, blackboard, f) {
417+
(Running, _) => continue,
418+
(status, dt) => return (status, dt),
419+
},
420+
}
421+
}
422+
RUNNING
423+
}
407424
(
408425
_,
409426
&mut After {

bonsai/src/visualizer.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub(crate) enum NodeType<A> {
1919
WhenAll,
2020
WhenAny,
2121
After,
22+
Race,
2223
}
2324

2425
impl<A: Clone + Debug, K: Debug> BT<A, K> {
@@ -125,6 +126,13 @@ impl<A: Clone + Debug, K: Debug> BT<A, K> {
125126
Self::dfs_recursive(graph, b, node_id)
126127
}
127128
}
129+
Behavior::Race(behaviors) => {
130+
let node_id = graph.add_node(NodeType::Race);
131+
graph.add_edge(parent_node, node_id, 1);
132+
for b in behaviors {
133+
Self::dfs_recursive(graph, b, node_id)
134+
}
135+
}
128136
}
129137
}
130138
}

bonsai/src/when_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ where
5555
terminated += 1;
5656
*cur = None;
5757
}
58+
#[allow(clippy::manual_unwrap_or)]
5859
match terminated {
5960
// If there are no events, there is a whole 'dt' left.
6061
0 if cursors.is_empty() => (

bonsai/tests/behavior_tests.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::behavior_tests::TestActions::{Dec, Inc, LessThan, LessThanRunningSuccess};
22
use bonsai_bt::{
3-
Action, ActionArgs, After, AlwaysSucceed, Event, Failure, Float, If, Invert, Select, Sequence, Status::Running,
4-
Success, UpdateArgs, Wait, WaitForever, WhenAll, While, WhileAll, BT,
3+
Action, ActionArgs, After, AlwaysSucceed, Event, Failure, Float, If, Invert, Race, Select, Sequence,
4+
Status::Running, Success, UpdateArgs, Wait, WaitForever, WhenAll, WhenAny, While, WhileAll, BT,
55
};
66

77
/// Some test actions.
@@ -610,3 +610,95 @@ fn test_repeat_sequence_empty() {
610610
// panics because no behaviors...
611611
let _state = BT::new(after, ());
612612
}
613+
614+
#[test]
615+
fn race_returns_first_success() {
616+
let a: i32 = 0;
617+
// Inc succeeds immediately, Wait is still running
618+
let behavior = Race(vec![Action(Inc), Wait(10.0)]);
619+
let mut state = BT::new(behavior, ());
620+
let (a, s, _) = tick(a, 0.1, &mut state);
621+
assert_eq!(a, 1);
622+
assert_eq!(s, Success);
623+
}
624+
625+
#[test]
626+
fn race_returns_first_failure() {
627+
let a: i32 = 5;
628+
// LessThan(1) fails immediately since 5 >= 1, Wait is still running
629+
let behavior = Race(vec![Action(LessThan(1)), Wait(10.0)]);
630+
let mut state = BT::new(behavior, ());
631+
let (a, s, _) = tick(a, 0.1, &mut state);
632+
assert_eq!(a, 5);
633+
assert_eq!(s, Failure);
634+
}
635+
636+
#[test]
637+
fn race_running_until_first_completes() {
638+
let a: i32 = 0;
639+
// Both children are time-based, neither completes on first tick
640+
let behavior = Race(vec![Wait(1.0), Wait(2.0)]);
641+
let mut state = BT::new(behavior, ());
642+
643+
// After 0.5s, both still running
644+
let (_a, s, _) = tick(a, 0.5, &mut state);
645+
assert_eq!(s, Running);
646+
647+
// After another 0.5s (total 1.0s), first Wait completes with Success
648+
let (_a, s, _) = tick(_a, 0.5, &mut state);
649+
assert_eq!(s, Success);
650+
}
651+
652+
#[test]
653+
fn race_second_child_wins_if_first_is_running() {
654+
let a: i32 = 0;
655+
let behavior = Race(vec![WaitForever, Action(Inc)]);
656+
let mut state = BT::new(behavior, ());
657+
let (a, s, _) = tick(a, 0.1, &mut state);
658+
assert_eq!(a, 1);
659+
assert_eq!(s, Success);
660+
}
661+
662+
#[test]
663+
fn race_failure_short_circuits_unlike_when_any() {
664+
// the main difference from WhenAny:
665+
// WhenAny would swallow the failure and keep running.
666+
// Race returns the failure immediately.
667+
let a: i32 = 5;
668+
// LessThan(1) fails immediately (5 >= 1), Wait(10.0) is still running
669+
let behavior = Race(vec![Action(LessThan(1)), Wait(10.0)]);
670+
let mut state = BT::new(behavior, ());
671+
let (a, s, _) = tick(a, 0.1, &mut state);
672+
assert_eq!(s, Failure);
673+
674+
// for WhenAny: same children, but failure is swallowed
675+
let behavior_any = WhenAny(vec![Action(LessThan(1)), Wait(10.0)]);
676+
let mut state_any = BT::new(behavior_any, ());
677+
let (_, s_any, _) = tick(a, 0.1, &mut state_any);
678+
assert_eq!(s_any, Running);
679+
}
680+
681+
#[test]
682+
fn race_timeout_pattern() {
683+
let a: i32 = 0;
684+
// Simulate a "slow action" using WaitForever with a 1-second timeout.
685+
// The timeout (Wait) fires first.
686+
let behavior = Race(vec![WaitForever, Wait(1.0)]);
687+
let mut state = BT::new(behavior, ());
688+
689+
let (_, s, _) = tick(a, 0.5, &mut state);
690+
assert_eq!(s, Running);
691+
692+
let (_, s, _) = tick(a, 0.5, &mut state);
693+
assert_eq!(s, Success);
694+
}
695+
696+
#[test]
697+
fn race_empty() {
698+
let a: i32 = 0;
699+
let behavior = Race(vec![]);
700+
let mut state = BT::new(behavior, ());
701+
let (_, s, _) = tick(a, 0.1, &mut state);
702+
// No children means nothing can complete, stays Running
703+
assert_eq!(s, Running);
704+
}

docs/concepts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ For example, if you have a state `A` and a state `B`:
5656
- Do `A`, `B` forever: `While(WaitForever, [A, B])`
5757
- Run `A` and `B` in parallel and wait for both to succeed: `WhenAll([A, B])`
5858
- Run `A` and `B` in parallel and wait for any to succeed: `WhenAny([A, B])`
59+
- Run `A` and `B` in parallell and wait for any to complete regardless of success or failure: `Race([A, B])`
5960
- Run `A` and `B` in parallel, but `A` has to succeed before `B`: `After([A, B])`
6061

6162
See the `Behavior` enum for more information.

0 commit comments

Comments
 (0)