Closed keivanzavari closed 4 years ago
I think I have figured out what the problem is (correct me if I am wrong please). Once you create a state with children (sub-state) as
self._running = RunningStateMachine()
{"name": "running", "children": self._running},
a copy of the sub-state is created. That's why machine._running
and machine
are in different states. Copying is fine by me, but the drawback is the following:
If there are callbacks inside the sub-state who are supposed to do stuff and then trigger events, they cannot do their job anymore. I can create a transition in the parent state as
self.add_transition("e_start_chain", "running_init", "running_configuration")
and trigger this event immediately after the call of e_run()
. So I would do
machine.e_run() # machine state = running_init
machine.e_start_chain() # machine state = running_configuration
However, since configuration method in RunningStateMachine
class has self.e_success_config()
, triggering this event raises an error because the sub-state defined by machine._running
is obviously in init
state and I get the error of Can't trigger event e_success_config from state init!
. But triggering machine.e_success_config()
does the job.
So what is the right way to be able to fully reuse a machine as a sub-state of another? Does my parent machine need to inherit from the sub-state?
P.S. If the sub-state is copied, how deep does this copy go? Until it reaches the leaves? Or does it stop somewhere?
Hello @keivanzavari,
To my experience, nesting state machines is very useful as you can reuse the states you have written before. Besides as your state machine grows, it helps to keep things more modular. So no state becomes huge and difficult to read/understand.
totally agree
I think I have figured out what the problem is (correct me if I am wrong please). Once you create a state with children (sub-state) [...] a copy of the sub-state is created.
yes, that's mentioned in the last paragraph of the reuse section:
Note that the HierarchicalMachine will not integrate the machine instance itself but the states and transitions by creating copies of them. This way you are able to continue using your previously created instance without interfering with the embedded version.
If the sub-state is copied, how deep does this copy go?
All states and substates will be integrated into the new Machine
So what is the right way to be able to fully reuse a machine as a sub-state of another?
I cannot claim this is the 'right' way because using machines in machines is a common practice for realizing HSMs afaik. So your initial thoughts are pretty much in line with how some state machine frameworks work. However, transitions
' philosophy differs a bit here: we'd like to consider Machine
instances to be the 'rulebooks' which defines transitions and configurations whereas the actual state-dependent behavior is part of the model.
Here is a rather abstract example about how to organized behavior and transitions in models and machines. More explanation below:
from transitions.extensions.factory import HierarchicalGraphMachine as HSM
# define a basic model; useful for generic error handling
class BaseBehaviour:
def emergency_shutdown(self):
print('something is wrong!')
# define a task-specific behaviour which will act as a model;
# such a model will contain all related callbacks
class BehaviourA(BaseBehaviour):
def do_A(self):
print('doing A')
def prepare_A(self):
print('prepare A')
# define another independent model
class BehaviourB(BaseBehaviour):
def do_B(self):
print('doing B')
# extend the previously defined behaviour
class ImprovedBehaviourA(BehaviourA):
def do_A(self):
super(ImprovedBehaviourA, self).do_A()
print('post process A')
# the final model will be a combination of the previously defined models
class Agent(ImprovedBehaviourA, BehaviourB):
pass
states_A = ['initial', 'A', 'done']
states_B = ['initial', 'B', 'done']
transitions_A = [{'trigger': 'do', 'source': 'initial', 'dest': 'A'},
{'trigger': 'do', 'source': 'A', 'dest': 'done', 'before': 'do_A'}]
transitions_B = [{'trigger': 'do', 'source': 'initial', 'dest': 'B'},
{'trigger': 'do', 'source': 'B', 'dest': 'done', 'before': 'do_B'}]
# initialized the individual models and pass them to their related machines
behaviourA = BehaviourA()
machine_A = HSM(model=behaviourA, states=states_A, transitions=transitions_A, initial='initial', title='Behaviour A',
auto_transitions=False)
behaviourB = BehaviourB()
machine_B = HSM(model=behaviourB, states=states_B, transitions=transitions_B, initial='initial', title='Behaviour B',
auto_transitions=False)
# stitch everything together
states_agent = ['initial',
{'name': 'behaviourA', 'children': machine_A,
'remap': {'done': 'initial'}},
{'name': 'behaviourB', 'children': machine_B,
'remap': {'done': 'initial'}}]
transitions_agent = [['initA', 'initial', 'behaviourA'],
['initB', 'initial', 'behaviourB']]
agent = Agent()
machine_agent = HSM(model=agent, states=states_agent, transitions=transitions_agent, initial='initial', title='Agent',
auto_transitions=False)
# Graphviz plots of all rather simple state machines. See below.
behaviourA.get_graph().draw('behaviourA.png', prog='dot')
behaviourB.get_graph().draw('behaviourB.png', prog='dot')
agent.get_graph().draw('agent.png', prog='dot')
assert agent.state == 'initial'
agent.initA()
assert agent.state == 'behaviourA_initial'
agent.do()
assert agent.state == 'behaviourA_A'
agent.do()
assert agent.state == 'initial'
agent.initB()
agent.do()
agent.do()
assert agent.state == 'initial'
Separation of rules and behaviour -- models and states/transitions can be used independently of each other
Trigger events in callbacks -- events in submodules will be processed, can be overridden when necessary and will 'dispatch' to their parent in case they cannot be processed in the current context. This is handy when a specific event might end a subroutine but this event is not specified in the subroutine itself. See for instance the 'relax' event in the example notebook which exits the nested state independently of the current substate.
Namespace collision -- As everything will end up in a (set of) macro models, methods with the same name will be overridden by inheritance. This can be desired for specialization but may also cause conflicts when generic callback names (e.g. 'on_event' ) are frequently used in submodels and transitions.
onenteron_enter_stateX
in submodels will not work anylonger when state names changes due to nesting.
Thanks for the answer, much appreciated.
In the meantime, I changed the implementation to just one machine that has all the states. So in the example above RunningStateMachine
is incorporated into the parent state machine.
If my state machine grows, I'll have to find ways to fix this. I have previously used rFSM and made quite complicated state machines. There it's very easy to load another state machine without having to know what's inside of it like here.
So I would very much like to use some smaller states as a sort of plug and play modules and do not have to tweak with them. If I understand you correctly, I'll have to load each state as a separate model, right?
So for my example above it would be separate add_model
calls. What if one state machine needs to use another?
There it's very easy to load another state machine without having to know what's inside of it [...] So I would very much like to use some smaller states as a sort of plug and play modules and do not have to tweak with them.
I see the appeal of that. I'd assume it can be done with transitions
as well. Since callbacks can also be function/method references instead of strings. The degree of isolation depends on how the module is defined:
class BehaviorA:
def __init__(self):
states = [{'name': 'A', 'on_enter': self.do_something}, ...]
transitions = [{'trigger':'foo', ..., 'prepare': self.prepare,
'conditions': 'model_callback'}, ...]
# initialized machine without model
self.machine = Machine(model=None, states=states,
transitions=transitions, initial=...)
However, it might not be as convenient as it should be. Feel free to give feedback about your experience and how to improve the handling of nested states.
If I understand you correctly, I'll have to load each state as a separate model, right?
The states and transitions are defined for the machine. However, the model is the actual 'stateful' object and contains the state- and transition-related callbacks. I'd suggest to isolate context-specific behaviour into a machine-model unit.
I created a feature draft based on this discussion and will close this issue for now. Thank you for your feedback! If you have a good test case for convenient nesting (with possible corner cases), feel free to post it here or in the issue mentioned above.
Hi I have been looking at closed issues, SO, and googling to solve this issue. But I haven't been able to solve my problem and this seems to be the only way to describe it and hope to get an answer. I am making a state machine which can include several substates which are also all state machines. So this basically boils down to reusing HSM according to readme.
my highest level SM looks like this:
As you see a state machine with three states
init
,running
andstop
. Once the evente_run()
is sent via something likemachine transitions to
running
state. _I do it in an indirect way because I wanted things to happen automatically.e_run()
transitions torunning
and afterrun_machine
callsinitialize
method of running class which fires an event to start up the chain of events. Below I showrunning
and that clears thing up._So the running state is defined as
which similar to its parent, is composed of a few states and a few substates. I have also enabled logging to see which states I enter and exit. To my experience, nesting state machines is very useful as you can reuse the states you have written before. Besides as your state machine grows, it helps to keep things more modular. So no state becomes huge and difficult to read/understand.
So the unusual behavior is that when
e_run()
is called I get prints ofAs you see
while
I can of course move the transition definitions to the parent state, but that's unhandy. I cannot do that for all sub-states. Obviously, I want each substate to be responsible for it's own behavior. What is the common practice? Is this a bug or intended behavior?