Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Nested states (compound / parallel) #329

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from

Conversation

fgmacedo
Copy link
Owner

@fgmacedo fgmacedo commented Jan 23, 2023

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

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

@fgmacedo fgmacedo force-pushed the macedo/compound-states branch 5 times, most recently from 0a548c1 to cf2cc43 Compare January 27, 2023 19:11
@Rosi2143
Copy link
Contributor

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.

@fgmacedo fgmacedo force-pushed the macedo/compound-states branch 3 times, most recently from c28a40c to 64e9bc8 Compare February 11, 2023 19:34
@fgmacedo fgmacedo force-pushed the macedo/compound-states branch 4 times, most recently from 3fef20a to c1fc3bd Compare February 24, 2023 17:22
@fgmacedo fgmacedo force-pushed the macedo/compound-states branch 2 times, most recently from 8315afd to 4abd2ab Compare March 4, 2023 21:02
@sandeep2rawat
Copy link

Wow looking for this feat, required in my project.

@fgmacedo fgmacedo force-pushed the macedo/compound-states branch 2 times, most recently from d98ebff to a8a139a Compare November 2, 2023 09:13
Repository owner deleted a comment Jul 9, 2024
Repository owner deleted a comment from sonarcloud bot Jul 9, 2024
Repository owner deleted a comment from codecov bot Jul 9, 2024
Repository owner deleted a comment from sonarcloud bot Jul 9, 2024
@fgmacedo fgmacedo force-pushed the macedo/compound-states branch 2 times, most recently from 65536cd to 2c427cd Compare July 9, 2024 20:28
Copy link

sonarcloud bot commented Jul 11, 2024

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarCloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants