Reputation: 21
I would like to simulate a machine using the impressive python-statemachine
package. It is the first time I've read about the concept of State Machine, so I've been spending about one day reading the (again) impressive package documentation. I think I have a use case that the concept and the package is applicable to. I would like to learn how to.
The machine has multiple operating states. For a minimal working example (MWE), I assume three states, namely Off
, Standby
, On
. The machine can do work only in one state, that is On
in the MWE. The machine can only transition from Off
to On
via Standby
, which will require some time.
Once the machine is requested to do work (an event), it should transition to the On
state regardless of the state it is currently in, do the work, and then transition to back Standby
, respecting the defined sequence Off
-> Standby
-> On
.
I have currently achieved the desired behavior by using a custom method switch_on
, which implements the given sequence of states via if
-statements. It is called in the method ("action") do_work
, which handles the work procedure of the machine.
from statemachine import StateMachine, State
class Machine(StateMachine):
"""
A simple machine model with three states: off, standby, and on.
"""
# Define the states
off = State(initial=True)
standby = State()
on = State()
# Define events triggering transitions
off_to_standby = off.to(standby, on="off_to_standby_action")
standby_to_on = standby.to(on, on="standby_to_on_action")
on_to_standby = on.to(standby, on="on_to_standby_action")
def __init__(self, off_to_standby_time, standby_to_on_time, on_to_standby_time):
# characteristics
self.off_to_standby_time = off_to_standby_time
self.standby_to_on_time = standby_to_on_time
self.on_to_standby_time = on_to_standby_time
# live variable
self.time_counter = 0
self.work_done = 0
super(Machine, self).__init__()
# Define state actions. Use decorator syntax to avoid stupid method names like `on_enter_on`.
@off.enter
def turn_off(self):
print("Turning off")
@standby.enter
def turn_standby(self):
print("Turning to standby")
@on.enter
def turn_on(self):
print("Turning on")
# Define transition actions
def off_to_standby_action(self):
print("Transition: Off to standby")
self.time_counter += self.off_to_standby_time
return self.time_counter
def standby_to_on_action(self):
print("Transition: Standby to on")
self.time_counter += self.standby_to_on_time
return self.time_counter
def on_to_standby_action(self):
print("Transition: On to standby")
self.time_counter += self.on_to_standby_time
return self.time_counter
# Define custom methods
def switch_on(self): # <------------ enforce the defined sequence of states via if-statement
if self.off.is_active:
self.off_to_standby()
self.standby_to_on()
elif self.standby.is_active:
self.standby_to_on()
def work_action(self, amount_of_work, time): # action to be executed in the "on" state only
print("Doing work")
self.work_done += amount_of_work
self.time_counter += time
return self.work_done, self.time_counter
def do_work(self, amount_of_work, time): # <------------ can this be declared using the event syntax instead?
self.switch_on() # call to the "event" triggering transition to the "on" state in the defined sequence
w, t = self.work_action(amount_of_work, time) # call to the action to be executed in the "on" state
self.on_to_standby()
return w, t
def main():
print("Start machine.")
machine = Machine(1, 2, 3)
machine.do_work(10, 1)
machine.do_work(20, 2)
print("finished work.")
print("State: ", machine.current_state, ", Time: ", machine.time_counter, ", Work done: ", machine.work_done)
if __name__ == '__main__':
main()
When I run the code I get the following output.
Start machine.
Turning off
Transition: Off to standby
Turning to standby
Transition: Standby to on
Turning on
Doing work
Transition: On to standby
Turning to standby
Transition: Standby to on
Turning on
Doing work
Transition: On to standby
Turning to standby
finished work.
State: Standby , Time: 14 , Work done: 30
This is the expected behavior.
time_counter
: 1+2+1+3+2+2+3=14
work_done
: 10+20=30
I've reduced the above MWE to a shorter and simpler one by removing the context. I will add it here for clearity and for people who may have other use cases.
Consider the states a
, b
, c
. The machine should go to state c
always in the sequence a
-> b
-> c
. After reaching state c
and performing some action, it should go back to state b
.
(I kept this last extra transition in the example to avoid a UserWarning
by the package that all non-final states should have at least one outgoing transition.)
My current solution is the same as in the other MWE: I use a custom method to_c
which implements the defined sequence of states via if
-statements and is called in the custom method do_c
before performing some action.
from statemachine import StateMachine, State
class AbstractMachine(StateMachine):
# Define the states
a = State(initial=True)
b = State()
c = State()
# Define events triggering transitions
a_to_b = a.to(b)
b_to_c = b.to(c)
c_to_b = c.to(b)
def __init__(self):
self.action_list = []
super(AbstractMachine, self).__init__()
# Define state actions
def on_enter_a(self):
print("On a")
def on_enter_b(self):
print("On b")
def on_enter_c(self):
print("On c")
# Define custom methods
def to_c(self): # <------------ enforce the defined sequence of states via if-statement
if self.a.is_active:
self.a_to_b()
self.b_to_c()
elif self.b.is_active:
self.b_to_c()
def do_c(self, action): # <------------ can this be done using the event syntax instead?
self.to_c() # call to the "event" triggering transition to state "a" in the defined sequence
print("Action: ", action)
self.action_list.append(action) # action to be executed in the "c" state only
self.c_to_b()
return self.action_list
def main():
print("Start abstract machine.")
abstract_machine = AbstractMachine()
abstract_machine.do_c("do_something")
abstract_machine.do_c("do_something_else")
print("finished.")
print("State: ", abstract_machine.current_state, ", Actions: ", abstract_machine.action_list)
if __name__ == '__main__':
main()
Start abstract machine.
On b
On c
Action: do_something
On b
On c
Action: do_something_else
On b
finished.
State: B , Actions: ['do_something', 'do_something_else']
How can I achieve the desired behavior by employing the syntax provided by the package, i.e. defining events triggering transitions between states and invoking the desired actions?
The main reason is that I would like to avoid redundance in case of a larger number of states by only defining the sequence of states once. Another reason is that I would like to learn best-practices in the application of the python-statemachine
package.
Edit: I managed to come up with an improved version that solves the first part of the question. I submitted it as an answer to this question. I will not mark it as solved since it is only one part of what I was asking for and I hope to get some interaction from users with some experience on the topic.
How can I avoid explicitly enforcing the defined transition sequence via if
-statements in a custom method, when attempting to execute work_action
in the correct state On
? In particular, is there a solution to respect the defined transtition sequence and execute the action work_action
that involves defining one or multiple events like switch_on
using the syntax with the overloaded OR |
operator provided by the package? In addition, how can I invoke the work_action
as a proper action callback? Specifically, I would like to access the machine from the outside (here: main
method) only via calls to events like machine.do_work(amount, time)
with do_work
defined as described in the docs.
Again, how can I avoid the use of a custom method to_c
with if
-statements and instead use the event syntax provided by the package? Furthermore, how can I avoid the definition of a custom method do_c
and rather cover this functionality using the event-syntax provided by the package.
I tried to think about it a lot while reading the documentation. I also tried interacting with a chatbot. The best solution I came up with is the one in the MWE, which is why I'm now betting on a conversation with real human beings.
Upvotes: 1
Views: 59
Reputation: 21
I came up with a solution I like better than the one in my MWE. I have solved the aspect of defining the sequence of states only once using the packages event-syntax and then using a while
-loop in the custom method.
from statemachine import StateMachine, State
class Machine(StateMachine):
# Define the states
off = State(initial=True)
standby = State()
on = State()
# Define the sequence of states in a single event triggering transitions
boot_up = off.to(standby, on="off_to_standby_action") | standby.to(on, on="standby_to_on_action")
on_to_standby = on.to(standby, on="on_to_standby_action")
def __init__(self, off_to_standby_time, standby_to_on_time, on_to_standby_time):
# characteristics
self.off_to_standby_time = off_to_standby_time
self.standby_to_on_time = standby_to_on_time
self.on_to_standby_time = on_to_standby_time
# live variable
self.time_counter = 0
self.work_done = 0
super(Machine, self).__init__()
# Define state actions. Use decorator syntax to avoid stupid method names like `on_enter_on`.
@off.enter
def turn_off(self):
print("Turning off")
@standby.enter
def turn_standby(self):
print("Turning to standby")
@on.enter
def turn_on(self):
print("Turning on")
# Define transition actions
def off_to_standby_action(self):
print("Transition: Off to standby")
self.time_counter += self.off_to_standby_time
return self.time_counter
def standby_to_on_action(self):
print("Transition: Standby to on")
self.time_counter += self.standby_to_on_time
return self.time_counter
def on_to_standby_action(self):
print("Transition: On to standby")
self.time_counter += self.on_to_standby_time
return self.time_counter
# Define custom methods
def work_action(self, amount_of_work, time): # action to be executed in the "on" state only
print("Doing work")
self.work_done += amount_of_work
self.time_counter += time
return self.work_done, self.time_counter
def do_work(self, amount_of_work, time): # <------------ can this be declared using the event syntax instead?
while not self.on.is_active: # while loop stopping when the "on" state is reached
self.boot_up() # event triggering transitions according to the defined sequence of states
w, t = self.work_action(amount_of_work, time)
self.on_to_standby()
return w, t
def main():
print("Start machine.")
machine = Machine(1, 2, 3)
machine.do_work(10, 1)
machine.do_work(20, 2)
print("finished work.")
print("State: ", machine.current_state, ", Time: ", machine.time_counter, ", Work done: ", machine.work_done)
if __name__ == '__main__':
main()
Start machine.
Turning off
Transition: Off to standby
Turning to standby
Transition: Standby to on
Turning on
Doing work
Transition: On to standby
Turning to standby
Transition: Standby to on
Turning on
Doing work
Transition: On to standby
Turning to standby
finished work.
State: Standby , Time: 14 , Work done: 30
from statemachine import StateMachine, State
class AbstractMachine(StateMachine):
# Define the states
a = State(initial=True)
b = State()
c = State()
# Define the sequence of states in a single event triggering transitions
boot_up = a.to(b) | b.to(c)
c_to_b = c.to(b)
def __init__(self):
self.action_list = []
super(AbstractMachine, self).__init__()
# Define state actions
def on_enter_a(self):
print("On a")
def on_enter_b(self):
print("On b")
def on_enter_c(self):
print("On c")
# Define custom methods
def do_c(self, action): # <------------ can this be done using the event syntax instead?
while not self.c.is_active: # while loop stopping when the "on" state is reached
self.boot_up() # event triggering transitions according to the defined sequence of states
print("Action: ", action)
self.action_list.append(action)
self.c_to_b()
return self.action_list
def main():
print("Start abstract machine.")
abstract_machine = AbstractMachine()
abstract_machine.do_c("do_something")
abstract_machine.do_c("do_something_else")
print("finished.")
print("State: ", abstract_machine.current_state, ", Actions: ", abstract_machine.action_list)
if __name__ == '__main__':
main()
Start abstract machine.
On a
On b
On c
Action: do_something
On b
On c
Action: do_something_else
On b
finished.
State: B , Actions: ['do_something', 'do_something_else']
boot_up
using the event syntax that implements the defined sequence. The On
state is reached via the correct transitions by executing this event in a while
-loop in the custom method do_work
. Thus, avoid enforcing the transitions in the defined sequence of states Off
-> Standby
-> On
via explicit if
-statements in the method switch_on
in the original MWE.do_work
with an action callback to the work_action
method, while also transitioning to the On
state via the defined sequence of states.boot_up
implementing the defined sequence of states only once. The state c
is reached via the correct transitions by executing this event in a while
-loop in the custom method do_c
. Avoid using if
-statements to enforce the transition sequence.do_c
with the desired action callback self.action_list.append(action)
, while first transitioning to the state c
via the defined sequence.Upvotes: 1