Skip to content

Commit

Permalink
v0.3.2 - AtLeastOneState (#9)
Browse files Browse the repository at this point in the history
## [0.3.2] - 2021-04-16
- **[Added]** `AtLeastOneState` that ends when one of its children states completes (all are run parallely) and return `success`.
  • Loading branch information
xiangzhi authored Apr 29, 2021
1 parent 0300714 commit 32de8ba
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 72 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [0.3.2] - 2021-04-16
- **[Added]** `AtLeastOneState` that ends when one of its children states completes (all are run parallely) and return `success`.

## [0.3.1] - 2021-04-15
- **[Added]** Added `SelectorState` that mimics `Selector` in behavior trees. The state run the children in order until one of them returns `success`.
- **[Added]** debugging function `get_debug_name` that returns a string with name + type to help debugging and loggin.
Expand Down
2 changes: 1 addition & 1 deletion behavior_machine/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from .nested_state import NestedState
from .machine import Machine
from .state_status import StateStatus
from .board import Board
from .board import Board
3 changes: 2 additions & 1 deletion behavior_machine/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import typing


def parse_debug_info(info: typing.Dict[str, typing.Any], spacing: int = 0, margin: int = 2, prefix: str = "") -> typing.List[str]:
def parse_debug_info(info: typing.Dict[str, typing.Any], spacing: int = 0, margin: int = 2,
prefix: str = "") -> typing.List[str]:

ori_str = f'{prefix}{info["name"]}({info["type"]}) -- {info["status"].name}'
str_list = [ori_str.rjust(len(ori_str) + spacing)]
Expand Down
3 changes: 2 additions & 1 deletion behavior_machine/library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
from .sequential_state import SequentialState
from .common_state import IdleState, WaitState
from .parallel_state import ParallelState
from .selector_state import SelectorState
from .selector_state import SelectorState
from .atleastone_state import AtLeastOneState
50 changes: 50 additions & 0 deletions behavior_machine/library/atleastone_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import sys

from ..core import StateStatus, Board
from .parallel_state import ParallelState


class AtLeastOneState(ParallelState):
""" An extension of parallel state where it stops and return if at least one of the children state
is successful.
"""

def _statestatus_criteria(self) -> StateStatus:
# check the state of all the children & return success if and only if all are successful
for child in self._children:
if child.check_status(StateStatus.SUCCESS):
return StateStatus.SUCCESS
return StateStatus.FAILED

def tick(self, board: Board):
# Note, this is very similar to the ParallelState tick function. Any fixes should also be applied there.
# check if we should transition out of this state
next_state = super().tick(board)
if next_state == self:
# we are staying in this state, tick each of the child
at_least_one_running = False
for child in self._children:
# if the child is running, tick it
if child.check_status(StateStatus.RUNNING):
at_least_one_running = True
child.tick(board)
elif child.check_status(StateStatus.SUCCESS):
self._children_complete_event.set()
elif child.check_status(StateStatus.EXCEPTION):
self.propergate_exception_information(child)
self._child_exception = True
self._children_complete_event.set()
# if all child already done, we need to let the main process knows
if not at_least_one_running:
self._children_complete_event.set()
# return itself since nothing transitioned
return self
else:
# we are going to a new start, interrupt everthing that is going on
if not self.interrupt():
# we wasn't able to complete the interruption.
# This is bad.. meaning there are bunch of zombie threads running about
print(
f"ERROR {self._name} of type {self.__class__} unable to complete Interrupt Action. \
Zombie threads likely", file=sys.stderr)
return next_state
37 changes: 21 additions & 16 deletions behavior_machine/library/parallel_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ def interrupt(self, timeout=None):
return not self._run_thread.is_alive()
# return super().interrupt(timeout=timeout)

def execute(self, board: Board):
def _statestatus_criteria(self) -> StateStatus:
# check the state of all the children & return success if and only if all are successful
all_success = True
for child in self._children:
if not child.check_status(StateStatus.SUCCESS):
all_success = False
return StateStatus.SUCCESS if all_success else StateStatus.FAILED

def execute(self, board: Board) -> StateStatus:

# clear the event flag before starting
self._children_complete_event.clear()
Expand Down Expand Up @@ -74,14 +82,10 @@ def execute(self, board: Board):
# return the exception state
return StateStatus.EXCEPTION

# check the state of all the children & return success if and only if all are successful
all_success = True
for child in self._children:
if not child.check_status(StateStatus.SUCCESS):
all_success = False
return StateStatus.SUCCESS if all_success else StateStatus.FAILED
return self._statestatus_criteria()

def tick(self, board):
def tick(self, board: Board):
# Note, this is very similar to the AtLeastOneState's tick function. Any fixes should also be applied there.
# check if we should transition out of this state
next_state = super().tick(board)
if next_state == self:
Expand All @@ -103,14 +107,15 @@ def tick(self, board):
self._children_complete_event.set()
# return itself since nothing transitioned
return self
# we are going to a new start, interrupt everthing that is going on
if not self.interrupt():
# we wasn't able to complete the interruption.
# This is bad.. meaning there are bunch of zombie threads running about
print(
f"ERROR {self._name} of type {self.__class__} unable to complete Interrupt Action. \
Zombie threads likely", file=sys.stderr)
return next_state
else:
# we are going to a new start, interrupt everthing that is going on
if not self.interrupt():
# we wasn't able to complete the interruption.
# This is bad.. meaning there are bunch of zombie threads running about
print(
f"ERROR {self._name} of type {self.__class__} unable to complete Interrupt Action. \
Zombie threads likely", file=sys.stderr)
return next_state

def get_debug_info(self) -> typing.Dict[str, typing.Any]:

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
long_description = fh.read()

setuptools.setup(name='behavior_machine',
version='0.3.1',
version='0.3.2',
description='An implementation of a behavior tree + hierarchical state machine hybrid.',
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
56 changes: 56 additions & 0 deletions tests/library/atleastone_state_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import time
import sys
import io
import pytest
import typing

from behavior_machine.core import Machine, State, StateStatus, Board
from behavior_machine.library import PrintState, AtLeastOneState, SequentialState, IdleState, WaitState


def test_atleastone_state(capsys):

ps1 = PrintState('p1', "ps1")
ws1 = WaitState("w1", 0.5)
ps2 = PrintState('p2', "ps2")

one = AtLeastOneState("one", children=[
ps2,
SequentialState("seq", children=[
ws1,
ps1
])
])
es = IdleState("endState")
one.add_transition_on_success(es)
exe = Machine("xe", one, end_state_ids=["endState"], rate=10)
exe.run()

assert capsys.readouterr().out == "ps2\n"


def test_atleastone_interrupt(capsys):

interrupted = False

class WaitAndPrint(State):
def execute(self, board: Board) -> typing.Optional[StateStatus]:
time.sleep(0.5)
if self.is_interrupted():
nonlocal interrupted
interrupted = True
return StateStatus.INTERRUPTED
print("HelloWorld")
return StateStatus.SUCCESS

one = AtLeastOneState("one", children=[
PrintState('p5', "ps5"),
WaitAndPrint("ws")
])
es = IdleState("endState")
one.add_transition_on_success(es)
exe = Machine("xe", one, end_state_ids=["endState"], rate=10)
exe.run()

assert capsys.readouterr().out == "ps5\n"
assert interrupted
55 changes: 55 additions & 0 deletions tests/performance_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import time

from behavior_machine.core import Board, StateStatus, State, Machine


def test_repeat_node_in_machine_fast():

counter = 0

class CounterState(State):
def execute(self, board: Board) -> StateStatus:
nonlocal counter
counter += 1
return StateStatus.SUCCESS

ds1 = CounterState("ds1")
ds2 = CounterState("ds2")
ds3 = CounterState("ds3")
ds1.add_transition_on_success(ds2)
ds2.add_transition_on_success(ds3)
ds3.add_transition_on_success(ds1)

exe = Machine('exe', ds1, rate=60)
exe.start(None)
time.sleep(2)
exe.interrupt()
# the performance of the computer might change this.
assert counter >= (60 * 2) - 2
assert counter <= (60 * 2) + 1


def test_validate_transition_immediate():

counter = 0

class CounterState(State):
def execute(self, board: Board) -> StateStatus:
nonlocal counter
counter += 1
return StateStatus.SUCCESS

ds1 = CounterState("ds1")
ds2 = CounterState("ds2")
ds3 = CounterState("ds3")
ds1.add_transition(lambda s, b: True, ds2)
ds2.add_transition(lambda s, b: True, ds3)
ds3.add_transition(lambda s, b: True, ds1)

exe = Machine('exe', ds1, rate=60)
exe.start(None)
time.sleep(2)
exe.interrupt()
# the performance of the computer might change this.
assert counter >= (60 * 2) - 2
assert counter <= (60 * 2) + 1
52 changes: 0 additions & 52 deletions tests/statemachine_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,55 +309,3 @@ def execute(self, board: Board) -> StateStatus:
assert exe._curr_state == ds5
exe.interrupt()
assert counter == (5 * 5) + 1


def test_repeat_node_in_machine_fast():

counter = 0

class CounterState(State):
def execute(self, board: Board) -> StateStatus:
nonlocal counter
counter += 1
return StateStatus.SUCCESS

ds1 = CounterState("ds1")
ds2 = CounterState("ds2")
ds3 = CounterState("ds3")
ds1.add_transition_on_success(ds2)
ds2.add_transition_on_success(ds3)
ds3.add_transition_on_success(ds1)

exe = Machine('exe', ds1, rate=60)
exe.start(None)
time.sleep(2)
exe.interrupt()
# the performance of the computer might change this.
assert counter >= (60 * 2) - 2
assert counter <= (60 * 2) + 1


def test_validate_transition_immediate():

counter = 0

class CounterState(State):
def execute(self, board: Board) -> StateStatus:
nonlocal counter
counter += 1
return StateStatus.SUCCESS

ds1 = CounterState("ds1")
ds2 = CounterState("ds2")
ds3 = CounterState("ds3")
ds1.add_transition(lambda s, b: True, ds2)
ds2.add_transition(lambda s, b: True, ds3)
ds3.add_transition(lambda s, b: True, ds1)

exe = Machine('exe', ds1, rate=60)
exe.start(None)
time.sleep(2)
exe.interrupt()
# the performance of the computer might change this.
assert counter >= (60 * 2) - 2
assert counter <= (60 * 2) + 1

0 comments on commit 32de8ba

Please sign in to comment.