From 2110c7ad0373696a04e8a6b394912f6479e39210 Mon Sep 17 00:00:00 2001 From: Zhi Date: Wed, 16 Feb 2022 22:41:01 -0500 Subject: [PATCH] v0.4.0 - Version Push (#13) ## [0.4.0] - 2022-02-16 - **[Added]** `RandomPickState` which randomly pick one of the children to be executed. All children has uniform probability being picked. - **[Added]** added `get_status` method in `State` which return the status of the State. - **[Changed]** name for `State` is now optional, the default is its class name. - **[Changed]** streamlined `ParallelState` to prevent potential interrupt race issues. Enable `AtLeastOneState` to overwrite two function for its functionality. - **[Changed]** `ParallelState` now ignores empty/None States if passed in as children. - **[Fixed]** condition in `AtLeastOneState` where other states aren't destroyed when exiting under success condition. --- CHANGELOG.md | 6 ++- README.md | 8 +--- behavior_machine/core/state.py | 14 ++++++- behavior_machine/library/__init__.py | 1 + behavior_machine/library/parallel_state.py | 2 +- .../library/probability_states.py | 40 +++++++++++++++++++ setup.py | 2 +- tests/library/prob_states_test.py | 29 ++++++++++++++ 8 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 behavior_machine/library/probability_states.py create mode 100644 tests/library/prob_states_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 075a3dd..ed42dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # Changelog -## [Unreleased] +## [0.4.0] - 2022-02-16 +- **[Added]** `RandomPickState` which randomly pick one of the children to be executed. All children has uniform probability being picked. +- **[Added]** added `get_status` method in `State` which return the status of the State. +- **[Changed]** name for `State` is now optional, the default is its class name. - **[Changed]** streamlined `ParallelState` to prevent potential interrupt race issues. Enable `AtLeastOneState` to overwrite two function for its functionality. +- **[Changed]** `ParallelState` now ignores empty/None States if passed in as children. - **[Fixed]** condition in `AtLeastOneState` where other states aren't destroyed when exiting under success condition. ## [0.3.5] - 2021-06-15 diff --git a/README.md b/README.md index bb785a3..4eacfb7 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,4 @@ m2.run() ![Example](examples/readme.png) ## Documentation -You can view more detailed documentation by following this [link](https://behavior-machine.readthedocs.io/en/latest/index.html) - - -## TODO Lists: -1. Implement flows for `ParallelState`. The idea would be the same flow value is duplicated (deep?) for each state -2. Rethink flows for `SelectorState`. -3. A better logging system. \ No newline at end of file +You can view more detailed documentation by following this [link](https://behavior-machine.readthedocs.io/en/latest/index.html) \ No newline at end of file diff --git a/behavior_machine/core/state.py b/behavior_machine/core/state.py index bcb64a8..98f4a06 100644 --- a/behavior_machine/core/state.py +++ b/behavior_machine/core/state.py @@ -30,8 +30,8 @@ class State(): _state_last_start_time: float _state_last_end_time: float - def __init__(self, name): - self._name = name + def __init__(self, name: str = ""): + self._name = name if name != "" else self.__class__.__name__ self._transitions = [] self._run_thread = None self._interupted_event = threading.Event() @@ -80,6 +80,16 @@ def checkStatus(self, compare: StateStatus) -> bool: warnings.warn("use check_status instead", DeprecationWarning) return self.check_status(compare) + def get_status(self) -> StateStatus: + """Get the state's status if completed. + + Returns + ------- + StateStatus + The status of the state. + """ + return self._status + def add_transition(self, cond: typing.Callable[['State', Board], bool], next_state: 'State') -> None: """Add transition to the state. Provide a checking method (cond) that when returns true, will signal this state to transition to the state associated. Note, the transition is test in a list. If multiple diff --git a/behavior_machine/library/__init__.py b/behavior_machine/library/__init__.py index 9efedc7..5b53a66 100644 --- a/behavior_machine/library/__init__.py +++ b/behavior_machine/library/__init__.py @@ -5,3 +5,4 @@ from .selector_state import SelectorState from .common_state import SetFlowState, SetFlowFromBoardState, SaveFlowState, SetBoardState, GetBoardState from .atleastone_state import AtLeastOneState +from .probability_states import RandomPickState \ No newline at end of file diff --git a/behavior_machine/library/parallel_state.py b/behavior_machine/library/parallel_state.py index da472d2..4608b9a 100644 --- a/behavior_machine/library/parallel_state.py +++ b/behavior_machine/library/parallel_state.py @@ -13,7 +13,7 @@ class ParallelState(NestedState): def __init__(self, name, children: list = None): super(ParallelState, self).__init__(name) - self._children = [] if children is None else children + self._children = [] if children is None else list(filter(None, children)) self._state_complete_event = threading.Event() self._child_exception = False diff --git a/behavior_machine/library/probability_states.py b/behavior_machine/library/probability_states.py new file mode 100644 index 0000000..bb01b35 --- /dev/null +++ b/behavior_machine/library/probability_states.py @@ -0,0 +1,40 @@ +import threading +from ..core import StateStatus, State, NestedState, Board +import typing +import random +import threading + +class RandomPickState(NestedState): + + _picked_state: State + _children: typing.Sequence[State] + _lock: threading.RLock + + def __init__(self, children: typing.Sequence[State],name = ""): + self._children = children + self._picked_state = None + self._lock = threading.RLock() + super().__init__(name) + + def execute(self, board: Board) -> StateStatus: + + with self._lock: + self._picked_state = random.choice(self._children) + self._picked_state.start(board) + + self._picked_state.wait() + + # set the flow out and pass the status out. + self.flow_out = self._picked_state.flow_out + result_status = self._picked_state.get_status() + with self._lock: + self._picked_state = None + return result_status + + def interrupt(self, timeout: float = None) -> bool: + # we have a lock here just in case it suddenly become None when interrupting. + self.signal_interrupt() + with self._lock: + if self._picked_state is not None: + return self._picked_state.interrupt(timeout) + return True diff --git a/setup.py b/setup.py index 357b4f9..fd84d12 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ long_description = fh.read() setuptools.setup(name='behavior_machine', - version='0.3.5', + version='0.4.0', description='An implementation of a behavior tree + hierarchical state machine hybrid.', long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/library/prob_states_test.py b/tests/library/prob_states_test.py new file mode 100644 index 0000000..82c6cf3 --- /dev/null +++ b/tests/library/prob_states_test.py @@ -0,0 +1,29 @@ +from behavior_machine.core import Machine, State, StateStatus, Board +from behavior_machine.library import RandomPickState + + +def test_random_pick(): + + c1 = 0 + c2 = 0 + class s1(State): + def execute(self, board: Board) -> StateStatus: + nonlocal c1 + c1 += 1 + return StateStatus.SUCCESS + class s2(State): + def execute(self, board: Board) -> StateStatus: + nonlocal c2 + c2 += 1 + return StateStatus.SUCCESS + + ranPick = RandomPickState(children=[ + s1(), + s2() + ]) + + for i in range(0,1000): + ranPick.start(None) + ranPick.wait() + assert 450 < c1 < 550 + assert 450 < c2 < 550