[Documentation] [TitleIndex] [WordIndex

API review

Proposer: Jonathan Bohren

Present at review:

Question / concerns / comments

SMACH Construction API

The goals of the SMACH construction API are:

  1. Make constructing SMACH trees as easy as possible
  2. Suppress the likelihood errors in tree specification
  3. Maintain readability of the construction calls
  4. Maintain the flexibility with which states can be moved around

Current "pre-0.2.0" API

The "pre-0.2.0" API is the API that the pr2_plugs branch and pr2_doors use. It has several features which distinguish it from the 0.1.0 API. These include:

This first-pass API attempted to make it easy to nest states on construction, so that the code clearly showed the structure of the SMACH tree at a glance. This was accomplished by making the add() methods of the container return themselves.

While good-intentioned, this resulted in a mess of parentheses and braces that would only serve to confuse someone into thinking they were writing LISP code. All containers still needed to be constructed out-of-line (like at the beginning of the function scope). The type of information being added and stored in various places is good, but this does not accomplish the API syntax goals.

The following is a contrived hierarchical state machine written with the "pre-0.2.0" API.

   1   # Construct containers
   2   sm = StateMachine(['aborted','preempted'])
   3   sm2 = StateMachine(['done'])
   4   sm3 = StateMachine(['done'])
   5   sm4 = StateMachine(['done'])
   6   sm5 = StateMachine(['done'])
   7 
   8   # Fill split container
   9   con_split = ConcurrentSplit(default_outcome = 'succeeded')
  10   con_split.add(
  11       ('SETTER', Setter()),
  12       ('RADICAL',
  13         sm5.add(
  14           ('T6',SPAState(),
  15             { 'succeeded':'SETTER',
  16               'aborted':'T6',
  17               'preempted':'SETTER'}),
  18           ('SETTER',Setter(),
  19             { 'done':'done'}) ) ),
  20       ('GETTER', Getter()) )
  21   con_split.add_outcome_map(({'SETTER':'done'},'succeeded'))
  22 
  23   # Fill root tree
  24   sm.add(
  25       state_machine.sequence('done',
  26         ('GETTER1', Getter(), {}),
  27         ('S2',
  28           sm2.add(
  29             ('SETTER', Setter(), {'done':'A_SPLIT'}),
  30             ('A_SPLIT', con_split, {'succeeded':'done'}) ), {} ),
  31         ('S3',
  32           sm3.add(
  33             ('SETTER', Setter(), {'done':'RADICAL'}),
  34             ('RADICAL',
  35               sm4.add(
  36                 ('T5',SPAState(),
  37                   { 'succeeded':'SETTER',
  38                     'aborted':'T5',
  39                     'preempted':'SETTER'}),
  40                   ('SETTER',Setter(),{'done':'done'}) ),
  41               {'done':'SETTER2'} ),
  42             ('SETTER2', Setter(), {'done':'done'}) ),
  43           {'done':'TRINARY!'} ) ) )
  44 
  45   sm.add(('TRINARY!', SPAState(),
  46       {'succeeded':'T2','aborted':'T3','preempted':'T4'}))
  47   sm.add(
  48       state_machine.sequence('succeeded',
  49         ('T2',SPAState(),{}),
  50         ('T3',SPAState(),{'aborted':'S2'}),
  51         ('T4',SPAState(),{'succeeded':'GETTER2','aborted':'TRINARY!'}) ) )
  52 
  53   sm.add(('GETTER2', Getter(), {'done':'GETTER1'}))
  54 
  55   # Set default initial states
  56   sm.set_initial_state(['GETTER1'],smach.UserData())
  57   sm2.set_initial_state(['SETTER'],smach.UserData())
  58   sm3.set_initial_state(['SETTER'],smach.UserData())
  59   sm4.set_initial_state(['T5'],smach.UserData())
  60   sm5.set_initial_state(['T6'],smach.UserData())

Some of the most confusing parts of the "pre-0.2.0" API are also the most useful. Convenient macros that do things like automatically connect up a bunch of states, or apply a set of transitions to a group of states, for example. These macros are just static functions that take in some args and a list of state specifications and output a corresponding list of modified state specifications.

Any structure that may be apparent if the state machine construction code is written like this is lost if the writer does not obey strict indentation style. Not only that, but they are not required to use it at all; the API allows SMACH trees to be constructed in many different, unreadable ways.

Proposed SMACH 0.2.0 API

A new API (along with several predacessors) has been designed to address these syntactical issues. The proposed API uses Python's context managers to keep track of the mode in which states are added to containers. This provides the readability and clearness while adding well-defined context management for catching errors in specification.

The main idea is to treat the containers themselves, or methods called on the containers as context managers. They enable the user to add states to the containers only within that block. With this API you can do things like:

Simply add states to a single state machine

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 sm0.set_initial_state(['FOO'])
   3 
   4 with sm0:
   5   # Add some states
   6   sm0.add('FOO',SPAState(...),{'succeeded':'BAR','aborted':'failed'})
   7   sm0.add('BAR',SPAState(...),{'succeeded':'done','aborted':'FOO'})

Nest two state machines

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 sm1 = StateMachine(['done','failed','preempted'])
   3 
   4 sm0.set_initial_state(['FOO'])
   5 sm1.set_initial_state(['DEEP'])
   6 
   7 with sm0:
   8   # Add some states
   9   sm0.add('FOO',SPAState(...),{'succeeded':'BAR','aborted':'failed'})
  10   sm0.add('BAR',SPAState(...),{'succeeded':'BAZ','aborted':'FOO'})
  11   sm0.add('BAZ',sm1,{'succeeded':'done','aborted':'BAR'})
  12   
  13   # Open the nested state machine
  14   with sm1:
  15     sm1.add('DEEP',SPAState(...),{'aborted':'DEEP'})

Nest two state machines, constructing the nested one inline

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 
   3 with sm0:
   4   sm0.set_initial_state(['FOO'])
   5 
   6   sm0.add('FOO',SPAState(...),{'succeeded':'BAR','aborted':'failed'})
   7   sm0.add('BAR',SPAState(...),{'succeeded':'BAZ','aborted':'FOO'})
   8   
   9   # Create nested state machine
  10   sm1 = StateMachine(['done','failed','preempted'])
  11 
  12   # Add and open the nested state machine
  13   with sm0.add('BAZ',sm1,{'succeeded':'done','aborted':'BAR'}) as local_sm:
  14     local_sm.set_initial_state(['DEEP'])
  15 
  16     local_sm.add('DEEP',SPAState(...),{'aborted':'DEEP'})

Add states to a state machine such that each state's 'succeeded' label transitions to the next state

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 sm0.set_initial_state(['FOO'])
   3 
   4 with sm0.add_connected_by_outcome('succeeded'):
   5   # Add some states
   6   sm0.add('FOO',SPAState(...),{'aborted':'failed'})
   7   sm0.add('BAR',SPAState(...),{'aborted':'FOO'})
   8   sm0.add('BAZ',SPAState(...),{'succeeded':'done','aborted':'FOO'})

Contrived SMACH example

   1 sm0 = StateMachine(['aborted','preempted'])
   2 sm0.set_initial_state(['GETTER1'])
   3 
   4 sm1 = StateMachine(['done'])
   5 sm1.set_initial_state(['SETTER'])
   6 
   7 sm2 = StateMachine(['done'])
   8 sm2.set_initial_state(['SETTER'])
   9 
  10 sm3 = StateMachine(['done'])
  11 sm3.set_initial_state(['T5'])
  12 
  13 sm4 = StateMachine(['done'])
  14 sm4.set_initial_state(['T6'])
  15 
  16 cs1 = ConcurrentSplit(['succeeded'])
  17 
  18 with sm0.add_connected_by_outcome('done'):
  19   sm0.add('GETTER1', Getter())
  20   
  21   with sm0.add('S2',sm1):
  22     sm1.set_initial_state(['SETTER'])
  23     sm1.add('SETTER', Setter(), {'done':'A_SPLIT'})
  24 
  25     with sm1.add('A_SPLIT',cs1):
  26       cs1.add_outcome_map(({'SETTER':'done'},'succeeded'))
  27 
  28       cs1.add('SETTER',Setter())
  29       cs1.add('GETTER',Getter())
  30       
  31       with cs1.add('RADICAL',sm4):
  32         sm4.add('T6', SPAState(),
  33           { 'succeeded':'SETTER',
  34             'aborted':'T6',
  35             'preempted':'SETTER'})
  36         sm4.add('SETTER',Setter(), { 'done':'done'})
  37   
  38   with sm0.add('S3',sm2,{'done':'TRINARY!'}):
  39     sm2.add('SETTER', Setter(), {'done':'RADICAL'})
  40     sm2.set_initial_state(['SETTER'])
  41 
  42     with sm2.add('RADICAL',sm3,{'done':'SETTER2'}):
  43       sm3.set_initial_state(['T5'])
  44       sm3.add('T5',SPAState(),
  45           { 'succeeded':'SETTER',
  46             'aborted':'T5',
  47             'preempted':'SETTER'})
  48       sm3.add('SETTER',Setter(),{'done':'done'})
  49 
  50     sm0.add('SETTER2', Setter(), {'done':'done'})
  51 
  52 with sm0.opened():
  53   sm0.add('TRINARY!',SPAState(),
  54       {'succeeded':'T2','aborted':'T3','preempted':'T4'})
  55 
  56 with sm0.connected_by_outcome('succeeded'):
  57   sm0.add('T2',SPAState(),{})
  58   sm0.add('T3',SPAState(),{'aborted':'S2'})
  59   sm0.add('T4',SPAState(),{'succeeded':'GETTER2','aborted':'TRINARY!'})

Meeting agenda

To be filled out by proposer based on comments gathered during API review period

Gil

Matei

Conclusion

Package status change mark change manifest)



2023-10-28 13:04