From dbe1c56b55ebb397e0643789485349fcdf907676 Mon Sep 17 00:00:00 2001 From: tangkong Date: Sun, 22 Sep 2024 12:08:44 -0700 Subject: [PATCH 1/9] ENH: Add py_trees.behaviours Behaviours as beams.tree_config BaseItems, test more thoroughly --- beams/tests/artifacts/eggs.json | 20 +++-- beams/tests/artifacts/eggs2.json | 34 ++++----- beams/tests/test_tree_config.py | 64 ++++++++++++++++ beams/tree_config.py | 124 +++++++++++++++++++++++++++---- 4 files changed, 196 insertions(+), 46 deletions(-) create mode 100644 beams/tests/test_tree_config.py diff --git a/beams/tests/artifacts/eggs.json b/beams/tests/artifacts/eggs.json index 03d0c2c..6395aa2 100644 --- a/beams/tests/artifacts/eggs.json +++ b/beams/tests/artifacts/eggs.json @@ -11,19 +11,17 @@ "operator": "ge" }, "do": { - "IncPVActionItem": { - "name": "self_test_do", + "name": "self_test_do", + "description": "", + "pv": "PERC:COMP", + "increment": 10, + "loop_period_sec": 0.01, + "termination_check": { + "name": "", "description": "", - "loop_period_sec": 0.01, "pv": "PERC:COMP", - "increment": 10, - "termination_check": { - "name": "", - "description": "", - "pv": "PERC:COMP", - "value": 100, - "operator": "ge" - } + "value": 100, + "operator": "ge" } } } diff --git a/beams/tests/artifacts/eggs2.json b/beams/tests/artifacts/eggs2.json index 2bcba87..cb1f0aa 100644 --- a/beams/tests/artifacts/eggs2.json +++ b/beams/tests/artifacts/eggs2.json @@ -17,19 +17,17 @@ "operator": "ge" }, "do": { - "SetPVActionItem": { - "name": "ret_find_do", + "name": "ret_find_do", + "description": "", + "pv": "RET:FOUND", + "value": 1, + "loop_period_sec": 0.01, + "termination_check": { + "name": "", "description": "", - "loop_period_sec": 0.01, "pv": "RET:FOUND", "value": 1, - "termination_check": { - "name": "", - "description": "", - "pv": "RET:FOUND", - "value": 1, - "operator": "ge" - } + "operator": "ge" } } } @@ -46,19 +44,17 @@ "operator": "ge" }, "do": { - "SetPVActionItem": { + "name": "", + "description": "", + "pv": "RET:INSERT", + "value": 1, + "loop_period_sec": 1.0, + "termination_check": { "name": "", "description": "", - "loop_period_sec": 1.0, "pv": "RET:INSERT", "value": 1, - "termination_check": { - "name": "", - "description": "", - "pv": "RET:INSERT", - "value": 1, - "operator": "ge" - } + "operator": "ge" } } } diff --git a/beams/tests/test_tree_config.py b/beams/tests/test_tree_config.py new file mode 100644 index 0000000..8e1a63d --- /dev/null +++ b/beams/tests/test_tree_config.py @@ -0,0 +1,64 @@ +import apischema +import pytest +from py_trees.behaviour import Behaviour +from py_trees.behaviours import (BlackboardToStatus, + CheckBlackboardVariableExists, + CheckBlackboardVariableValue, Periodic, + SetBlackboardVariable, StatusQueue, + SuccessEveryN, TickCounter, + UnsetBlackboardVariable, + WaitForBlackboardVariable, + WaitForBlackboardVariableValue) +from py_trees.composites import Parallel, Selector, Sequence + +from beams.behavior_tree.ActionNode import ActionNode +from beams.behavior_tree.CheckAndDo import CheckAndDo +from beams.behavior_tree.ConditionNode import ConditionNode +from beams.tree_config import (BaseItem, BlackboardToStatusItem, + CheckAndDoItem, + CheckBlackboardVariableExistsItem, + CheckBlackboardVariableValueItem, ConditionItem, + IncPVActionItem, ParallelItem, PeriodicItem, + SelectorItem, SequenceItem, + SetBlackboardVariableItem, SetPVActionItem, + StatusQueueItem, SuccessEveryNItem, + TickCounterItem, UnsetBlackboardVariableItem, + WaitForBlackboardVariableItem, + WaitForBlackboardVariableValueItem) + +ITEM_TO_BEHAVIOUR = [ + (ParallelItem, Parallel), + (SelectorItem, Selector), + (SequenceItem, Sequence), + (ConditionItem, ConditionNode), + (SetPVActionItem, ActionNode), + (IncPVActionItem, ActionNode), + (CheckAndDoItem, CheckAndDo), + (PeriodicItem, Periodic), + (StatusQueueItem, StatusQueue), + (SuccessEveryNItem, SuccessEveryN), + (TickCounterItem, TickCounter), + (BlackboardToStatusItem, BlackboardToStatus), + (CheckBlackboardVariableExistsItem, CheckBlackboardVariableExists), + (WaitForBlackboardVariableItem, WaitForBlackboardVariable), + (UnsetBlackboardVariableItem, UnsetBlackboardVariable), + (SetBlackboardVariableItem, SetBlackboardVariable), + (CheckBlackboardVariableValueItem, CheckBlackboardVariableValue), + (WaitForBlackboardVariableValueItem, WaitForBlackboardVariableValue), +] + + +@pytest.mark.parametrize('item, node_type', ITEM_TO_BEHAVIOUR) +def test_get_tree(item: BaseItem, node_type: Behaviour): + item_instance = item() + assert isinstance(item_instance.get_tree(), node_type) + + +@pytest.mark.parametrize('item_class', [item[0] for item in ITEM_TO_BEHAVIOUR]) +def test_item_serialize_roundtrip(item_class: BaseItem): + item = item_class() + ser = apischema.serialize(item_class, item) + + deser = apischema.deserialize(item_class, ser) + + assert item == deser diff --git a/beams/tree_config.py b/beams/tree_config.py index 819b54a..2fda590 100644 --- a/beams/tree_config.py +++ b/beams/tree_config.py @@ -4,16 +4,18 @@ import logging import operator import time -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from enum import Enum from pathlib import Path -from typing import Any, Callable, List, Optional +from typing import Any, Callable, List, Optional, Union import py_trees from apischema import deserialize from epics import caget, caput from py_trees.behaviour import Behaviour -from py_trees.common import ParallelPolicy +from py_trees.behaviours import (CheckBlackboardVariableValue, + WaitForBlackboardVariableValue) +from py_trees.common import ComparisonExpression, ParallelPolicy, Status from py_trees.composites import Parallel, Selector, Sequence from beams.behavior_tree.ActionNode import ActionNode @@ -74,7 +76,7 @@ class ParallelMode(Enum): @dataclass class ParallelItem(BaseItem): policy: ParallelMode = ParallelMode.Base - children: Optional[List[BaseItem]] = field(default_factory=list) + children: List[BaseItem] = field(default_factory=list) def get_tree(self) -> Parallel: children = [] @@ -93,9 +95,8 @@ def get_tree(self) -> Parallel: @dataclass class SelectorItem(BaseItem): """aka fallback node""" - memory: bool = False - children: Optional[List[BaseItem]] = field(default_factory=list) + children: List[BaseItem] = field(default_factory=list) def get_tree(self) -> Selector: children = [] @@ -109,7 +110,7 @@ def get_tree(self) -> Selector: @dataclass class SequenceItem(BaseItem): memory: bool = False - children: Optional[List[BaseItem]] = field(default_factory=list) + children: List[BaseItem] = field(default_factory=list) def get_tree(self) -> Sequence: children = [] @@ -154,16 +155,11 @@ def cond_func(): return cond_func -@as_tagged_union @dataclass -class ActionItem(BaseItem): - loop_period_sec: float = 1.0 - - -@dataclass -class SetPVActionItem(ActionItem): +class SetPVActionItem(BaseItem): pv: str = "" value: Any = 1 + loop_period_sec: float = 1.0 termination_check: ConditionItem = field(default_factory=ConditionItem) @@ -198,9 +194,10 @@ def work_func(comp_condition: Callable[[], bool]): @dataclass -class IncPVActionItem(ActionItem): +class IncPVActionItem(BaseItem): pv: str = "" increment: float = 1 + loop_period_sec: float = 1.0 termination_check: ConditionItem = field(default_factory=ConditionItem) @@ -240,7 +237,7 @@ def work_func(comp_condition: Callable[[], bool]) -> py_trees.common.Status: @dataclass class CheckAndDoItem(BaseItem): check: ConditionItem = field(default_factory=ConditionItem) - do: ActionItem = field(default_factory=ActionItem) + do: Union[SetPVActionItem, IncPVActionItem] = field(default_factory=SetPVActionItem) def get_tree(self) -> CheckAndDo: check_node = self.check.get_tree() @@ -249,3 +246,98 @@ def get_tree(self) -> CheckAndDo: node = CheckAndDo(name=self.name, check=check_node, do=do_node) return node + + +# py_trees.behaviours Behaviour items +class PyTreesItem: + def get_tree(self): + cls = getattr(py_trees.behaviours, type(self).__name__.removesuffix('Item')) + kwargs = {} + for inst_field in fields(self): + if inst_field.name in ('description',): + continue + kwargs[inst_field.name] = getattr(self, inst_field.name) + + return cls(**kwargs) + + +@dataclass +class PeriodicItem(PyTreesItem, BaseItem): + n: int = 1 + + +@dataclass +class StatusQueueItem(PyTreesItem, BaseItem): + queue: list[Status] = field(default_factory=list) + eventually: Optional[Status] = None + + +@dataclass +class SuccessEveryNItem(PyTreesItem, BaseItem): + n: int = 2 + + +@dataclass +class TickCounterItem(PyTreesItem, BaseItem): + duration: int = 5 + completion_status: Status = Status.SUCCESS + + +@dataclass +class BlackboardToStatusItem(PyTreesItem, BaseItem): + variable_name: str = 'default_variable' + + +@dataclass +class CheckBlackboardVariableExistsItem(PyTreesItem, BaseItem): + variable_name: str = 'default_variable' + + +@dataclass +class WaitForBlackboardVariableItem(PyTreesItem, BaseItem): + variable_name: str = 'default_variable' + + +@dataclass +class UnsetBlackboardVariableItem(PyTreesItem, BaseItem): + key: str = 'default_variable' + + +@dataclass +class SetBlackboardVariableItem(PyTreesItem, BaseItem): + variable_name: str = 'default_variable' + variable_value: Any = 1 + overwrite: bool = True + + +@dataclass +class PyTreesComparison: + variable_name: str = '' + value: Any = 1 + operator: ConditionOperator = ConditionOperator.equal + + +@dataclass +class CheckBlackboardVariableValueItem(BaseItem): + check: PyTreesComparison = field(default_factory=PyTreesComparison) + + def get_tree(self): + comp_exp = ComparisonExpression( + variable=self.check.variable_name, + value=self.check.value, + operator=getattr(operator, self.check.operator.value) + ) + return CheckBlackboardVariableValue(name=self.name, check=comp_exp) + + +@dataclass +class WaitForBlackboardVariableValueItem(BaseItem): + check: PyTreesComparison = field(default_factory=PyTreesComparison) + + def get_tree(self): + comp_exp = ComparisonExpression( + variable=self.check.variable_name, + value=self.check.value, + operator=getattr(operator, self.check.operator.value) + ) + return WaitForBlackboardVariableValue(name=self.name, check=comp_exp) From 3817cb3856e07a5e447b348a396d3944102a50a4 Mon Sep 17 00:00:00 2001 From: tangkong Date: Mon, 23 Sep 2024 16:22:32 -0700 Subject: [PATCH 2/9] ENH: add basic status items --- beams/tests/test_tree_config.py | 17 +++++++++++------ beams/tree_config.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/beams/tests/test_tree_config.py b/beams/tests/test_tree_config.py index 8e1a63d..fafb778 100644 --- a/beams/tests/test_tree_config.py +++ b/beams/tests/test_tree_config.py @@ -3,10 +3,10 @@ from py_trees.behaviour import Behaviour from py_trees.behaviours import (BlackboardToStatus, CheckBlackboardVariableExists, - CheckBlackboardVariableValue, Periodic, - SetBlackboardVariable, StatusQueue, - SuccessEveryN, TickCounter, - UnsetBlackboardVariable, + CheckBlackboardVariableValue, Dummy, Failure, + Periodic, Running, SetBlackboardVariable, + StatusQueue, Success, SuccessEveryN, + TickCounter, UnsetBlackboardVariable, WaitForBlackboardVariable, WaitForBlackboardVariableValue) from py_trees.composites import Parallel, Selector, Sequence @@ -18,10 +18,11 @@ CheckAndDoItem, CheckBlackboardVariableExistsItem, CheckBlackboardVariableValueItem, ConditionItem, - IncPVActionItem, ParallelItem, PeriodicItem, + DummyItem, FailureItem, IncPVActionItem, + ParallelItem, PeriodicItem, RunningItem, SelectorItem, SequenceItem, SetBlackboardVariableItem, SetPVActionItem, - StatusQueueItem, SuccessEveryNItem, + StatusQueueItem, SuccessEveryNItem, SuccessItem, TickCounterItem, UnsetBlackboardVariableItem, WaitForBlackboardVariableItem, WaitForBlackboardVariableValueItem) @@ -30,6 +31,10 @@ (ParallelItem, Parallel), (SelectorItem, Selector), (SequenceItem, Sequence), + (SuccessItem, Success), + (FailureItem, Failure), + (RunningItem, Running), + (DummyItem, Dummy), (ConditionItem, ConditionNode), (SetPVActionItem, ActionNode), (IncPVActionItem, ActionNode), diff --git a/beams/tree_config.py b/beams/tree_config.py index 2fda590..b8fd054 100644 --- a/beams/tree_config.py +++ b/beams/tree_config.py @@ -261,6 +261,26 @@ def get_tree(self): return cls(**kwargs) +@dataclass +class SuccessItem(PyTreesItem, BaseItem): + pass + + +@dataclass +class FailureItem(PyTreesItem, BaseItem): + pass + + +@dataclass +class RunningItem(PyTreesItem, BaseItem): + pass + + +@dataclass +class DummyItem(PyTreesItem, BaseItem): + pass + + @dataclass class PeriodicItem(PyTreesItem, BaseItem): n: int = 1 From caa2b95bf02960137836d5113defd8c287dbdfda Mon Sep 17 00:00:00 2001 From: tangkong Date: Mon, 23 Sep 2024 16:23:48 -0700 Subject: [PATCH 3/9] ENH: Build `beams run` command, which runs a tree and outputs information to the console --- beams/bin/main.py | 11 ++++++- beams/bin/run.py | 25 +++++++++++++++ beams/bin/run_main.py | 72 ++++++++++++++++++++++++++++++++++++++----- beams/logging.py | 21 +++++++++++++ 4 files changed, 121 insertions(+), 8 deletions(-) diff --git a/beams/bin/main.py b/beams/bin/main.py index 343afce..21d8d75 100644 --- a/beams/bin/main.py +++ b/beams/bin/main.py @@ -7,7 +7,7 @@ import logging import beams -from beams.logging import setup_logging +from beams.logging import configure_log_directory, setup_logging DESCRIPTION = __doc__ @@ -70,6 +70,12 @@ def main(): type=str, help='Python logging level (e.g. DEBUG, INFO, WARNING)' ) + top_parser.add_argument( + "--log-dir", dest="log_dir", + type=str, + default="", + help="directory to create log files. If not provided, do not log to file." + ) subparsers = top_parser.add_subparsers(help='Possible subcommands') for command_name, (build_func, main) in COMMANDS.items(): @@ -81,6 +87,9 @@ def main(): kwargs = vars(args) log_level = kwargs.pop('log_level') + log_dir = kwargs.pop("log_dir") + if log_dir: + configure_log_directory(log_dir) setup_logging(log_level) logger = logging.getLogger("beams") diff --git a/beams/bin/run.py b/beams/bin/run.py index 666c72a..d79dfc3 100644 --- a/beams/bin/run.py +++ b/beams/bin/run.py @@ -25,6 +25,31 @@ def build_arg_parser(argparser=None): type=str, help="Behavior Tree configuration filepath" ) + argparser.add_argument( + "-i", "--interactive", + action="store_true", default=False, + help="pause and wait for keypress at each tick", + ) + argparser.add_argument( + "-t", "--tick-count", + dest="tick_count", default=1, type=int, + help="How many times to tick the tree. Values <=0 mean continuous ticking" + ) + argparser.add_argument( + "--show-node-status", + action="store_true", dest="show_node_status", default=True, + help="Show node status each time one is ticked" + ) + argparser.add_argument( + "--show-tree", + action="store_true", dest="show_tree", default=True, + help="Show tree statuses after each tree tick" + ) + argparser.add_argument( + "--show-blackboard", + action="store_true", dest="show_blackboard", default=False, + help="Show blackboard status after each tree tick" + ) def main(*args, **kwargs): diff --git a/beams/bin/run_main.py b/beams/bin/run_main.py index bc0751b..bdbdc08 100644 --- a/beams/bin/run_main.py +++ b/beams/bin/run_main.py @@ -3,14 +3,59 @@ """ import logging +import time +from functools import partial from pathlib import Path +from py_trees.console import read_single_keypress +from py_trees.display import unicode_blackboard, unicode_tree +from py_trees.trees import BehaviourTree +from py_trees.visitors import SnapshotVisitor + +from beams.logging import LoggingVisitor from beams.tree_config import get_tree_from_path logger = logging.getLogger(__name__) -def main(filepath: str): +def snapshot_post_tick_handler( + snapshot_visitor: SnapshotVisitor, + show_tree: bool, + show_blackboard: bool, + behaviour_tree: BehaviourTree, +) -> None: + """ + Print data about the part of the tree visited. + Does not log, to allow use of color codes. + ``snapshot_visitor`` keeps track of visited nodes and their status + + Args: + snapshot_handler: gather data about the part of the tree visited + behaviour_tree: tree to gather data from + """ + if show_tree: + print( + "\n" + + unicode_tree( + root=behaviour_tree.root, + visited=snapshot_visitor.visited, + previously_visited=snapshot_visitor.previously_visited, + show_status=True + ) + ) + + if show_blackboard: + print(unicode_blackboard()) + + +def main( + filepath: str, + tick_count: int, + interactive: bool, + show_node_status: bool, + show_tree: bool, + show_blackboard: bool, +): logger.info(f"Running behavior tree at {filepath}") # grab config fp = Path(filepath).resolve() @@ -18,9 +63,22 @@ def main(filepath: str): raise ValueError("Provided filepath is not a file") tree = get_tree_from_path(fp) - print(tree) - # TODO: the rest of whatever we determine the "run" process to be - # run external server? - # setup tree? - # tick? - # settings for ticking? (continuous tick?, as separate process?) + tree.visitors.append(LoggingVisitor(print_status=show_node_status)) + + snapshot_visitor = SnapshotVisitor() + tree.add_post_tick_handler( + partial(snapshot_post_tick_handler, + snapshot_visitor, + show_tree, + show_blackboard) + ) + tree.setup() + for _ in range(tick_count): + try: + tree.tick() + if interactive: + read_single_keypress() + else: + time.sleep(0.5) + except KeyboardInterrupt: + break diff --git a/beams/logging.py b/beams/logging.py index d86e380..85e77e1 100644 --- a/beams/logging.py +++ b/beams/logging.py @@ -14,6 +14,8 @@ from typing import Optional, Union import yaml +from py_trees.behaviour import Behaviour +from py_trees.visitors import VisitorBase LOGGER_QUEUE = mp.Queue(-1) LOGGER_THREAD: Optional[threading.Thread] = None @@ -159,3 +161,22 @@ def setup_logging(level: int = logging.INFO): target=logger_thread, args=(LOGGER_QUEUE,), daemon=True ) LOGGER_THREAD.start() + + +class LoggingVisitor(VisitorBase): + """ + logs feedback messages and behaviour status + + Uses the beams logger rather than the py_trees logger + """ + + def __init__(self, print_status: bool = False): + self.print_status = print_status + super().__init__(full=False) + + def run(self, behaviour: Behaviour) -> None: + out_msg = f"{behaviour.__class__.__name__}.run() [{behaviour.status}]" + if behaviour.feedback_message: + logger.debug(out_msg + f": [{behaviour.feedback_message}]") + else: + logger.debug(out_msg) From 893f6192cfbae66cd7ea94f809ff52cd61850a7c Mon Sep 17 00:00:00 2001 From: tangkong Date: Mon, 23 Sep 2024 16:27:29 -0700 Subject: [PATCH 4/9] TST: add basic cli tests --- beams/tests/artifacts/eternal_guard.json | 56 +++++++++++++++++++++++ beams/tests/conftest.py | 26 +++++++++++ beams/tests/test_bin.py | 58 ++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 beams/tests/artifacts/eternal_guard.json create mode 100644 beams/tests/test_bin.py diff --git a/beams/tests/artifacts/eternal_guard.json b/beams/tests/artifacts/eternal_guard.json new file mode 100644 index 0000000..b2d7101 --- /dev/null +++ b/beams/tests/artifacts/eternal_guard.json @@ -0,0 +1,56 @@ +{ + "root": { + "SequenceItem": { + "name": "Eternal Guard", + "description": "", + "memory": false, + "children": [ + { + "StatusQueueItem": { + "name": "Condition 1", + "description": "", + "queue": [ + "SUCCESS", + "FAILURE", + "SUCCESS" + ], + "eventually": "SUCCESS" + } + }, + { + "StatusQueueItem": { + "name": "Condition 2", + "description": "", + "queue": [ + "SUCCESS", + "SUCCESS", + "FAILURE" + ], + "eventually": "SUCCESS" + } + }, + { + "SequenceItem": { + "name": "Task Sequence", + "description": "", + "memory": true, + "children": [ + { + "SuccessItem": { + "name": "Worker 1", + "description": "" + } + }, + { + "RunningItem": { + "name": "Worker 2", + "description": "" + } + } + ] + } + } + ] + } + } +} diff --git a/beams/tests/conftest.py b/beams/tests/conftest.py index e49d219..9c46ea6 100644 --- a/beams/tests/conftest.py +++ b/beams/tests/conftest.py @@ -1,4 +1,7 @@ import logging +import sys +from contextlib import contextmanager +from copy import copy import py_trees.logging import pytest @@ -16,3 +19,26 @@ def central_logging_setup(caplog): setup_logging(logging.DEBUG) # Set py_trees logging level (not our library) py_trees.logging.level = py_trees.logging.Level.DEBUG + + +@contextmanager +def cli_args(args): + """ + Context manager for running a block of code with a specific set of + command-line arguments. + """ + prev_args = sys.argv + sys.argv = args + yield + sys.argv = prev_args + + +@contextmanager +def restore_logging(): + """ + Context manager for reverting our logging config after testing a function + that configures the logging. + """ + prev_handlers = copy(logging.root.handlers) + yield + logging.root.handlers = prev_handlers diff --git a/beams/tests/test_bin.py b/beams/tests/test_bin.py new file mode 100644 index 0000000..64063cf --- /dev/null +++ b/beams/tests/test_bin.py @@ -0,0 +1,58 @@ +import functools +import itertools +import logging +from pathlib import Path + +import pytest + +from beams.bin.main import main +from beams.tests.conftest import cli_args, restore_logging + +logger = logging.getLogger(__name__) + +SUBCOMMANDS = ["", "run",] + + +def arg_variants(variants: tuple[tuple[tuple[str]]]): + """ + Collapse argument variants into all possible combinations. + """ + for idx, arg_set in enumerate(itertools.product(*variants), 1): + item = functools.reduce( + lambda x, y: x+y, + arg_set, + ) + summary = f"args{idx}_" + ",".join(item) + yield pytest.param(item, id=summary) + + +@pytest.mark.parametrize("subcommand", SUBCOMMANDS) +def test_main_normal(subcommand: str): + args = ["beams", "--help"] + if subcommand: + args.insert(1, subcommand) + with pytest.raises(SystemExit), cli_args(args), restore_logging(): + main() + + +def test_main_noargs(): + with cli_args(["beams"]), restore_logging(): + main() + + +RUN_ARGS = ( + (("-t", "2"), ("--tick-count", "2"), ()), + (("--show-node-status",), ()), + (("--show-tree",), ()), + (("--show-blackboard",), ()), +) + + +@pytest.mark.parametrize("added_args", tuple(arg_variants(RUN_ARGS))) +def test_run(added_args: tuple[str]): + test_cfg = Path(__file__).parent / "artifacts" / "eternal_guard.json" + args = ["beams", "run", str(test_cfg)] + args.extend(added_args) + print(args) + with cli_args(args), restore_logging(): + main() From b30f977fbb0421fa7c35e890f6e2b756c2f271bb Mon Sep 17 00:00:00 2001 From: tangkong Date: Mon, 23 Sep 2024 18:42:39 -0700 Subject: [PATCH 5/9] ENH: add delay time argument for non-interactive run mode --- beams/bin/run.py | 5 +++++ beams/bin/run_main.py | 3 ++- beams/tests/test_bin.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/beams/bin/run.py b/beams/bin/run.py index d79dfc3..248910b 100644 --- a/beams/bin/run.py +++ b/beams/bin/run.py @@ -35,6 +35,11 @@ def build_arg_parser(argparser=None): dest="tick_count", default=1, type=int, help="How many times to tick the tree. Values <=0 mean continuous ticking" ) + argparser.add_argument( + "-d", "--tick-delay", + dest="tick_delay", default=0.5, type=float, + help="Delay time (s) between each tick. Ignored if interactive mode is enabled" + ) argparser.add_argument( "--show-node-status", action="store_true", dest="show_node_status", default=True, diff --git a/beams/bin/run_main.py b/beams/bin/run_main.py index bdbdc08..bdb5b18 100644 --- a/beams/bin/run_main.py +++ b/beams/bin/run_main.py @@ -51,6 +51,7 @@ def snapshot_post_tick_handler( def main( filepath: str, tick_count: int, + tick_delay: float, interactive: bool, show_node_status: bool, show_tree: bool, @@ -79,6 +80,6 @@ def main( if interactive: read_single_keypress() else: - time.sleep(0.5) + time.sleep(tick_delay) except KeyboardInterrupt: break diff --git a/beams/tests/test_bin.py b/beams/tests/test_bin.py index 64063cf..f5b6d99 100644 --- a/beams/tests/test_bin.py +++ b/beams/tests/test_bin.py @@ -51,7 +51,7 @@ def test_main_noargs(): @pytest.mark.parametrize("added_args", tuple(arg_variants(RUN_ARGS))) def test_run(added_args: tuple[str]): test_cfg = Path(__file__).parent / "artifacts" / "eternal_guard.json" - args = ["beams", "run", str(test_cfg)] + args = ["beams", "run", str(test_cfg), "-d", "0"] args.extend(added_args) print(args) with cli_args(args), restore_logging(): From 683ba67791f6c97b501985d0dda79abdabf47b3d Mon Sep 17 00:00:00 2001 From: tangkong Date: Mon, 23 Sep 2024 20:23:40 -0700 Subject: [PATCH 6/9] DOC: pre-release notes --- .../47-enh_pytrees_behaviors.rst | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/source/upcoming_release_notes/47-enh_pytrees_behaviors.rst diff --git a/docs/source/upcoming_release_notes/47-enh_pytrees_behaviors.rst b/docs/source/upcoming_release_notes/47-enh_pytrees_behaviors.rst new file mode 100644 index 0000000..be13a47 --- /dev/null +++ b/docs/source/upcoming_release_notes/47-enh_pytrees_behaviors.rst @@ -0,0 +1,23 @@ +47 enh_pytrees_behaviors +######################## + +API Breaks +---------- +- N/A + +Features +-------- +- Adds new `BaseItem` subclasses for the py_trees built-in Behaviours +- Adds a more fully featured `beams run` subcommand, with interactive-mode and console output options + +Bugfixes +-------- +- N/A + +Maintenance +----------- +- N/A + +Contributors +------------ +- tangkong From 547535cfadd3b44cef477bfed23d100be213663b Mon Sep 17 00:00:00 2001 From: tangkong Date: Tue, 24 Sep 2024 15:40:59 -0700 Subject: [PATCH 7/9] MNT: properly toggle LoggingVisitor print_status, enable continuous tick as promised, fix cli flags --- beams/bin/run.py | 15 ++++++++------- beams/bin/run_main.py | 24 ++++++++++++++++-------- beams/logging.py | 12 ++++++++++++ beams/tests/test_bin.py | 4 ++-- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/beams/bin/run.py b/beams/bin/run.py index 248910b..d5ba0db 100644 --- a/beams/bin/run.py +++ b/beams/bin/run.py @@ -33,7 +33,8 @@ def build_arg_parser(argparser=None): argparser.add_argument( "-t", "--tick-count", dest="tick_count", default=1, type=int, - help="How many times to tick the tree. Values <=0 mean continuous ticking" + help="How many times to tick the tree. Values <=0 mean continuous ticking " + "(Ctrl+C to terminate tree)" ) argparser.add_argument( "-d", "--tick-delay", @@ -41,14 +42,14 @@ def build_arg_parser(argparser=None): help="Delay time (s) between each tick. Ignored if interactive mode is enabled" ) argparser.add_argument( - "--show-node-status", - action="store_true", dest="show_node_status", default=True, - help="Show node status each time one is ticked" + "--hide-node-status", + action="store_false", dest="show_node_status", default=True, + help="Hide individual node status output" ) argparser.add_argument( - "--show-tree", - action="store_true", dest="show_tree", default=True, - help="Show tree statuses after each tree tick" + "--hide-tree", + action="store_false", dest="show_tree", default=True, + help="Hide tree status summary after each tree tick" ) argparser.add_argument( "--show-blackboard", diff --git a/beams/bin/run_main.py b/beams/bin/run_main.py index bdb5b18..5e4b239 100644 --- a/beams/bin/run_main.py +++ b/beams/bin/run_main.py @@ -74,12 +74,20 @@ def main( show_blackboard) ) tree.setup() - for _ in range(tick_count): - try: - tree.tick() - if interactive: - read_single_keypress() - else: + if tick_count <= 0: + while True: + try: + tree.tick() time.sleep(tick_delay) - except KeyboardInterrupt: - break + except KeyboardInterrupt: + break + else: + for _ in range(tick_count): + try: + tree.tick() + if interactive: + read_single_keypress() + else: + time.sleep(tick_delay) + except KeyboardInterrupt: + break diff --git a/beams/logging.py b/beams/logging.py index 85e77e1..482d55a 100644 --- a/beams/logging.py +++ b/beams/logging.py @@ -173,10 +173,22 @@ class LoggingVisitor(VisitorBase): def __init__(self, print_status: bool = False): self.print_status = print_status super().__init__(full=False) + stream_handler = [h for h in logging.getLogger("beams").handlers + if h.name == "console"][0] + self.stream_handler_level = stream_handler.level or logging.DEBUG def run(self, behaviour: Behaviour) -> None: + """ + Write node status to logging stream. + If print_status is requested and the console logger won't display, also + print to console + """ out_msg = f"{behaviour.__class__.__name__}.run() [{behaviour.status}]" if behaviour.feedback_message: logger.debug(out_msg + f": [{behaviour.feedback_message}]") + if self.print_status and (self.stream_handler_level > logging.DEBUG): + print(out_msg + f": [{behaviour.feedback_message}]") else: logger.debug(out_msg) + if self.print_status and (self.stream_handler_level > logging.DEBUG): + print(out_msg) diff --git a/beams/tests/test_bin.py b/beams/tests/test_bin.py index f5b6d99..d8e83fa 100644 --- a/beams/tests/test_bin.py +++ b/beams/tests/test_bin.py @@ -42,8 +42,8 @@ def test_main_noargs(): RUN_ARGS = ( (("-t", "2"), ("--tick-count", "2"), ()), - (("--show-node-status",), ()), - (("--show-tree",), ()), + (("--hide-node-status",), ()), + (("--hide-tree",), ()), (("--show-blackboard",), ()), ) From 609d90993b21a6faaa88edf86b297330a8d29520 Mon Sep 17 00:00:00 2001 From: Robert Tang-Kong <35379409+tangkong@users.noreply.github.com> Date: Tue, 24 Sep 2024 18:06:05 -0700 Subject: [PATCH 8/9] MNT: Honor interactive argument in continuous mode (ty zllentz) Co-authored-by: Zachary Lentz --- beams/bin/run_main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beams/bin/run_main.py b/beams/bin/run_main.py index 5e4b239..1002ab9 100644 --- a/beams/bin/run_main.py +++ b/beams/bin/run_main.py @@ -78,7 +78,10 @@ def main( while True: try: tree.tick() - time.sleep(tick_delay) + if interactive: + read_single_keypress() + else: + time.sleep(tick_delay) except KeyboardInterrupt: break else: From b7869b06760506784e2b2552706938858df6fc15 Mon Sep 17 00:00:00 2001 From: tangkong Date: Tue, 24 Sep 2024 19:45:59 -0700 Subject: [PATCH 9/9] MNT: Make main function less complex to satisfy flake8 --- beams/bin/run_main.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/beams/bin/run_main.py b/beams/bin/run_main.py index 1002ab9..8324d63 100644 --- a/beams/bin/run_main.py +++ b/beams/bin/run_main.py @@ -48,6 +48,14 @@ def snapshot_post_tick_handler( print(unicode_blackboard()) +def tick_tree(tree: BehaviourTree, interactive: bool, tick_delay: float): + tree.tick() + if interactive: + read_single_keypress() + else: + time.sleep(tick_delay) + + def main( filepath: str, tick_count: int, @@ -77,20 +85,12 @@ def main( if tick_count <= 0: while True: try: - tree.tick() - if interactive: - read_single_keypress() - else: - time.sleep(tick_delay) + tick_tree(tree, interactive, tick_delay) except KeyboardInterrupt: break else: for _ in range(tick_count): try: - tree.tick() - if interactive: - read_single_keypress() - else: - time.sleep(tick_delay) + tick_tree(tree, interactive, tick_delay) except KeyboardInterrupt: break