HenryShines
HenryShines

Reputation: 21

How to transition multiple states in a predefined sequence in dependence of the current state using `python-statemachine` events syntax?

Motivation

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.

Context

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.

Use case minimal working example

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()

Output

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

Abstract minimal working example

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()

Output

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']

Question

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.

Question specified for the use case MWE

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.

Question specified for the abstract MWE

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.

Efforts

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

Answers (1)

HenryShines
HenryShines

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.

Improved use case minimal working example

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()

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

Improved abstract minimal working example

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()

Output

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']

Evaluation

  • [x] Define the sequence of states only once using the packages event syntax, avoiding redundance.
  • [ ] Use the packages event syntax to implement the action callback to be executed once the desired state is reached.

Evaluation specific to the use case minimal working example

  • [x] Define an event 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.
  • [ ] Use the packages event syntax to implement the event do_work with an action callback to the work_action method, while also transitioning to the On state via the defined sequence of states.

Evaluation specific to the abstract working example

  • [x] Use the event syntax to define an event 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.
  • [ ] Use the packages event syntax to implement the event 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

Related Questions