Found a Python State Machine Package - oh boy

I often find after the fact that I have created a reentrant function with one or more variables and a giant if_elif_elif_elif_elif_else with more if_else beneath each elif. Essentially this is a state machine with legal transitions, but it ends up being nearly impossible to get right, to debug, and to maintain.

I found the python-statemachine package which allows a somewhat more readable coding style. I’m still learning all it has to offer and how to code “events”. (There is a running controversy about allowing events in state machines, that I have never understood. It has always been clear to me that events trigger transitions. But then I feel that states should execute code but everyone knows it is the transitions that execute code.)

The Brooks subsumption mobile robot architecture is supposed to be layers of simultaneously executing state machines with an arbitrator to decide what layer is given control over the wheels (or other critical shared resource).

In trying to create a threaded drive_to_the_dock behavior, I have to have the behavior check for an ArUco marker, then issue driving directions while periodically adjusting the drive direction commands to center the marker, all while checking the distance to the dock, and then stopping and turning a 180 and then detecting when the 180 turn is complete.

The code is a nightmare of flags to indicate what section of the code to execute each time the behavior loops, so I need to flatten it into a true state machine.

Wish me luck.

AFAIK, it is events that trigger state transitions and state transitions that trigger actions.

However, IMHO, this whole “controversy” is a debate over what shade of blue the sky is.

Also IMHO, if the code works, be happy.  Then refactor.

1 Like

That has always been my take, but I first got tied up in the controversy in 1978 in a remote learning Stanford U. course “Theory of Automata and Modern Compilers”. No events were allowed, only an input stream of one character at a time in a Deterministic Finite State Machine. I kept wanting to generalize and the professor wanted me to remain specific to the definition.

The ROS community is all a buzz with behavior-trees so I’m going to have to wrap up my python-statemachine investigation quickly and move onto investigate PyTrees:

  • PyTrees is a python implementation of behaviour trees designed to facilitate the rapid development of medium sized decision making engines for use in fields like robotics. Brief feature list:

    • Sequence, Selector, Parallel composites
    • Blackboards for data sharing

I love blackboard systems - roughly 10 years of my career was writing blackboard “expert system” rule-based applications. (I still get weekly head hunter emails from one of my last projects writing cancer drug authentication rules in the Pega blackboard system in someone’s old copy of my CV)

P.S. I just did 5 minutes of reading on PyTrees and it clearly is way too sophisticated for my needs at this point.

1 Like

I hate to bust your professor’s bubble, but in that scenario the characters in the character stream represented the events.

What’s a “blackboard system”?

1 Like

A blackboard system consists of a global memory area used as a “Knowledge Base”, that multiple “Knowledge Sources” are watching for conditions that they may be of help to solve a problem or advance the knowledge base.

For example a “distance sensor knowledge source” (a process or thread) might read the distance sensor 10 times a second and post the reading on the blackboard.

An “Obstacle Recognition Knowledge Source” might have a rule that says “if a distance_sensor_reading (on the blackboard) is less than 20 cm, an obstacle might be present” (post “possible obstacle ahead” to blackboard.)

Lots of knowledge sources are watching for conditions and goals they can help with, even though no single knowledge source has all the smarts to complete the goal without the help of the others.

Sometimes more than one knowledge source may be able to advance toward a goal and an arbitrator will need to choose which to trust more.

There can also be parallel potential solution plans being built, and the first plan that will achieve a goal will proceed to implementation.

The blackboard system can also provide explanations of why something happened or did not happen.

Finite State Machines can be built in a Blackboard system but they are not easily recognizable.

1 Like

Maybe now everyone does - up until a few minutes ago it was apparently everyone - 1 :rofl:

Wow - how was that done in 1978? I know they’ve had “courses by mail” going way back, but I wouldn’t think this topic would lend itself to that.

As someone who’s professional background is in a completely different field, I’m always impressed by the depth of technical knowledge you and Jim have. Doesn’t diminish the pleasure I get in dabbling, but I know I’m just scratching the surface.

/K

2 Likes

It was very weird for me. I had graduated with a Mechanical Engineering degree and hired into Hewlett-Packard’s “Silicon Mountain” Colorado facility designing the mechanical parts of one of the first “Microprocessor Development Workstation” with keyboard, display, integral editor, Assembler, Pascal Compiler, EPROM burner, source level debugger, and full address+data logic analyzer.

I had been taking some Master’s level computer science night classes at the local university, and eventually announced to the management that I wanted to switch to writing software. While everyone there was either a Stanford or UC Davis Masters Comp-Sci grad, I had stuck with Mechanical Engineering because the only computer degrees available to me were either Electronics Engineer (none of my childhood transistor electronics projects ever worked) or Business Programming (meaning COBOL which I had learned on my own in high school and had no interest in making a career of).

I did manage to pass the interview, perhaps because they wanted a software engineer that didn’t mind doing behind-the-scenes, not going to win any awards, programming tasks, like creating the Pascal compiler run-time libraries written in Intel 8080 assembler. ( I worked under a female Stanford MS degree’d compiler designer from Puerto Rico, that suffered from double chips on her shoulders, but I was finally being paid to write software and loving it.)

The wanted me to up my game by taking a newly initiated “Stanford Univ. Remote Learning Master’s Degree” course, which included listening over the phone to twice weekly lectures, looking at emailed lecture slides, and mailing in tests taken in a private room monitored by a supervisor. I only barely passed that course, but continued on to locally taught, equally challenging, “Analysis Of Algorithms” and “Introduction to Artificial Intelligence” masters coursework. I gave up trying to achieve status in the HP “Development Lab” when my reviews came in “slow” and “below average.” Their average was “only” idiot-savant to Nobel Prize nominee. I took a big raise following one of them to a startup to write laser-disk controller software, which put me in classes on error detection/correction with finite math. Talk about jumping from the pot into the fire.

I never did finish my degree, because I learned I could make more studying on my own to be a “one-step ahead expert” in my team, AND changing employers with the latest technology buzzwords at the top of my resume.

I love taking classes, but only as “Audit”, and only in-person for me.

2 Likes

That is actually not complete - the transitions execute the action code, the states execute the decision code.

Finite State Machines have these in-between virtual things that actually implement the code “on_transition_a2b()” and “on_entry_state_b()” such that “state” and “transition” are only variables.

Some FSM engines even have “on_exit_state_a()” and “preconditions_state_a[if_this, if_that]” which always messes me up. (These funcs allow combining several related states into one state, and confusing me further.)

Often the reality is a “State” doesn’t do anything, only things that don’t appear on the drawing as objects actually execute code. e.g. A “Driving” on_state_entry decides when to transition to a “Stopped” state, and the “Driving to Stopped” on_transition does the stopping action.

2 Likes

Thanks - I’m at the level of having heard of state machines. So it’s pretty much all Greek to me :slight_smile:
/K

2 Likes

State machines can be weird.
:wink:

2 Likes

I remember seeing some simple state diagrams for [something I don’t remember] awhile ago, back in the '70’s or '80’s and there were about six or so different states, with definite entry and exit criteria, (if you are at state “X” and [this] happens, go to state “Y”) - it was simple and very deterministic.

Nowadays state machines can be as complicated as an “elegant” program in C.  (i.e.  Egyptian Hieroglyphics).

  • If you leave state “X” because of [this], but you didn’t say “Mother, May I?” before leaving, you go to state “Q”, otherwise you go to state “Y”.
  • That is, unless it’s Monday, or it’s snowing, then you loop back to state “X” while sending a re-entrant get_animation_frame() message to states “Q”, “Y”, and “F” apologizing for not visiting.
  • HOWEVER!  If it’s snowing on a Monday in July AND you are in the Northern Hemisphere, you go to jail, go directly to jail, do NOT pass “GO” and do NOT collect $200!

:man_facepalming:

2 Likes

The team that created the python-statemachine package responded to my query explaining their intent was for “pure statemachines” and what I am describing is an Automata so look elsewhere.

I’ve got a design pattern in mind that will allow using this package for an event driven finite state machine. Additionally, I figured out how to package an FSM plotter, passing the module name and state machine class name.

drive.DriveBehavior.png:
drive.DriveBehavior.png

// drive.DriveBehavior
digraph {
	Driving -> Obstacle [label=d2o]
	Driving -> Exit [label=quit]
	Driving -> Driving [label=cycle]
	Exit -> Exit [label=cycle]
	Obstacle -> Waiting [label=o2w]
	Obstacle -> Exit [label=quit]
	Obstacle -> Obstacle [label=cycle]
	Startup -> Waiting [label=s2w]
	Startup -> Exit [label=quit]
	Startup -> Startup [label=cycle]
	Waiting -> Driving [label=w2d]
	Waiting -> Obstacle [label=w2o]
	Waiting -> Exit [label=quit]
	Waiting -> Waiting [label=cycle]
}

and this is the drive.py module:

"""
FILE:  drive.py
PURPOSE:  Demonstrate handling events and limiting cycle rate
REQUIRES:  python-statemachine
	   (pip3 install python-statemachine)
IMPLEMENTS:
 - waiting state: accepts a drive_request if no obstacle seen
 - driving state: stops driving if obstacle seen
 - all states: if exit_flag is set transition to exit state
 - main: simulates drive_request every 5 cycles
 - main: simulates obstacle every 10 cycles
 - main: simulates exit_flag after 60 cycles
 - main: exits after 65 cycles
"""
from statemachine import StateMachine, State
import logging
from time import sleep


NOTHING = 3000
distance_reading = NOTHING
OBSTACLE = 50
states_per_second = 1
exit_flag = False
drive_request = 0
motor_speed = 0

class DriveBehavior(StateMachine):
    "State Machine Example"

    # States
    startup    = State('Startup', initial = True)
    waiting    = State('Waiting')
    driving    = State('Driving')
    obstacle   = State('Obstacle')
    exit       = State('Exit')


    # Transitions
    s2w      = startup.to(waiting)
    w2d      = waiting.to(driving)
    w2o      = waiting.to(obstacle)
    d2o      = driving.to(obstacle)
    o2w      = obstacle.to(waiting)
    quit     = startup.to(exit) | waiting.to(exit) | driving.to(exit) | obstacle.to(exit)

    cycle = startup.to.itself() | waiting.to.itself() | driving.to.itself() | obstacle.to.itself() | exit.to.itself()

    # on_transitions
    def on_s2w(self):
        logging.info("transition")

    def on_w2o(self):
        logging.info("transition")

    def on_w2d(self):
        logging.info("transition accepted drive_request: {}".format(drive_request))
        motor_speed = drive_request

    def on_d2o(self):
        logging.info("transition stopping")
        motor_speed = 0

    def on_o2w(self):
        logging.info("transition")

    def on_cycle(self):
        logging.info("null transition")


    # entering states  (event detectors)

    def on_enter_startup(self):
        logging.info("checking events - ef:{} dist:{} req:{}".format(exit_flag, distance_reading, drive_request))
        if exit_flag:
            self.quit()
        else:
            self.s2w()  # auto transition

    def on_enter_waiting(self):
        logging.info("checking events - ef:{} dist:{} req:{}".format(exit_flag, distance_reading, drive_request))
        if exit_flag:
            self.quit()
        elif distance_reading <= OBSTACLE:    # should not drive?
            self.w2o()
        elif drive_request > 0:    # drive wanted?
            self.w2d()

    def on_enter_driving(self):
        logging.info("checking events - ef:{} dist:{} req:{}".format(exit_flag, distance_reading, drive_request))
        if exit_flag:
            self.quit()
        elif distance_reading <= OBSTACLE:
            self.d2o()

    def on_enter_obstacle(self):
        logging.info("checking events - ef:{} dist:{} req:{}".format(exit_flag, distance_reading, drive_request))
        if exit_flag:
            self.quit()
        elif distance_reading > OBSTACLE:
            self.o2w()  # go back to waiting

    def on_enter_exit(self):
        logging.info("wrapping up - ef:{} dist:{} req:{}".format(exit_flag, distance_reading, drive_request))

This is the test main that runs the statemachine and simulates distance readings, drive requests, and exit_flag shutdown requests:

def main():
    global drive_request, distance_reading, exit_flag

    try:
	    logging.basicConfig(level=logging.INFO, format='%(asctime)s %(funcName)s: %(message)s')
	    drive_behavior = DriveBehavior()

	    state_count = 0

	    while True:
	        logging.info("cycle: {}".format(state_count))
	        drive_behavior.cycle()
	        state_count += 1
	        sleep(1/states_per_second)
	        if  (state_count % 5) == 0:
	            drive_request = 10
	        else:
	            drive_request = 0

	        if (state_count % 10) == 0:
	            distance_reading = OBSTACLE
	        else:
	            distance_reading = NOTHING

	        if state_count == 60:
	            exit_flag = True

	        if state_count == 65:
	            exit()
    except KeyboardInterrupt:
        print("")
        logging.info("Cntrl-C: Exiting")

if __name__ == "__main__":
   main()

This is the first part of the log output (# annotations added):

2022-05-29 14:39:49,087 main: cycle: 0

# null transition because FSM starts in the initial state "startup" so was no on_enter_startup yet
2022-05-29 14:39:49,088 on_cycle: null transition

# "cycle" null transition causes startup state "entry" - it auto transitions to waiting state
2022-05-29 14:39:49,089 on_enter_startup: checking events - ef:False dist:3000 req:0
2022-05-29 14:39:49,089 on_s2w: transition
2022-05-29 14:39:49,090 on_enter_waiting: checking events - ef:False dist:3000 req:0
2022-05-29 14:39:50,091 main: cycle: 1
2022-05-29 14:39:50,092 on_cycle: null transition
2022-05-29 14:39:50,092 on_enter_waiting: checking events - ef:False dist:3000 req:0

# cycle (null) transition on waiting state checks for obstacles and checks for drive requests
2022-05-29 14:39:51,094 main: cycle: 2
2022-05-29 14:39:51,095 on_cycle: null transition
2022-05-29 14:39:51,095 on_enter_waiting: checking events - ef:False dist:3000 req:0

# again cycles waiting for distance < 50 or drive request > 0
2022-05-29 14:39:52,096 main: cycle: 3
2022-05-29 14:39:52,097 on_cycle: null transition
2022-05-29 14:39:52,097 on_enter_waiting: checking events - ef:False dist:3000 req:0

# again
2022-05-29 14:39:53,099 main: cycle: 4
2022-05-29 14:39:53,100 on_cycle: null transition
2022-05-29 14:39:53,101 on_enter_waiting: checking events - ef:False dist:3000 req:0

# and again, but this time there is a drive request! so executes transition waiting2drive w2d
2022-05-29 14:39:54,102 main: cycle: 5
2022-05-29 14:39:54,103 on_cycle: null transition
2022-05-29 14:39:54,103 on_enter_waiting: checking events - ef:False dist:3000 req:10
2022-05-29 14:39:54,104 on_w2d: transition accepted drive_request: 10

# entering driving state causes check for obstacle  (none) so no further transitions this cycle
2022-05-29 14:39:54,104 on_enter_driving: checking events - ef:False dist:3000 req:10

# in driving state, cycle causes check for obstacle - none yet
2022-05-29 14:39:55,106 main: cycle: 6
2022-05-29 14:39:55,107 on_cycle: null transition
2022-05-29 14:39:55,108 on_enter_driving: checking events - ef:False dist:3000 req:0

# in driving state, cycle causes check for obstacle - none yet
2022-05-29 14:39:56,109 main: cycle: 7
2022-05-29 14:39:56,110 on_cycle: null transition
2022-05-29 14:39:56,110 on_enter_driving: checking events - ef:False dist:3000 req:0

# still in driving state, cycle causes check for obstacle - none yet
2022-05-29 14:39:57,112 main: cycle: 8
2022-05-29 14:39:57,113 on_cycle: null transition
2022-05-29 14:39:57,114 on_enter_driving: checking events - ef:False dist:3000 req:0

# still in driving state, cycle causes check for obstacle - none yet
2022-05-29 14:39:58,115 main: cycle: 9
2022-05-29 14:39:58,122 on_cycle: null transition
2022-05-29 14:39:58,123 on_enter_driving: checking events - ef:False dist:3000 req:0

# still in driving state, cycle causes distance check -  possible obstacle.  Transition to obstacle state
2022-05-29 14:39:59,125 main: cycle: 10
2022-05-29 14:39:59,126 on_cycle: null transition
2022-05-29 14:39:59,126 on_enter_driving: checking events - ef:False dist:50 req:10
2022-05-29 14:39:59,127 on_d2o: transition stopping
2022-05-29 14:39:59,127 on_enter_obstacle: checking events - ef:False dist:50 req:10

# in obstacle state, distance check shows obstacle no longer present.  transition to waiting again.
2022-05-29 14:40:00,128 main: cycle: 11
2022-05-29 14:40:00,129 on_cycle: null transition
2022-05-29 14:40:00,130 on_enter_obstacle: checking events - ef:False dist:3000 req:0
2022-05-29 14:40:00,131 on_o2w: transition
2022-05-29 14:40:00,131 on_enter_waiting: checking events - ef:False dist:3000 req:0

...continues till exit_flag is True, then transitions to exit state from any current state
1 Like

I did my usual searching and figured out how to import a module knowing the module name as a string variable. Next was to figure out how to instantiate an object from a class name in a string variable.

Search, search but I didn’t seem to be getting anywhere, so I posted my question on StackExchange. Someone immediately closed my question as a duplicate, and dinged me -2 reputation points.

In the meantime I had actually figured it out and was going to answer my own question with the two line answer.

I took a read through the ten pages that supposedly was the fount of knowledge that I was supposed to have found and understood rather than asking my succinct question, and decided once more there are some arrogant geniuses on Stack, and I should stay away from there.

2 Likes

There is a cottage industry in computer science education/research with a taxonomy of state machine based things, all expressed as mathematical statements of Greek alphabet symbols.

In my usual robotic naivety, I jumped into an ongoing debate on robots and state machines, with terminology and distinguishing design rules for each researcher’s machine type.

I like to hear what researchers think, but mostly what I end up hearing from each is “You are asking the wrong question. What is interesting is <this thing I am doing right now>”, leaving me to answer my own question since I am the only one interested. I end up mutilating all the rules to get something that is useful at the moment, but deeply flawed in some way I don’t foresee and the researchers all learned long ago.

2 Likes

Yeah, but they probably learned the hard way too…
/K

2 Likes

Genius is marked by graciousness.

Illegitimate anal orifaces like the people, (and I am using that loosely), on Stack are most definitely NOT geniuses, despite their over-inflated egos.

There are geniuses there, but they are overshadowed by the Richards that infest the place.
:face_with_symbols_over_mouth:
:man_facepalming:

2 Likes

Einstein is credited with two quotes that are directly on point:

  • There are no “stupid” questions, only stupid, (ill-considered), answers.

  • If you can’t explain something to a six-year-old, you don’t understand it either.

2 Likes