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 1 commit 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__()

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.
  • Implement support for compound state:
  • Implement support for parallel state:

@codecov
Copy link

codecov bot commented Jan 23, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (9d177b2) 100.00% compared to head (93fd520) 100.00%.

❗ Current head 93fd520 differs from pull request most recent head a8a139a. Consider uploading reports for the commit a8a139a to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##           develop      #329    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           20        19     -1     
  Lines         1106       966   -140     
  Branches       173       163    -10     
==========================================
- Hits          1106       966   -140     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
statemachine/factory.py 100.00% <100.00%> (ø)
statemachine/state.py 100.00% <100.00%> (ø)

... and 10 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@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
@sonarcloud
Copy link

sonarcloud bot commented Mar 20, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 1 Code Smell

No Coverage information No Coverage information
0.0% 0.0% Duplication

@sandeep2rawat
Copy link

Wow looking for this feat, required in my project.

@ghost
Copy link

ghost commented Nov 2, 2023

👇 Click on the image for a new way to code review

Review these changes using an interactive CodeSee Map

Legend

CodeSee Map legend

Copy link

sonarcloud bot commented Nov 2, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 1 Code Smell

No Coverage information No Coverage information
0.0% 0.0% Duplication

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.

None yet

3 participants