py_branches 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_branches/__init__.py +9 -0
- py_branches/alternating.py +215 -0
- py_branches/blackboard.py +110 -0
- py_branches/cooldown.py +65 -0
- py_branches/counter.py +73 -0
- py_branches/latch.py +51 -0
- py_branches/pause.py +134 -0
- py_branches/random.py +131 -0
- py_branches/retry.py +77 -0
- py_branches/timeout.py +45 -0
- py_branches-1.2.1.dist-info/LICENSE +31 -0
- py_branches-1.2.1.dist-info/METADATA +172 -0
- py_branches-1.2.1.dist-info/RECORD +14 -0
- py_branches-1.2.1.dist-info/WHEEL +4 -0
py_branches/__init__.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import py_trees
|
|
3
|
+
import random
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ActivateBehavior(py_trees.decorators.Decorator):
|
|
9
|
+
'''
|
|
10
|
+
Enables activation of a behavior from an external source as long as it has a handle to
|
|
11
|
+
this decorator.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
child(Behavior): The child behavior that is being activated or not activated.
|
|
15
|
+
name(str): Name of this behavior
|
|
16
|
+
activate(bool): Whether or not to start this behavior activated or not.
|
|
17
|
+
'''
|
|
18
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
19
|
+
name: str,
|
|
20
|
+
activate: bool,
|
|
21
|
+
success_if_skip:bool=False):
|
|
22
|
+
super(ActivateBehavior, self).__init__(name=name, child=child)
|
|
23
|
+
self._activate = activate
|
|
24
|
+
self._success_if_skip = success_if_skip
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def activate(self):
|
|
28
|
+
return self._activate
|
|
29
|
+
|
|
30
|
+
@activate.setter
|
|
31
|
+
def activate(self, activate: bool):
|
|
32
|
+
self._activate = activate
|
|
33
|
+
|
|
34
|
+
def tick(self):
|
|
35
|
+
if not self._activate:
|
|
36
|
+
if self._success_if_skip:
|
|
37
|
+
self.stop(py_trees.common.Status.SUCCESS)
|
|
38
|
+
else:
|
|
39
|
+
self.stop(py_trees.common.Status.FAILURE)
|
|
40
|
+
yield self
|
|
41
|
+
else:
|
|
42
|
+
for node in super().tick():
|
|
43
|
+
yield node
|
|
44
|
+
|
|
45
|
+
def update(self) -> py_trees.common.Status:
|
|
46
|
+
return self.decorated.status
|
|
47
|
+
|
|
48
|
+
class _RunAlternatingHelper(py_trees.behaviour.Behaviour):
|
|
49
|
+
def __init__(self, name: str, activatable_behaviors: List[ActivateBehavior], counts: List[int]):
|
|
50
|
+
self._counts = counts
|
|
51
|
+
self._current_behavior_idx = 0
|
|
52
|
+
self._current_behavior_num_consecutive_runs = 0
|
|
53
|
+
self._activatable_behaviors = activatable_behaviors
|
|
54
|
+
self._activatable_behaviors[0].activate = True
|
|
55
|
+
|
|
56
|
+
super().__init__(name)
|
|
57
|
+
|
|
58
|
+
def initialise(self) -> None:
|
|
59
|
+
if self._current_behavior_num_consecutive_runs >= self._counts[self._current_behavior_idx]:
|
|
60
|
+
self._activatable_behaviors[self._current_behavior_idx].activate = False
|
|
61
|
+
self._current_behavior_idx = (self._current_behavior_idx + 1) % len(self._counts)
|
|
62
|
+
self._activatable_behaviors[self._current_behavior_idx].activate = True
|
|
63
|
+
self._current_behavior_num_consecutive_runs = 0
|
|
64
|
+
|
|
65
|
+
self._current_behavior_num_consecutive_runs += 1
|
|
66
|
+
|
|
67
|
+
def update(self) -> py_trees.common.Status:
|
|
68
|
+
return py_trees.common.Status.FAILURE
|
|
69
|
+
|
|
70
|
+
def run_alternating(name: str, behaviors: List[py_trees.behaviour.Behaviour], counts: List[int]):
|
|
71
|
+
'''
|
|
72
|
+
Args:
|
|
73
|
+
name(str): Name of the behavior
|
|
74
|
+
behaviors(List[Behaviors]): List of all the behaviors to run alternating.
|
|
75
|
+
count(List[int]): A list of how many times the corresponding behavior ought to be ran.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
A if behaviors is [behavior_a, behavior_b, behavior_c] and count is [3,2,4] then
|
|
79
|
+
behavior_a will run 3 times in a row, behavior_b will run 2 times in a row, and
|
|
80
|
+
behavior_c will run 4 times in a row before repeating.
|
|
81
|
+
'''
|
|
82
|
+
if 0 in counts:
|
|
83
|
+
raise ValueError(f'counts({counts}) can not have 0 in the list.')
|
|
84
|
+
if len(counts) != len(behaviors):
|
|
85
|
+
raise ValueError('len(counts) != len(behaviors), two lists must be of same length.')
|
|
86
|
+
|
|
87
|
+
alternating_behaviors = []
|
|
88
|
+
for idx, behavior in enumerate(behaviors):
|
|
89
|
+
activate_decorator = ActivateBehavior(behavior, f'activate_{behavior.name}', False)
|
|
90
|
+
alternating_behaviors.append(activate_decorator)
|
|
91
|
+
run_alternating_helper = _RunAlternatingHelper(f'{name}_helper', alternating_behaviors, counts)
|
|
92
|
+
|
|
93
|
+
children = []
|
|
94
|
+
children.append(run_alternating_helper)
|
|
95
|
+
children += alternating_behaviors
|
|
96
|
+
|
|
97
|
+
run_alternating_selector = py_trees.composites.Selector(name, False, children)
|
|
98
|
+
return run_alternating_selector
|
|
99
|
+
|
|
100
|
+
class RunEveryRange(py_trees.decorators.Decorator):
|
|
101
|
+
'''
|
|
102
|
+
Enables the activation of child every for a range of calls within
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
child(Behavior): The child behavior that is being activated or not activated.
|
|
106
|
+
name(str): Name of this behavior.
|
|
107
|
+
max_range(int): Maximum number of runs before the iterations resets to 1.
|
|
108
|
+
run_range(Tuple[int, int]): Determines which range of iterations that the child
|
|
109
|
+
will execute for. Range is inclusive.
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
E: Executes that cycle.
|
|
113
|
+
S: Skips that cycle.
|
|
114
|
+
if max_range and run_range is:
|
|
115
|
+
6 and (4,6) the the child will run on the 4th, 5th, and 6th cycle.
|
|
116
|
+
S, S, S, E, E, E, S, S, S, E, E, E, S, S, S, ...
|
|
117
|
+
6 and (2,4) then the child will run on the 2nd, 3rd, and 4th cycle.
|
|
118
|
+
S, E, E, E, S, S, S, E, E, E, S, S, S, E, E, ...
|
|
119
|
+
'''
|
|
120
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
121
|
+
name: str,
|
|
122
|
+
max_range: int,
|
|
123
|
+
run_range: Tuple[int, int],
|
|
124
|
+
success_if_skip: bool = False):
|
|
125
|
+
if run_range[0] > run_range[1]:
|
|
126
|
+
raise ValueError('run_range must be a tuple with (smaller_number, bigger_number)')
|
|
127
|
+
if run_range[0] < 1:
|
|
128
|
+
raise ValueError('Lower run range must be greater or equal to 1')
|
|
129
|
+
if run_range[1] > max_range:
|
|
130
|
+
raise ValueError(f'Upper run range must be lower or equal to {max_range}')
|
|
131
|
+
|
|
132
|
+
super(RunEveryRange, self).__init__(name=name, child=child)
|
|
133
|
+
self._max_range = max_range
|
|
134
|
+
self._run_range = run_range
|
|
135
|
+
self._success_if_skip = success_if_skip
|
|
136
|
+
self._iteration = 1
|
|
137
|
+
|
|
138
|
+
def tick(self):
|
|
139
|
+
if self._run_range[0] <= self._iteration <= self._run_range[1]:
|
|
140
|
+
for node in super().tick():
|
|
141
|
+
yield node
|
|
142
|
+
else:
|
|
143
|
+
if self._success_if_skip:
|
|
144
|
+
self.stop(py_trees.common.Status.SUCCESS)
|
|
145
|
+
else:
|
|
146
|
+
self.stop(py_trees.common.Status.FAILURE)
|
|
147
|
+
yield self
|
|
148
|
+
|
|
149
|
+
def terminate(self, new_status: py_trees.common.Status) -> None:
|
|
150
|
+
self._iteration += 1
|
|
151
|
+
if self._iteration > self._max_range:
|
|
152
|
+
self._iteration = 1
|
|
153
|
+
|
|
154
|
+
def update(self) -> py_trees.common.Status:
|
|
155
|
+
return self.decorated.status
|
|
156
|
+
|
|
157
|
+
class RunEveryX(py_trees.decorators.Decorator):
|
|
158
|
+
'''
|
|
159
|
+
Enables the activation of child every X calls. X can falls within a range
|
|
160
|
+
and gets recalculated every success.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
child(Behavior): The child behavior that is being activated or not activated.
|
|
164
|
+
name(str): Name of this behavior
|
|
165
|
+
every_x_range(Tuple[int, int]): Run the child ever however many cycles. Number of
|
|
166
|
+
cycles is within a range. Range is inclusive.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
E: Executes that cycle.
|
|
170
|
+
S: Skips that cycle.
|
|
171
|
+
if every_x_range is:
|
|
172
|
+
(1,1) then the child behavior will run every cycle.
|
|
173
|
+
E, E, E, E, E, E, E, E, E, E, E, E, E, E, E, ...
|
|
174
|
+
(5,5) then the child behavior will run every 5th cycle.
|
|
175
|
+
S, S, S, S, E, S, S, S, S, E, S, S, S, S, E, ...
|
|
176
|
+
(1,5) then the child behavior will run randomly between every
|
|
177
|
+
cycle or every 5th cycle. Changes every times it child gets executed.
|
|
178
|
+
S, S, E, S, S, S, S, E, E, S, S, S, E, S, S, ...
|
|
179
|
+
The execute cycle above goes:
|
|
180
|
+
3: S, S, E
|
|
181
|
+
5: S, S, S, S, E
|
|
182
|
+
1: E
|
|
183
|
+
4: S, S, S, E
|
|
184
|
+
'''
|
|
185
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
186
|
+
name: str,
|
|
187
|
+
every_x_range: Tuple[int, int],
|
|
188
|
+
success_if_skip:bool=False):
|
|
189
|
+
if every_x_range[0] > every_x_range[1]:
|
|
190
|
+
raise ValueError('every_x_range must be a tuple with (smaller_number, bigger_number)')
|
|
191
|
+
if every_x_range[0] < 1:
|
|
192
|
+
raise ValueError('Can not have range be lower than 1.')
|
|
193
|
+
|
|
194
|
+
super(RunEveryX, self).__init__(name=name, child=child)
|
|
195
|
+
self._every_x_range = every_x_range
|
|
196
|
+
self._cycles_remaining = random.randint(*self._every_x_range)-1
|
|
197
|
+
self._success_if_skip = success_if_skip
|
|
198
|
+
|
|
199
|
+
def initialise(self):
|
|
200
|
+
self._cycles_remaining = random.randint(*self._every_x_range)-1
|
|
201
|
+
|
|
202
|
+
def tick(self):
|
|
203
|
+
if self._cycles_remaining > 0:
|
|
204
|
+
self._cycles_remaining -= 1
|
|
205
|
+
if self._success_if_skip:
|
|
206
|
+
self.stop(py_trees.common.Status.SUCCESS)
|
|
207
|
+
else:
|
|
208
|
+
self.stop(py_trees.common.Status.FAILURE)
|
|
209
|
+
yield self
|
|
210
|
+
else:
|
|
211
|
+
for node in super().tick():
|
|
212
|
+
yield node
|
|
213
|
+
|
|
214
|
+
def update(self):
|
|
215
|
+
return self.decorated.status
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import py_trees
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_and_check(bb: py_trees.blackboard.Client, var: str, types: Optional[list], logger):
|
|
8
|
+
try:
|
|
9
|
+
value = bb.get(var)
|
|
10
|
+
except KeyError:
|
|
11
|
+
logger.warning(f'Tried to access blackboard variable {var} but it does not exist.')
|
|
12
|
+
return None
|
|
13
|
+
if value is None:
|
|
14
|
+
logger.warning(f'Tried to access blackboard variable {var} but it does not exist.')
|
|
15
|
+
return None
|
|
16
|
+
if types is not None and type(value) not in types:
|
|
17
|
+
logger.warning(f'Tried to access blackboard variable {var} ' +
|
|
18
|
+
f'of type {type(value)}, variable must be one of {types}.')
|
|
19
|
+
return None
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
class IncrementBlackboardVariable(py_trees.behaviour.Behaviour):
|
|
23
|
+
def __init__(self, name: str, variable_name: str, increment_by: float=1.0):
|
|
24
|
+
super(IncrementBlackboardVariable, self).__init__(name)
|
|
25
|
+
self._variable_name = variable_name
|
|
26
|
+
self._increment_by = increment_by
|
|
27
|
+
self._return_sucess = False
|
|
28
|
+
self._blackboard = py_trees.blackboard.Client()
|
|
29
|
+
self._blackboard.register_key(key=variable_name, access=py_trees.common.Access.WRITE)
|
|
30
|
+
|
|
31
|
+
def initialise(self):
|
|
32
|
+
self._return_sucess = False
|
|
33
|
+
current_value = _get_and_check(self._blackboard, self._variable_name, [int, float], self.logger)
|
|
34
|
+
if current_value is None:
|
|
35
|
+
self.logger.warning(
|
|
36
|
+
f'Failed to increment blackboard variable {self._variable_name}: value missing or invalid.'
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
self._blackboard.set(self._variable_name, current_value+self._increment_by)
|
|
40
|
+
self._return_sucess = True
|
|
41
|
+
|
|
42
|
+
def update(self):
|
|
43
|
+
if self._return_sucess:
|
|
44
|
+
return py_trees.common.Status.SUCCESS
|
|
45
|
+
else:
|
|
46
|
+
return py_trees.common.Status.FAILURE
|
|
47
|
+
|
|
48
|
+
class IncrementBlackboardVariableIfCondition(py_trees.decorators.Decorator):
|
|
49
|
+
def __init__(self, child, name: str, variable_name: str, condition: py_trees.common.Status, increment_by: float=1.0):
|
|
50
|
+
super(IncrementBlackboardVariableIfCondition, self).__init__(name=name, child=child)
|
|
51
|
+
self._variable_name = variable_name
|
|
52
|
+
self._condition = condition
|
|
53
|
+
self._increment_by = increment_by
|
|
54
|
+
self._blackboard = py_trees.blackboard.Client()
|
|
55
|
+
self._blackboard.register_key(key=variable_name, access=py_trees.common.Access.WRITE)
|
|
56
|
+
|
|
57
|
+
def update(self):
|
|
58
|
+
if self.decorated.status == self._condition:
|
|
59
|
+
current_value = _get_and_check(self._blackboard, self._variable_name, [int, float], self.logger)
|
|
60
|
+
if current_value is not None:
|
|
61
|
+
self._blackboard.set(self._variable_name, current_value+self._increment_by, overwrite=True)
|
|
62
|
+
|
|
63
|
+
return self.decorated.status
|
|
64
|
+
|
|
65
|
+
class SetBlackboardVariableIfCondition(py_trees.decorators.Decorator):
|
|
66
|
+
def __init__(self, child, name: str, variable_name: str, condition: py_trees.common.Status, set_to: Any):
|
|
67
|
+
super(SetBlackboardVariableIfCondition, self).__init__(name=name, child=child)
|
|
68
|
+
self._variable_name = variable_name
|
|
69
|
+
self._condition = condition
|
|
70
|
+
self._set_to = set_to
|
|
71
|
+
self._blackboard = py_trees.blackboard.Client()
|
|
72
|
+
self._blackboard.register_key(key=variable_name, access=py_trees.common.Access.WRITE)
|
|
73
|
+
|
|
74
|
+
def update(self):
|
|
75
|
+
if self.decorated.status == self._condition:
|
|
76
|
+
self._blackboard.set(self._variable_name, self._set_to, overwrite=True)
|
|
77
|
+
|
|
78
|
+
return self.decorated.status
|
|
79
|
+
|
|
80
|
+
class RunIfBlackboardVariableEquals(py_trees.decorators.Decorator):
|
|
81
|
+
def __init__(self, child, name: str, variable_name: str, equals: Any, success_if_skip: bool=True):
|
|
82
|
+
super(RunIfBlackboardVariableEquals, self).__init__(name=name, child=child)
|
|
83
|
+
self._variable_name = variable_name
|
|
84
|
+
self._equals = equals
|
|
85
|
+
self._blackboard = py_trees.blackboard.Client()
|
|
86
|
+
self._blackboard.register_key(key=variable_name, access=py_trees.common.Access.READ)
|
|
87
|
+
self._run_child = False
|
|
88
|
+
self._ret_status_on_failure = py_trees.common.Status.SUCCESS if success_if_skip else py_trees.common.Status.FAILURE
|
|
89
|
+
|
|
90
|
+
def tick(self):
|
|
91
|
+
# Re-evaluate the condition on each fresh entry; preserve it while child is RUNNING.
|
|
92
|
+
if self.status != py_trees.common.Status.RUNNING:
|
|
93
|
+
current_value = _get_and_check(self._blackboard, self._variable_name, None, self.logger)
|
|
94
|
+
self._run_child = current_value == self._equals
|
|
95
|
+
|
|
96
|
+
if self._run_child:
|
|
97
|
+
for node in py_trees.decorators.Decorator.tick(self):
|
|
98
|
+
yield node
|
|
99
|
+
else:
|
|
100
|
+
for node in py_trees.behaviour.Behaviour.tick(self):
|
|
101
|
+
yield node
|
|
102
|
+
|
|
103
|
+
def update(self):
|
|
104
|
+
if self._run_child:
|
|
105
|
+
if self.decorated.status != py_trees.common.Status.RUNNING:
|
|
106
|
+
self._run_child = False
|
|
107
|
+
return self.decorated.status
|
|
108
|
+
else:
|
|
109
|
+
self._run_child = False
|
|
110
|
+
return self._ret_status_on_failure
|
py_branches/cooldown.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import time
|
|
3
|
+
import py_trees
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Cooldown(py_trees.decorators.Decorator):
|
|
7
|
+
'''
|
|
8
|
+
Prevents a child from running again until a cooldown period has elapsed
|
|
9
|
+
after its last completion.
|
|
10
|
+
|
|
11
|
+
The child runs normally on the first tick. Once it completes (SUCCESS or
|
|
12
|
+
FAILURE), a cooldown timer starts. During the cooldown, the child is not
|
|
13
|
+
ticked and this decorator returns FAILURE (or SUCCESS if
|
|
14
|
+
success_if_cooling=True). After the cooldown expires the child may run
|
|
15
|
+
again.
|
|
16
|
+
|
|
17
|
+
A child that stays RUNNING is never subject to the cooldown — the timer
|
|
18
|
+
only starts once the child actually finishes.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
child (Behaviour): The child behavior to rate-limit.
|
|
22
|
+
name (str): Name of this decorator.
|
|
23
|
+
duration (float): Cooldown period in seconds after each completion.
|
|
24
|
+
success_if_cooling (bool): Return SUCCESS instead of FAILURE while
|
|
25
|
+
cooling down. Default False.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
child = py_trees.behaviours.Success(name="Expensive")
|
|
29
|
+
# Run child freely, but enforce a 5-second gap between executions.
|
|
30
|
+
cooled = Cooldown(child, name="Cooldown", duration=5.0)
|
|
31
|
+
'''
|
|
32
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
33
|
+
name: str,
|
|
34
|
+
duration: float,
|
|
35
|
+
success_if_cooling: bool = False):
|
|
36
|
+
if duration <= 0.0:
|
|
37
|
+
raise ValueError(f'duration({duration}) must be positive.')
|
|
38
|
+
super(Cooldown, self).__init__(name=name, child=child)
|
|
39
|
+
self._duration = duration
|
|
40
|
+
self._success_if_cooling = success_if_cooling
|
|
41
|
+
self._cooling = False
|
|
42
|
+
self._cool_start = None
|
|
43
|
+
|
|
44
|
+
def tick(self):
|
|
45
|
+
if self._cooling:
|
|
46
|
+
elapsed = time.time() - self._cool_start
|
|
47
|
+
if elapsed < self._duration:
|
|
48
|
+
if self._success_if_cooling:
|
|
49
|
+
self.stop(py_trees.common.Status.SUCCESS)
|
|
50
|
+
else:
|
|
51
|
+
self.stop(py_trees.common.Status.FAILURE)
|
|
52
|
+
yield self
|
|
53
|
+
return
|
|
54
|
+
else:
|
|
55
|
+
self._cooling = False
|
|
56
|
+
|
|
57
|
+
for node in super().tick():
|
|
58
|
+
yield node
|
|
59
|
+
|
|
60
|
+
def update(self) -> py_trees.common.Status:
|
|
61
|
+
status = self.decorated.status
|
|
62
|
+
if status != py_trees.common.Status.RUNNING:
|
|
63
|
+
self._cooling = True
|
|
64
|
+
self._cool_start = time.time()
|
|
65
|
+
return status
|
py_branches/counter.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import py_trees
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Counter(py_trees.decorators.Decorator):
|
|
6
|
+
'''
|
|
7
|
+
Runs a child behavior exactly num_runs times (total completions), then
|
|
8
|
+
permanently returns completion_status without ever running the child again.
|
|
9
|
+
|
|
10
|
+
A "completion" is any tick where the child returns SUCCESS or FAILURE.
|
|
11
|
+
RUNNING ticks do not count — the counter waits for the child to finish each
|
|
12
|
+
run before counting it.
|
|
13
|
+
|
|
14
|
+
The run count and done flag persist across tree re-entries (i.e. they are
|
|
15
|
+
NOT reset by initialise()). This makes Counter suitable for one-time
|
|
16
|
+
initialization sequences. To reset and re-count, call reset() explicitly.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
child (Behaviour): The child behavior to count.
|
|
20
|
+
name (str): Name of this decorator.
|
|
21
|
+
num_runs (int): Total number of child completions to allow.
|
|
22
|
+
completion_status (Status): Status returned permanently once num_runs
|
|
23
|
+
completions have occurred. Default SUCCESS.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
child = InitializationBehavior(name="Init")
|
|
27
|
+
# Run Init exactly once; after it completes, always return SUCCESS.
|
|
28
|
+
counted = Counter(child, name="RunOnce", num_runs=1)
|
|
29
|
+
|
|
30
|
+
child = CalibrateStep(name="Calibrate")
|
|
31
|
+
# Run calibration exactly 3 times, then always return SUCCESS.
|
|
32
|
+
counted = Counter(child, name="Calibrate3x", num_runs=3)
|
|
33
|
+
'''
|
|
34
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
35
|
+
name: str,
|
|
36
|
+
num_runs: int,
|
|
37
|
+
completion_status: py_trees.common.Status = py_trees.common.Status.SUCCESS):
|
|
38
|
+
if num_runs < 1:
|
|
39
|
+
raise ValueError(f'num_runs({num_runs}) must be greater than 0.')
|
|
40
|
+
super(Counter, self).__init__(name=name, child=child)
|
|
41
|
+
self._num_runs = num_runs
|
|
42
|
+
self._completion_status = completion_status
|
|
43
|
+
self._runs_completed = 0
|
|
44
|
+
self._done = False
|
|
45
|
+
|
|
46
|
+
def reset(self) -> None:
|
|
47
|
+
'''Reset the run count so the child will be run num_runs times again.'''
|
|
48
|
+
self._runs_completed = 0
|
|
49
|
+
self._done = False
|
|
50
|
+
|
|
51
|
+
def tick(self):
|
|
52
|
+
if self._done:
|
|
53
|
+
self.stop(self._completion_status)
|
|
54
|
+
yield self
|
|
55
|
+
else:
|
|
56
|
+
for node in super().tick():
|
|
57
|
+
yield node
|
|
58
|
+
|
|
59
|
+
def update(self) -> py_trees.common.Status:
|
|
60
|
+
status = self.decorated.status
|
|
61
|
+
if status == py_trees.common.Status.RUNNING:
|
|
62
|
+
return py_trees.common.Status.RUNNING
|
|
63
|
+
|
|
64
|
+
# Child completed this tick (SUCCESS or FAILURE).
|
|
65
|
+
self._runs_completed += 1
|
|
66
|
+
if self._runs_completed >= self._num_runs:
|
|
67
|
+
self._done = True
|
|
68
|
+
self.decorated.stop(py_trees.common.Status.INVALID)
|
|
69
|
+
return self._completion_status
|
|
70
|
+
|
|
71
|
+
# More runs remain — reset child so it re-runs next tick.
|
|
72
|
+
self.decorated.stop(py_trees.common.Status.INVALID)
|
|
73
|
+
return py_trees.common.Status.RUNNING
|
py_branches/latch.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import py_trees
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Latch(py_trees.decorators.Decorator):
|
|
6
|
+
'''
|
|
7
|
+
Once the child returns SUCCESS, latches to SUCCESS and stops re-running
|
|
8
|
+
the child until reset() is called.
|
|
9
|
+
|
|
10
|
+
- While unlatched: child is ticked normally and its status is passed through.
|
|
11
|
+
- On first SUCCESS from child: latch engages.
|
|
12
|
+
- While latched: child is NOT ticked; this decorator returns SUCCESS.
|
|
13
|
+
- On reset(): latch disengages and the child will run again on the next tick.
|
|
14
|
+
|
|
15
|
+
The latch state persists across tree re-entries (i.e. it is NOT cleared by
|
|
16
|
+
initialise()). To get a fresh latch, call reset() explicitly.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
child (Behaviour): The child behavior to latch.
|
|
20
|
+
name (str): Name of this decorator.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
child = ExpensiveSetupBehavior(name="Setup")
|
|
24
|
+
# Run setup once; once it succeeds, always return SUCCESS without
|
|
25
|
+
# re-running it.
|
|
26
|
+
latched = Latch(child, name="Latch")
|
|
27
|
+
|
|
28
|
+
# Later, to trigger a re-run:
|
|
29
|
+
latched.reset()
|
|
30
|
+
'''
|
|
31
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
32
|
+
name: str):
|
|
33
|
+
super(Latch, self).__init__(name=name, child=child)
|
|
34
|
+
self._latched = False
|
|
35
|
+
|
|
36
|
+
def reset(self) -> None:
|
|
37
|
+
'''Disengage the latch so the child will run again on the next tick.'''
|
|
38
|
+
self._latched = False
|
|
39
|
+
|
|
40
|
+
def tick(self):
|
|
41
|
+
if self._latched:
|
|
42
|
+
self.stop(py_trees.common.Status.SUCCESS)
|
|
43
|
+
yield self
|
|
44
|
+
else:
|
|
45
|
+
for node in super().tick():
|
|
46
|
+
yield node
|
|
47
|
+
|
|
48
|
+
def update(self) -> py_trees.common.Status:
|
|
49
|
+
if self.decorated.status == py_trees.common.Status.SUCCESS:
|
|
50
|
+
self._latched = True
|
|
51
|
+
return self.decorated.status
|
py_branches/pause.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import py_trees
|
|
5
|
+
import datetime
|
|
6
|
+
import random
|
|
7
|
+
import yaml
|
|
8
|
+
import os
|
|
9
|
+
from typing import Dict
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
HOUR2SEC = 3600
|
|
14
|
+
MIN2SEC = 60
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PauseUniform(py_trees.behaviour.Behaviour):
|
|
18
|
+
def __init__(self, name: str, low: float, high: float):
|
|
19
|
+
super(PauseUniform, self).__init__(name=name)
|
|
20
|
+
self._high = high
|
|
21
|
+
self._low = low
|
|
22
|
+
|
|
23
|
+
def initialise(self):
|
|
24
|
+
self._pause_t = random.uniform(self._low, self._high)
|
|
25
|
+
self._start_t = time.time()
|
|
26
|
+
|
|
27
|
+
def update(self):
|
|
28
|
+
t_elapse = time.time() - self._start_t
|
|
29
|
+
if t_elapse < self._pause_t:
|
|
30
|
+
return py_trees.common.Status.RUNNING
|
|
31
|
+
else:
|
|
32
|
+
return py_trees.common.Status.SUCCESS
|
|
33
|
+
|
|
34
|
+
def load_schedule_file(schedule_filepath: str):
|
|
35
|
+
if not os.path.isfile(schedule_filepath):
|
|
36
|
+
raise FileNotFoundError(f'schedule_filepath: {schedule_filepath} is not a valid file')
|
|
37
|
+
|
|
38
|
+
with open(schedule_filepath, 'r') as schedule_file:
|
|
39
|
+
schedule_raw = yaml.safe_load(schedule_file)
|
|
40
|
+
|
|
41
|
+
schedule = []
|
|
42
|
+
for schedule_element_raw in schedule_raw:
|
|
43
|
+
start_pause_time = datetime.datetime.strptime(schedule_element_raw['start_pause_time'], '%H:%M:%S').time()
|
|
44
|
+
stop_pause_time = datetime.datetime.strptime(schedule_element_raw['stop_pause_time'], '%H:%M:%S').time()
|
|
45
|
+
variance_time = datetime.datetime.strptime(schedule_element_raw['variance'], '%H:%M:%S').time()
|
|
46
|
+
schedule.append({'start_pause_time': start_pause_time,
|
|
47
|
+
'stop_pause_time': stop_pause_time,
|
|
48
|
+
'variance_time': variance_time,
|
|
49
|
+
'start_plus_variance_time': add_variance_to_datetime_time(start_pause_time, variance_time),
|
|
50
|
+
'stop_plus_variance_time': add_variance_to_datetime_time(stop_pause_time, variance_time)})
|
|
51
|
+
return schedule
|
|
52
|
+
|
|
53
|
+
class CheckPauseSchedule(py_trees.behaviour.Behaviour):
|
|
54
|
+
def __init__(self, name: str, schedule):
|
|
55
|
+
self._schedule = schedule
|
|
56
|
+
self._last_schedule_idx = None
|
|
57
|
+
super(CheckPauseSchedule, self).__init__(name=name)
|
|
58
|
+
|
|
59
|
+
def update(self):
|
|
60
|
+
now_time = datetime.datetime.now().time()
|
|
61
|
+
matched_schedule_idx = None
|
|
62
|
+
for idx, schedule_element in enumerate(self._schedule):
|
|
63
|
+
start = schedule_element['start_plus_variance_time']
|
|
64
|
+
stop = schedule_element['stop_plus_variance_time']
|
|
65
|
+
if (start < stop and start < now_time < stop) or \
|
|
66
|
+
(start > stop and (now_time > start or now_time < stop)):
|
|
67
|
+
matched_schedule_idx = idx
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
# Re-arm once we've left all windows.
|
|
71
|
+
if matched_schedule_idx is None:
|
|
72
|
+
self._last_schedule_idx = None
|
|
73
|
+
return py_trees.common.Status.FAILURE
|
|
74
|
+
|
|
75
|
+
# Prevent repeated SUCCESS ticks while remaining in the same window.
|
|
76
|
+
if matched_schedule_idx == self._last_schedule_idx:
|
|
77
|
+
return py_trees.common.Status.FAILURE
|
|
78
|
+
|
|
79
|
+
self._last_schedule_idx = matched_schedule_idx
|
|
80
|
+
return py_trees.common.Status.SUCCESS
|
|
81
|
+
|
|
82
|
+
def datetime_time_to_sec(t: datetime.time):
|
|
83
|
+
sec = t.hour*HOUR2SEC+t.minute*MIN2SEC+t.second
|
|
84
|
+
return sec
|
|
85
|
+
|
|
86
|
+
def add_variance_to_datetime_time(t: datetime.time, variance_time: datetime.time) -> datetime.time:
|
|
87
|
+
variance_sec = datetime_time_to_sec(variance_time)
|
|
88
|
+
variance_timedelta = datetime.timedelta(seconds=random.uniform(0.0, variance_sec))
|
|
89
|
+
time_to_datetime = datetime.datetime.combine(datetime.date.today(), t)
|
|
90
|
+
time_with_variance = (time_to_datetime + variance_timedelta).time()
|
|
91
|
+
return time_with_variance
|
|
92
|
+
|
|
93
|
+
class PauseSchedule(py_trees.behaviour.Behaviour):
|
|
94
|
+
def __init__(self, name: str, schedule: List[Dict[str, datetime.time]]):
|
|
95
|
+
self._schedule = schedule
|
|
96
|
+
super(PauseSchedule, self).__init__(name=name)
|
|
97
|
+
|
|
98
|
+
def initialise(self):
|
|
99
|
+
super().initialise()
|
|
100
|
+
self._t_wait = None
|
|
101
|
+
self._t_start = time.time()
|
|
102
|
+
now_time = datetime.datetime.now().time()
|
|
103
|
+
for schedule_element in self._schedule:
|
|
104
|
+
start = schedule_element['start_plus_variance_time']
|
|
105
|
+
stop = schedule_element['stop_plus_variance_time']
|
|
106
|
+
variance = schedule_element['variance_time']
|
|
107
|
+
if (start < stop and start < now_time < stop) or \
|
|
108
|
+
(start > stop and (now_time > start or now_time < stop)):
|
|
109
|
+
if now_time < stop:
|
|
110
|
+
self._t_wait = datetime_time_to_sec(stop) - \
|
|
111
|
+
datetime_time_to_sec(now_time)
|
|
112
|
+
else:
|
|
113
|
+
self._t_wait = datetime_time_to_sec(datetime.time(23, 59, 59)) + 1 - \
|
|
114
|
+
datetime_time_to_sec(now_time) + \
|
|
115
|
+
datetime_time_to_sec(stop)
|
|
116
|
+
self._t_start = time.time()
|
|
117
|
+
logging.info(f'Wait has been scheduled for {self._t_wait:.3f} sec')
|
|
118
|
+
schedule_element['start_plus_variance_time'] = \
|
|
119
|
+
add_variance_to_datetime_time(schedule_element['start_pause_time'], variance)
|
|
120
|
+
schedule_element['stop_plus_variance_time'] = \
|
|
121
|
+
add_variance_to_datetime_time(schedule_element['stop_pause_time'], variance)
|
|
122
|
+
logging.info(f'new start_plus_variance_time: {schedule_element["start_plus_variance_time"]}')
|
|
123
|
+
logging.info(f'new stop_plus_variance_time: {schedule_element["stop_plus_variance_time"]}')
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
def update(self):
|
|
127
|
+
if self._t_wait is None:
|
|
128
|
+
return py_trees.common.Status.SUCCESS
|
|
129
|
+
|
|
130
|
+
t_elapse = time.time() - self._t_start
|
|
131
|
+
if t_elapse < self._t_wait:
|
|
132
|
+
return py_trees.common.Status.RUNNING
|
|
133
|
+
else:
|
|
134
|
+
return py_trees.common.Status.SUCCESS
|
py_branches/random.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import py_trees
|
|
4
|
+
import random
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RandomRun(py_trees.decorators.Decorator):
|
|
11
|
+
'''
|
|
12
|
+
Random chance of running the child of this decorator.
|
|
13
|
+
'''
|
|
14
|
+
def __init__(self, child, name, probability: float, success_if_skip: bool = False):
|
|
15
|
+
if not (0 <= probability <= 1.0):
|
|
16
|
+
raise ValueError(f'Probability == {probability} but needs to be in range [0, 1.0]')
|
|
17
|
+
super(RandomRun, self).__init__(name=name, child=child)
|
|
18
|
+
self._probability = probability
|
|
19
|
+
self._run = None # rolled on first tick; terminate() handles all subsequent rolls
|
|
20
|
+
self._success_if_skip = success_if_skip
|
|
21
|
+
|
|
22
|
+
def tick(self):
|
|
23
|
+
if self._run is None:
|
|
24
|
+
self._run = random.random() <= self._probability
|
|
25
|
+
if not self._run:
|
|
26
|
+
for node in py_trees.behaviour.Behaviour.tick(self):
|
|
27
|
+
yield node
|
|
28
|
+
else:
|
|
29
|
+
for node in super().tick():
|
|
30
|
+
yield node
|
|
31
|
+
|
|
32
|
+
def update(self):
|
|
33
|
+
if self._run:
|
|
34
|
+
return self.decorated.status
|
|
35
|
+
else:
|
|
36
|
+
if self._success_if_skip:
|
|
37
|
+
return py_trees.common.Status.SUCCESS
|
|
38
|
+
else:
|
|
39
|
+
return py_trees.common.Status.FAILURE
|
|
40
|
+
|
|
41
|
+
def terminate(self, new_status: py_trees.common.Status) -> None:
|
|
42
|
+
self._run = random.random() <= self._probability
|
|
43
|
+
|
|
44
|
+
class RandomDelay(py_trees.decorators.Decorator):
|
|
45
|
+
'''
|
|
46
|
+
Waits a random duration before running the child on each fresh entry.
|
|
47
|
+
|
|
48
|
+
On every fresh entry (i.e. when the decorator was not already RUNNING),
|
|
49
|
+
a delay is sampled uniformly from [low, high] seconds. The decorator
|
|
50
|
+
stays RUNNING without ticking the child until the delay has elapsed, then
|
|
51
|
+
passes through to the child normally.
|
|
52
|
+
|
|
53
|
+
The delay is re-sampled on every new entry, so repeated executions each
|
|
54
|
+
get independent jitter. This is useful for desynchronising multiple
|
|
55
|
+
agents that share the same tree structure.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
child (Behaviour): The child behavior to delay.
|
|
59
|
+
name (str): Name of this decorator.
|
|
60
|
+
low (float): Minimum delay in seconds (>= 0).
|
|
61
|
+
high (float): Maximum delay in seconds (>= low).
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
child = py_trees.behaviours.Success(name="Action")
|
|
65
|
+
# Pause 0.5–2.0 seconds before running the child each time.
|
|
66
|
+
delayed = RandomDelay(child, name="RandomDelay", low=0.5, high=2.0)
|
|
67
|
+
'''
|
|
68
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
69
|
+
name: str,
|
|
70
|
+
low: float,
|
|
71
|
+
high: float):
|
|
72
|
+
if low < 0.0:
|
|
73
|
+
raise ValueError(f'low({low}) must be >= 0.')
|
|
74
|
+
if low > high:
|
|
75
|
+
raise ValueError(f'low({low}) must be <= high({high}).')
|
|
76
|
+
super(RandomDelay, self).__init__(name=name, child=child)
|
|
77
|
+
self._low = low
|
|
78
|
+
self._high = high
|
|
79
|
+
self._delay = 0.0
|
|
80
|
+
self._start_time = None
|
|
81
|
+
self._waiting = False
|
|
82
|
+
|
|
83
|
+
def tick(self):
|
|
84
|
+
# Fresh entry: sample a new delay and start the timer.
|
|
85
|
+
if self.status != py_trees.common.Status.RUNNING:
|
|
86
|
+
self._delay = random.uniform(self._low, self._high)
|
|
87
|
+
self._start_time = time.time()
|
|
88
|
+
self._waiting = True
|
|
89
|
+
|
|
90
|
+
if self._waiting:
|
|
91
|
+
if time.time() - self._start_time < self._delay:
|
|
92
|
+
self.status = py_trees.common.Status.RUNNING
|
|
93
|
+
yield self
|
|
94
|
+
return
|
|
95
|
+
self._waiting = False
|
|
96
|
+
|
|
97
|
+
for node in super().tick():
|
|
98
|
+
yield node
|
|
99
|
+
|
|
100
|
+
def update(self) -> py_trees.common.Status:
|
|
101
|
+
return self.decorated.status
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def random_selector(name, behaviors: List[py_trees.behaviour.Behaviour], probabilities: List[float]):
|
|
105
|
+
if abs(sum(probabilities) - 1.0) >= 1e-9:
|
|
106
|
+
raise ValueError(f'sum(probabilities) must add up to 1.0, got {sum(probabilities)}')
|
|
107
|
+
if len(probabilities) != len(behaviors):
|
|
108
|
+
raise ValueError('len(probabilities) != len(behaviors), two lists must be of same length.')
|
|
109
|
+
|
|
110
|
+
children = []
|
|
111
|
+
new_probabilities = []
|
|
112
|
+
cumulative_prob = 0.0
|
|
113
|
+
for behavior, raw_prob in zip(behaviors, probabilities):
|
|
114
|
+
new_prob = raw_prob/(1.0-cumulative_prob)
|
|
115
|
+
if new_prob >= 1.0:
|
|
116
|
+
children.append(behavior)
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
new_probabilities.append(new_prob)
|
|
120
|
+
decorated_behavior_name = f'random_run_{behavior.name}'
|
|
121
|
+
decorated_behavior = RandomRun(name=decorated_behavior_name,
|
|
122
|
+
child=behavior,
|
|
123
|
+
probability=new_prob)
|
|
124
|
+
children.append(decorated_behavior)
|
|
125
|
+
|
|
126
|
+
cumulative_prob += raw_prob
|
|
127
|
+
|
|
128
|
+
logging.debug(f'behaviors->new_probabilities: {[b.name for b in behaviors]}->{new_probabilities}')
|
|
129
|
+
|
|
130
|
+
selector = py_trees.composites.Selector(name, False, children)
|
|
131
|
+
return selector
|
py_branches/retry.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import time
|
|
3
|
+
import py_trees
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Retry(py_trees.decorators.Decorator):
|
|
7
|
+
'''
|
|
8
|
+
Retries a child behavior on FAILURE up to max_attempts times.
|
|
9
|
+
|
|
10
|
+
Returns SUCCESS if the child ever succeeds, FAILURE once all attempts
|
|
11
|
+
are exhausted. Stays RUNNING between attempts (and during optional
|
|
12
|
+
delay between retries).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
child (Behaviour): The child behavior to retry.
|
|
16
|
+
name (str): Name of this decorator.
|
|
17
|
+
max_attempts (int): Maximum number of times to attempt the child.
|
|
18
|
+
delay (float): Seconds to wait between retry attempts. Default 0.0.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
child = py_trees.behaviours.Failure(name="Flaky")
|
|
22
|
+
# Try up to 3 times; fails permanently after 3 failures.
|
|
23
|
+
retry = Retry(child, name="Retry", max_attempts=3)
|
|
24
|
+
|
|
25
|
+
child = py_trees.behaviours.Failure(name="Flaky")
|
|
26
|
+
# Try up to 3 times with 1 second between each attempt.
|
|
27
|
+
retry = Retry(child, name="RetryWithDelay", max_attempts=3, delay=1.0)
|
|
28
|
+
'''
|
|
29
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
30
|
+
name: str,
|
|
31
|
+
max_attempts: int,
|
|
32
|
+
delay: float = 0.0):
|
|
33
|
+
if max_attempts < 1:
|
|
34
|
+
raise ValueError(f'max_attempts({max_attempts}) must be greater than 0.')
|
|
35
|
+
if delay < 0.0:
|
|
36
|
+
raise ValueError(f'delay({delay}) must be non-negative.')
|
|
37
|
+
super(Retry, self).__init__(name=name, child=child)
|
|
38
|
+
self._max_attempts = max_attempts
|
|
39
|
+
self._delay = delay
|
|
40
|
+
self._attempts = 0
|
|
41
|
+
self._waiting = False
|
|
42
|
+
self._wait_start = None
|
|
43
|
+
|
|
44
|
+
def initialise(self) -> None:
|
|
45
|
+
self._attempts = 0
|
|
46
|
+
self._waiting = False
|
|
47
|
+
self._wait_start = None
|
|
48
|
+
|
|
49
|
+
def tick(self):
|
|
50
|
+
if self._waiting:
|
|
51
|
+
elapsed = time.time() - self._wait_start
|
|
52
|
+
if elapsed < self._delay:
|
|
53
|
+
self.status = py_trees.common.Status.RUNNING
|
|
54
|
+
yield self
|
|
55
|
+
return
|
|
56
|
+
else:
|
|
57
|
+
self._waiting = False
|
|
58
|
+
self.decorated.stop(py_trees.common.Status.INVALID)
|
|
59
|
+
for node in super().tick():
|
|
60
|
+
yield node
|
|
61
|
+
|
|
62
|
+
def update(self) -> py_trees.common.Status:
|
|
63
|
+
if self.decorated.status == py_trees.common.Status.SUCCESS:
|
|
64
|
+
return py_trees.common.Status.SUCCESS
|
|
65
|
+
elif self.decorated.status == py_trees.common.Status.RUNNING:
|
|
66
|
+
return py_trees.common.Status.RUNNING
|
|
67
|
+
else: # FAILURE
|
|
68
|
+
self._attempts += 1
|
|
69
|
+
if self._attempts < self._max_attempts:
|
|
70
|
+
if self._delay > 0.0:
|
|
71
|
+
self._waiting = True
|
|
72
|
+
self._wait_start = time.time()
|
|
73
|
+
else:
|
|
74
|
+
self.decorated.stop(py_trees.common.Status.INVALID)
|
|
75
|
+
return py_trees.common.Status.RUNNING
|
|
76
|
+
else:
|
|
77
|
+
return py_trees.common.Status.FAILURE
|
py_branches/timeout.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import time
|
|
3
|
+
import py_trees
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Timeout(py_trees.decorators.Decorator):
|
|
7
|
+
'''
|
|
8
|
+
Fails a child behavior if it stays RUNNING beyond the specified duration.
|
|
9
|
+
|
|
10
|
+
- If the child returns SUCCESS or FAILURE before the timeout, that
|
|
11
|
+
status is passed through unchanged.
|
|
12
|
+
- If the child is still RUNNING when the timeout expires, the child is
|
|
13
|
+
stopped and FAILURE is returned.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
child (Behaviour): The child behavior to wrap with a timeout.
|
|
17
|
+
name (str): Name of this decorator.
|
|
18
|
+
duration (float): Maximum seconds the child may remain RUNNING.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
child = LongRunningBehavior(name="Slow")
|
|
22
|
+
# Fail if child does not complete within 5 seconds.
|
|
23
|
+
guarded = Timeout(child, name="Timeout", duration=5.0)
|
|
24
|
+
'''
|
|
25
|
+
def __init__(self, child: py_trees.behaviour.Behaviour,
|
|
26
|
+
name: str,
|
|
27
|
+
duration: float):
|
|
28
|
+
if duration <= 0.0:
|
|
29
|
+
raise ValueError(f'duration({duration}) must be positive.')
|
|
30
|
+
super(Timeout, self).__init__(name=name, child=child)
|
|
31
|
+
self._duration = duration
|
|
32
|
+
self._start_time = None
|
|
33
|
+
|
|
34
|
+
def initialise(self) -> None:
|
|
35
|
+
self._start_time = time.time()
|
|
36
|
+
|
|
37
|
+
def update(self) -> py_trees.common.Status:
|
|
38
|
+
if self.decorated.status != py_trees.common.Status.RUNNING:
|
|
39
|
+
return self.decorated.status
|
|
40
|
+
|
|
41
|
+
elapsed = time.time() - self._start_time
|
|
42
|
+
if elapsed >= self._duration:
|
|
43
|
+
self.decorated.stop(py_trees.common.Status.INVALID)
|
|
44
|
+
return py_trees.common.Status.FAILURE
|
|
45
|
+
return py_trees.common.Status.RUNNING
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Software License Agreement (BSD License)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 Shunong Wu
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted provided that the following conditions
|
|
8
|
+
# are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright
|
|
11
|
+
# notice, this list of conditions and the following disclaimer.
|
|
12
|
+
# * Redistributions in binary form must reproduce the above
|
|
13
|
+
# copyright notice, this list of conditions and the following
|
|
14
|
+
# disclaimer in the documentation and/or other materials provided
|
|
15
|
+
# with the distribution.
|
|
16
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived
|
|
18
|
+
# from this software without specific prior written permission.
|
|
19
|
+
#
|
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
21
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
22
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
23
|
+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
24
|
+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
25
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
26
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
27
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
29
|
+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
30
|
+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
31
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: py_branches
|
|
3
|
+
Version: 1.2.1
|
|
4
|
+
Summary: Useful behaviors built on top of the py_trees library.
|
|
5
|
+
Home-page: https://github.com/snwu1996/py_branches
|
|
6
|
+
License: BSD
|
|
7
|
+
Author: Shunong Wu
|
|
8
|
+
Author-email: shunongwu@gmail.com
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Dist: py-trees
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Project-URL: Repository, https://github.com/snwu1996/py_branches
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# py_branches
|
|
29
|
+
|
|
30
|
+
`py_branches` provides higher-level functionality designed to sit on top of the [py_trees](https://py-trees.readthedocs.io/) library. It extends py_trees with reusable behaviors and decorators for common patterns such as alternating execution, probabilistic selection, blackboard-driven conditionals, and time-based pausing.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
**From PyPI:**
|
|
35
|
+
```bash
|
|
36
|
+
pip install py_branches
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**From source (editable):**
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/snwu1996/py_branches.git
|
|
42
|
+
cd py_branches
|
|
43
|
+
pip install -e .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Modules
|
|
47
|
+
|
|
48
|
+
| Module | Description |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `alternating` | Cycle through behaviors in fixed patterns or run a child every N ticks |
|
|
51
|
+
| `blackboard` | Read/write/gate behaviors based on py_trees blackboard variables |
|
|
52
|
+
| `pause` | Time-based pauses — uniform random duration or YAML-defined schedules |
|
|
53
|
+
| `random` | Probabilistic behavior execution and weighted random selectors |
|
|
54
|
+
|
|
55
|
+
## Basic Usage
|
|
56
|
+
|
|
57
|
+
### Alternating — cycle through behaviors
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import py_trees
|
|
61
|
+
from py_branches.alternating import run_alternating
|
|
62
|
+
|
|
63
|
+
a = py_trees.behaviours.Success(name="A")
|
|
64
|
+
b = py_trees.behaviours.Success(name="B")
|
|
65
|
+
c = py_trees.behaviours.Success(name="C")
|
|
66
|
+
|
|
67
|
+
# Run A for 3 ticks, then B for 2 ticks, then C for 4 ticks, then repeat
|
|
68
|
+
root = run_alternating("Alternating", [a, b, c], [3, 2, 4])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Alternating — run a child every N ticks
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from py_branches.alternating import RunEveryX, RunEveryRange
|
|
75
|
+
|
|
76
|
+
child = py_trees.behaviours.Success(name="Child")
|
|
77
|
+
|
|
78
|
+
# Run child every 5th tick
|
|
79
|
+
every_5 = RunEveryX(child, name="Every5", every_x_range=(5, 5))
|
|
80
|
+
|
|
81
|
+
# Run child only on iterations 4–6 out of every 10
|
|
82
|
+
windowed = RunEveryRange(child, name="Window", max_range=10, run_range=(4, 6))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Blackboard — conditional execution and variable management
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import py_trees
|
|
89
|
+
from py_branches.blackboard import (
|
|
90
|
+
IncrementBlackboardVariable,
|
|
91
|
+
RunIfBlackboardVariableEquals,
|
|
92
|
+
SetBlackboardVariableIfCondition,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Set up blackboard
|
|
96
|
+
py_trees.blackboard.Blackboard.enable_activity_stream()
|
|
97
|
+
client = py_trees.blackboard.Client(name="setup")
|
|
98
|
+
client.register_key("counter", access=py_trees.common.Access.WRITE)
|
|
99
|
+
client.counter = 0
|
|
100
|
+
|
|
101
|
+
# Increment a blackboard counter each tick
|
|
102
|
+
increment = IncrementBlackboardVariable(
|
|
103
|
+
name="Increment", variable_name="counter", increment_by=1
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Only run a child behavior when counter == 5
|
|
107
|
+
child = py_trees.behaviours.Success(name="AtFive")
|
|
108
|
+
gate = RunIfBlackboardVariableEquals(
|
|
109
|
+
child, name="RunAt5", variable_name="counter", equals=5
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Pause — random duration pause
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from py_branches.pause import PauseUniform
|
|
117
|
+
|
|
118
|
+
# Pause for a random duration between 1.0 and 3.0 seconds
|
|
119
|
+
pause = PauseUniform(name="RandomPause", low=1.0, high=3.0)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Pause — schedule-based pause
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from py_branches.pause import load_schedule_file, CheckPauseSchedule, PauseSchedule
|
|
126
|
+
|
|
127
|
+
schedule = load_schedule_file("configs/schedules/example_schedule.yaml")
|
|
128
|
+
|
|
129
|
+
# Returns SUCCESS when the current time falls inside a scheduled window
|
|
130
|
+
check = CheckPauseSchedule(name="CheckSchedule", schedule=schedule)
|
|
131
|
+
|
|
132
|
+
# Pauses until the current scheduled window ends
|
|
133
|
+
pause = PauseSchedule(name="PauseSchedule", schedule=schedule)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Random — probabilistic execution
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import py_trees
|
|
140
|
+
from py_branches.random import RandomRun, random_selector
|
|
141
|
+
|
|
142
|
+
child = py_trees.behaviours.Success(name="Child")
|
|
143
|
+
|
|
144
|
+
# Execute child with 70% probability; return FAILURE otherwise
|
|
145
|
+
maybe = RandomRun(child, name="Maybe", probability=0.7)
|
|
146
|
+
|
|
147
|
+
# Weighted random selector: a=20%, b=30%, c=50%
|
|
148
|
+
a = py_trees.behaviours.Success(name="A")
|
|
149
|
+
b = py_trees.behaviours.Success(name="B")
|
|
150
|
+
c = py_trees.behaviours.Success(name="C")
|
|
151
|
+
selector = random_selector("WeightedSel", [a, b, c], [0.2, 0.3, 0.5])
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Running Tests
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pytest tests/
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
161
|
+
|
|
162
|
+
Detailed documentation for each module is in the [`docs/`](docs/) folder:
|
|
163
|
+
|
|
164
|
+
- [alternating.md](docs/alternating.md) — Alternating and periodic execution
|
|
165
|
+
- [blackboard.md](docs/blackboard.md) — Blackboard-driven behaviors
|
|
166
|
+
- [pause.md](docs/pause.md) — Time-based pausing and schedules
|
|
167
|
+
- [random.md](docs/random.md) — Probabilistic execution
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
BSD License. See [LICENSE](LICENSE) for details.
|
|
172
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
py_branches/__init__.py,sha256=f9w3Ws8Mp0lFK2bzr7EqGpxAfLLTQZ11htZSXUm7C5s,199
|
|
2
|
+
py_branches/alternating.py,sha256=E9myfYWO3ObYQHEvb9IP7JwhUvlaPCQnPcAT3wMQVtQ,8666
|
|
3
|
+
py_branches/blackboard.py,sha256=Kc_7EQmmz4VPpPGdJ-KhlIGY_Z2z3iLmrjZKSXHJqLY,5032
|
|
4
|
+
py_branches/cooldown.py,sha256=gpN_UAfYXd9h4K6IM_zkWqWzlyy5KqYweCxUuvFuiX8,2423
|
|
5
|
+
py_branches/counter.py,sha256=zI0umT3ZS8Qd68e9SetrXwhPCeQTFlJQSWgadOp_GHo,2934
|
|
6
|
+
py_branches/latch.py,sha256=mKpBucyzO7YI-h5tsLXG6dsaozBKuRjWgkR4vLRaqFw,1754
|
|
7
|
+
py_branches/pause.py,sha256=a3FLDw6spmDHFpFny19QMJqmzjrX150-EqBT6htw9SQ,5801
|
|
8
|
+
py_branches/random.py,sha256=CivVLpzfYGMPWT04SdQz8p9um8OAXz6KRk7hLAU6uJE,4874
|
|
9
|
+
py_branches/retry.py,sha256=PRUlIAKHSwItuTL4fZM8UEaFCzDV_DGWJvIggwwe1JY,2923
|
|
10
|
+
py_branches/timeout.py,sha256=O1ih80zPbN4yMEzftzrQ2JaPCMuyShnhUUv1icKVMXY,1647
|
|
11
|
+
py_branches-1.2.1.dist-info/LICENSE,sha256=6_acK5mJZz0h1RKJeUWRAgI-X-z8Swz3d3TjFwu4SQw,1624
|
|
12
|
+
py_branches-1.2.1.dist-info/METADATA,sha256=qVG5wEB4javTVwnC8VUbQTvRwzqrNZepbmW9jPiaJIM,5286
|
|
13
|
+
py_branches-1.2.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
14
|
+
py_branches-1.2.1.dist-info/RECORD,,
|