python-statemachine icon indicating copy to clipboard operation
python-statemachine copied to clipboard

feat: Nested states (compound / parallel)

Open fgmacedo opened this issue 3 years ago • 3 comments

Experimental branch to play with compound and parallel states.

On this PR, I'm trying to implement a "simple" example from SCXML called "microwave", that has parallel and compound states.

Microwave

SCXML

From the MicrowaveParallel example spec.

<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" datamodel="ecmascript" initial="oven">

    <!-- trivial 5 second microwave oven example -->
    <!-- using parallel and In() predicate -->
    <datamodel>
        <data id="cook_time" expr="5"/>
        <data id="door_closed" expr="true"/>
        <data id="timer" expr="0"/>
    </datamodel>

    <parallel id="oven">

        <!-- this region tracks the microwave state and timer -->
        <state id="engine">
            <initial>
                <transition target="off"/>
            </initial>

            <state id="off">
                <!-- off state -->
                <transition event="turn.on" target="on"/>
            </state>

            <state id="on">
                <initial>
                    <transition target="idle"/>
                </initial>

                <!-- on/pause state -->

                <transition event="turn.off" target="off"/>
                <transition cond="timer &gt;= cook_time" target="off"/>

                <state id="idle">
                    <transition cond="In('closed')" target="cooking"/>
                </state>

                <state id="cooking">
                    <transition cond="In('open')" target="idle"/>

                    <!-- a 'time' event is seen once a second -->
                    <transition event="time">
                        <assign location="timer" expr="timer + 1"/>
                    </transition>
                </state>
            </state>
        </state>

        <!-- this region tracks the microwave door state -->
        <state id="door">
            <initial>
                <transition target="closed"/>
            </initial>
            <state id="closed">
                <transition event="door.open" target="open"/>
            </state>
            <state id="open">
                <transition event="door.close" target="closed"/>
            </state>
        </state>

    </parallel>

</scxml>

Using python-statemachine

** Experimental syntax **

Note that I'm using a class as a namespace for constructing a State instance. Not a traditional choice, but I like the syntax so far.

from statemachine import State
from statemachine import StateMachine


class MicroWave(StateMachine):
    class oven(State.Builder, name="Microwave oven", parallel=True):
        class engine(State.Builder):
            off = State("Off", initial=True)

            class on(State.Builder):
                idle = State("Idle", initial=True)
                cooking = State("Cooking")

                idle.to(cooking, cond="closed.is_active")
                cooking.to(idle, cond="open.is_active")
                cooking.to.itself(internal=True, on="increment_timer")

            assert isinstance(on, State)  # so mypy stop complaining
            turn_off = on.to(off)
            turn_on = off.to(on)
            on.to(off, cond="cook_time_is_over")  # eventless transition

        class door(State.Builder):
            closed = State(initial=True)
            open = State()

            door_open = closed.to(open)
            door_close = open.to(closed)

    def __init__(self):
        self.cook_time = 5
        self.door_closed = True
        self.timer = 0
        super().__init__()

Diagram is already rendering nested states:

image

If you're reading this, feedback is welcome. Please let me know what you think.

I am trying to handle nested states in the lib (it works well for simple machines), but I have been reading quite a bit about statecharts (https://www.w3.org/TR/scxml/), and they solve a common problem with state machines: state explosion. More complex use cases become infeasible to express with a simple machine.

These nested states work in two ways:

  1. Compound: The substates act as an XOR, only one substate is active at a time, it's like a sub-state machine.
  2. Parallel: The substates act as an AND, meaning, all are active at the same time, it's like multiple sub-state machines.

The example I am trying to implement coming from SCXML documentation is a "microwave", in it, the "oven" and the "door" are two parallel states, as they work independently. The oven and the door are also compound states, as they have substates.

The syntax I am trying to validate is "how to express in a pythonic way" this hierarchy. The best syntax I came up with is the one in the PR, where I made "creative use" of the block context generated by a class to capture the variables created inside the context as substates of the parent state, and I use the class name and optional metaclass attributes to parameterize the parent state. The result is an instance of a 'State' already filled with the substrates.

So this:

class door(State.Builder):
    closed = State(initial=True)
    open = State()

    door_open = closed.to(open)
    door_close = open.to(closed)

Works like syntactic sugar for this (but keeping the parent namespace clean):

closed = State(initial=True) 
open = State()
door_open = closed.to(open)
door_close = open.to(closed)

door = State(substates=[closed, open])

TODO

  • [x] Syntax proposal.
  • [x] Diagram nested states
  • [ ] Implement support for compound state:
  • [ ] Implement support for parallel state:

fgmacedo avatar Jan 23 '23 18:01 fgmacedo

I like the idea of compound states, as they tend to make my life a lot easier.

I will have a look at it when I find some time.

Rosi2143 avatar Jan 30 '23 18:01 Rosi2143

Wow looking for this feat, required in my project.

sandeep2rawat avatar May 01 '23 14:05 sandeep2rawat

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
19.2% Duplication on New Code (required ≤ 10%)

See analysis details on SonarCloud

sonarqubecloud[bot] avatar Jul 11 '24 23:07 sonarqubecloud[bot]