-
Notifications
You must be signed in to change notification settings - Fork 5
Unified cleanup using scoped behaviors #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
198dbc0
Moved in decorator and idiom from py_trees#427
amalnanavati 353479e
Make OnPreempt decorator multi-tick, add on_preempt function to event…
amalnanavati 222222d
Integrate the code into this library
amalnanavati 4ac47f4
Implemented scoped behavior idiom
amalnanavati aff6e6b
Added scoped behavior to tree for concise cleanup
amalnanavati 5b40318
Added unit tests for `eventually_swiss`
amalnanavati db5019d
Updated eventually_swiss and verified that it passes tests
amalnanavati ecfa461
MoveTo bug fix
amalnanavati 73a320d
Updated test because we don't care about relative termination order b…
amalnanavati 2b23353
Update tests so that after worker/on_success/on_failure terminate, on…
amalnanavati 10233a6
Simplified eventually_swiss implementation
amalnanavati 8dbdeb4
Generalized `eventually_swiss` return status, started unit tests for …
amalnanavati fd553df
Completed scoped_behavior tests, simplified unit test generation
amalnanavati 74ef8da
Removed option to return on_failure status from eventually_swiss
amalnanavati 2e8abe5
Updated scoped_behavior to only use one post behavior
amalnanavati 9c4d229
Updated tests to preempt before the tree has started
amalnanavati 6fe55e2
Added nested test cases to scoped_behavior
amalnanavati File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
""" | ||
This module defines the ForceStatus decorator, which ignores the child's | ||
current status and always returns the status specified by the user. | ||
""" | ||
|
||
# Standard imports | ||
import typing | ||
|
||
# Third-party imports | ||
from py_trees import behaviour, common | ||
from py_trees.decorators import Decorator | ||
|
||
# Local imports | ||
|
||
|
||
class ForceStatus(Decorator): | ||
""" | ||
This decorator ignores the child's current status and always returns | ||
the status specified by the user. This can be useful e.g., to force a | ||
composite without memory to move on to the next child while still ticking | ||
the child of this decorator. | ||
While this decorator's behavior can be achieved with a chain of "X is Y" | ||
style decorators, it is often conceptually easier to reason about a decorator | ||
always returning one status, as opposed to a chain of decorators that each | ||
flip their child's status. | ||
""" | ||
|
||
def __init__(self, name: str, child: behaviour.Behaviour, status: common.Status): | ||
""" | ||
Initialise the decorator. | ||
Args: | ||
name: the decorator name | ||
child: the child to be decorated | ||
status: the status to return on :meth:`update` | ||
""" | ||
super().__init__(name=name, child=child) | ||
self.const_status = status | ||
|
||
def tick(self) -> typing.Iterator[behaviour.Behaviour]: | ||
""" | ||
Don't stop the child even if this decorator's status is non-RUNNING. | ||
Yields: | ||
a reference to itself | ||
""" | ||
self.logger.debug(f"{self.__class__.__name__}.tick()") | ||
# initialise just like other behaviours/composites | ||
if self.status != common.Status.RUNNING: | ||
self.initialise() | ||
# interrupt proceedings and process the child node | ||
# (including any children it may have as well) | ||
for node in self.decorated.tick(): | ||
yield node | ||
# resume normal proceedings for a Behaviour's tick | ||
self.status = self.update() | ||
amalnanavati marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
# do not stop even if this decorator's status is non-RUNNING | ||
yield self | ||
|
||
def update(self) -> common.Status: | ||
""" | ||
Return the status specified when creating this decorator. | ||
Returns: | ||
the behaviour's new status :class:`~py_trees.common.Status` | ||
""" | ||
return self.const_status |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
""" | ||
NOTE: This is a multi-tick version of the decorator discussed in | ||
https://github.com/splintered-reality/py_trees/pull/427 . Once a | ||
multi-tick version of that decorator is merged into py_trees, this | ||
decorator should be removed in favor of the main py_trees one. | ||
""" | ||
|
||
import time | ||
import typing | ||
|
||
from py_trees import behaviour, common | ||
from py_trees.decorators import Decorator | ||
|
||
|
||
class OnPreempt(Decorator): | ||
amalnanavati marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Trigger the child on preemption, e.g., when :meth:`terminate` is | ||
called with status :data:`~py_trees.common.Status.INVALID`. The | ||
child can either be triggered: (a) for a single tick; or (b) until it | ||
reaches a status other than :data:`~py_trees.common.Status.RUNNING` or | ||
times out. | ||
Always return `update_status` and on :meth:`terminate` (with new_status | ||
:data:`~py_trees.common.Status.INVALID`), call the child's | ||
:meth:`~py_trees.behaviour.Behaviour.update` method. | ||
This is useful to cleanup, restore a context switch or to | ||
implement a finally-like behaviour. | ||
.. seealso:: :meth:`py_trees.idioms.eventually`, :meth:`py_trees.idioms.eventually_swiss` | ||
""" | ||
|
||
# pylint: disable=too-many-arguments | ||
# This is acceptable, to give users maximum control over how this decorator | ||
# behaves. | ||
def __init__( | ||
self, | ||
name: str, | ||
child: behaviour.Behaviour, | ||
update_status: common.Status = common.Status.RUNNING, | ||
single_tick: bool = True, | ||
period_ms: int = 0, | ||
timeout: typing.Optional[float] = None, | ||
): | ||
""" | ||
Initialise with the standard decorator arguments. | ||
Args: | ||
name: the decorator name | ||
child: the child to be decorated | ||
update_status: the status to return on :meth:`update` | ||
single_tick: if True, tick the child once on preemption. Else, | ||
tick the child until it reaches a status other than | ||
:data:`~py_trees.common.Status.RUNNING`. | ||
period_ms: how long to sleep between ticks (in milliseconds) | ||
if `single_tick` is False. If 0, then do not sleep. | ||
timeout: how long (sec) to wait for the child to reach a status | ||
other than :data:`~py_trees.common.Status.RUNNING` if | ||
`single_tick` is False. If None, then do not timeout. | ||
""" | ||
super().__init__(name=name, child=child) | ||
self.update_status = update_status | ||
self.single_tick = single_tick | ||
self.period_ms = period_ms | ||
self.timeout = timeout | ||
|
||
def tick(self) -> typing.Iterator[behaviour.Behaviour]: | ||
""" | ||
Bypass the child when ticking. | ||
Yields: | ||
a reference to itself | ||
""" | ||
self.logger.debug(f"{self.__class__.__name__}.tick()") | ||
self.status = self.update() | ||
yield self | ||
|
||
def update(self) -> common.Status: | ||
""" | ||
Return the constant status specified in the constructor. | ||
Returns: | ||
the behaviour's new status :class:`~py_trees.common.Status` | ||
""" | ||
return self.update_status | ||
|
||
def terminate(self, new_status: common.Status) -> None: | ||
"""Tick the child behaviour once.""" | ||
self.logger.debug( | ||
f"{self.__class__.__name__}.terminate({self.status}->{new_status})" | ||
if self.status != new_status | ||
else f"{new_status}", | ||
) | ||
if new_status == common.Status.INVALID: | ||
terminate_start_s = time.time() | ||
# Tick the child once | ||
self.decorated.tick_once() | ||
# If specified, tick until the child reaches a non-RUNNING status | ||
if not self.single_tick: | ||
while self.decorated.status == common.Status.RUNNING and ( | ||
self.timeout is None | ||
or time.time() - terminate_start_s < self.timeout | ||
): | ||
if self.period_ms > 0: | ||
time.sleep(self.period_ms / 1000.0) | ||
self.decorated.tick_once() | ||
# Do not need to stop the child here - this method | ||
# is only called by Decorator.stop() which will handle | ||
# that responsibility immediately after this method returns. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
""" | ||
NOTE: This is a preempt-handling version of the idiom discussed in | ||
https://github.com/splintered-reality/py_trees/pull/427 . Once a | ||
preempt-handling version of that idiom is merged into py_trees, this | ||
idiom should be removed in favor of the main py_trees one. | ||
""" | ||
|
||
import typing | ||
|
||
from py_trees import behaviour, behaviours, common, composites | ||
from py_trees.decorators import Inverter, StatusToBlackboard, SuccessIsFailure | ||
|
||
from ada_feeding.decorators import ForceStatus, OnPreempt | ||
|
||
|
||
def eventually_swiss( | ||
name: str, | ||
workers: typing.List[behaviour.Behaviour], | ||
on_failure: behaviour.Behaviour, | ||
on_success: behaviour.Behaviour, | ||
on_preempt: behaviour.Behaviour, | ||
on_preempt_single_tick: bool = True, | ||
on_preempt_period_ms: int = 0, | ||
on_preempt_timeout: typing.Optional[float] = None, | ||
status_blackboard_key: typing.Optional[str] = None, | ||
) -> behaviour.Behaviour: | ||
""" | ||
Implement a multi-tick, general purpose 'try-except-else'-like pattern. | ||
|
||
This is a swiss knife version of the eventually idiom | ||
that facilitates a multi-tick response for specialised | ||
handling work sequence's completion status. Specifically, this idiom | ||
guarentees the following: | ||
1. The on_success behaviour is ticked only if the workers all return SUCCESS. | ||
2. The on_failure behaviour is ticked only if at least one worker returns FAILURE. | ||
3. The on_preempt behaviour is ticked only if `stop(INVALID)` is called on the | ||
root behavior returned from this idiom. | ||
|
||
.. graphviz:: dot/eventually-swiss.dot | ||
|
||
Args: | ||
name: the name to use for the idiom root | ||
workers: the worker behaviours or subtrees | ||
on_success: the behaviour or subtree to tick on work success | ||
on_failure: the behaviour or subtree to tick on work failure | ||
on_preempt: the behaviour or subtree to tick on work preemption | ||
on_preempt_single_tick: if True, tick the on_preempt behaviour once | ||
on preemption. Else, tick the on_preempt behaviour until it | ||
reaches a status other than :data:`~py_trees.common.Status.RUNNING`. | ||
on_preempt_period_ms: how long to sleep between ticks (in milliseconds) | ||
if `on_preempt_single_tick` is False. If 0, then do not sleep. | ||
on_preempt_timeout: how long (sec) to wait for the on_preempt behaviour | ||
to reach a status other than :data:`~py_trees.common.Status.RUNNING` | ||
if `on_preempt_single_tick` is False. If None, then do not timeout. | ||
status_blackboard_key: the key to use for the status blackboard variable. | ||
If None, use "/{name}/eventually_swiss_status". | ||
|
||
Returns: | ||
:class:`~py_trees.behaviour.Behaviour`: the root of the oneshot subtree | ||
|
||
.. seealso:: :meth:`py_trees.idioms.eventually`, :ref:`py-trees-demo-eventually-swiss-program` | ||
""" | ||
# pylint: disable=too-many-arguments, too-many-locals | ||
# This is acceptable, to give users maximum control over how the swiss knife | ||
# idiom behaves. | ||
|
||
# Create the subtree to handle `on_success` | ||
if status_blackboard_key is None: | ||
status_blackboard_key = f"/{name}/eventually_swiss_status" | ||
unset_status = behaviours.UnsetBlackboardVariable( | ||
name="Unset Status", key=status_blackboard_key | ||
) | ||
save_success_status = StatusToBlackboard( | ||
name="Save Success Status", | ||
child=on_success, | ||
variable_name=status_blackboard_key, | ||
) | ||
on_success_sequence = composites.Sequence( | ||
name="Work to Success", | ||
memory=True, | ||
children=[unset_status] + workers + [save_success_status], | ||
) | ||
|
||
# Create the subtree to handle `on_failure`. This subtree must start with | ||
# `return_status_if_set` so that `on_failure` is not ticked if `on_success` | ||
# fails. | ||
check_status_exists = behaviours.CheckBlackboardVariableExists( | ||
name="Wait for Status", | ||
variable_name=status_blackboard_key, | ||
) | ||
# Note that we can get away with using an Inverter here because the only way | ||
# we get to this branch is if either the `workers` or `on_success` fails. | ||
# So the status either doesn't exist or is FAILURE. | ||
return_status_if_set = Inverter( | ||
name="Return Status if Set", | ||
child=check_status_exists, | ||
) | ||
on_failure_always_fail = SuccessIsFailure( | ||
name="On Failure Always Failure", | ||
child=on_failure, | ||
) | ||
save_failure_status = StatusToBlackboard( | ||
name="Save Failure Status", | ||
child=on_failure_always_fail, | ||
variable_name=status_blackboard_key, | ||
) | ||
on_failure_sequence = composites.Sequence( | ||
name="On Failure", | ||
memory=True, | ||
children=[return_status_if_set, save_failure_status], | ||
) | ||
|
||
# Create the combined subtree to handle `on_success` and `on_failure` | ||
combined = composites.Selector( | ||
name="On Non-Preemption Subtree", | ||
memory=True, | ||
children=[on_success_sequence, on_failure_sequence], | ||
) | ||
# We force the outcome of this tree to FAILURE so that the Selector always | ||
# goes on to tick the `on_preempt_subtree`, which is necessary to ensure | ||
# that the `on_preempt` behavior will get run if the tree is preempted. | ||
on_success_or_failure_subtree = ForceStatus( | ||
name="On Non-Preemption", | ||
child=combined, | ||
status=common.Status.FAILURE, | ||
) | ||
|
||
# Create the subtree to handle `on_preempt` | ||
on_preempt_subtree = OnPreempt( | ||
name="On Preemption", | ||
child=on_preempt, | ||
# Returning FAILURE is necessary so (a) the Selector always moves on to | ||
# `set_status_subtree`; and (b) the decorator's status is not INVALID (which would | ||
# prevent the Selector from passing on a `stop(INVALID)` call to it). | ||
update_status=common.Status.FAILURE, | ||
single_tick=on_preempt_single_tick, | ||
period_ms=on_preempt_period_ms, | ||
timeout=on_preempt_timeout, | ||
) | ||
|
||
# Create the subtree to output the status once `on_success_or_failure_subtree` | ||
# is done. | ||
wait_for_status = behaviours.WaitForBlackboardVariable( | ||
name="Wait for Status", | ||
variable_name=status_blackboard_key, | ||
) | ||
blackboard_to_status = behaviours.BlackboardToStatus( | ||
name="Blackboard to Status", | ||
variable_name=status_blackboard_key, | ||
) | ||
set_status_subtree = composites.Sequence( | ||
name="Set Status", | ||
memory=True, | ||
children=[wait_for_status, blackboard_to_status], | ||
) | ||
|
||
root = composites.Selector( | ||
name=name, | ||
memory=False, | ||
children=[ | ||
on_success_or_failure_subtree, | ||
on_preempt_subtree, | ||
set_status_subtree, | ||
], | ||
) | ||
return root |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.