-
I have read the document and I don't think this is possible, but I thought I would check. I have a simple model with three states and four transitions.
If my model is in the 'MOWING' state, I would like to be able to request it to go to the 'STANDBY' state but have the machine (?) calculate the transition steps required (i.e. trigger 'PauseWork' and then 'CancelWork') and call the intermediate states. Is this possible? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 4 replies
-
Hello @Whytey, unfortunately, transitions does not have this feature built-in. This is a common graph theory task where import networkx as nx
from transitions.extensions import GraphMachine
states = ['STANDBY', 'MOWING', 'PAUSED']
transitions = [
{'trigger': 'StartMowing', 'source': 'STANDBY', 'dest': 'MOWING'},
{'trigger': 'PauseWork', 'source': 'MOWING', 'dest': 'PAUSED'},
{'trigger': 'ContinueWork', 'source': 'PAUSED', 'dest': 'MOWING'},
{'trigger': 'CancelWork', 'source': 'PAUSED', 'dest': 'STANDBY'},
]
class NetworkXMachine(GraphMachine):
def shortest_path(self, target):
graph = nx.drawing.nx_agraph.from_agraph(self.get_graph())
path = nx.shortest_path(graph, self.state, target)
# The shortest path will only contain nodes/states ['STANDBY', 'MOWING', ...] and thus we have to
# get the appropriate edges/transitions in an extra step. Luckily, networkx
# keeps the edge label which is equivalent to the trigger called to get from
# one state to the other
for trigger in (graph[u][v].get("label") for u, v in nx.utils.pairwise(path)):
print(f"Executing {trigger}...")
self.trigger(trigger)
machine = NetworkXMachine(states=states, transitions=transitions, initial="STANDBY")
machine.shortest_path("PAUSED")
assert machine.state == "PAUSED" This is a very simple example where there is only one transition between each state. This approach does not consider conditions either but assumes that all transitions will be possible. You might want to think about how to make use of 'weights' in case your path could look different depending on certain conditions. |
Beta Was this translation helpful? Give feedback.
-
Thanks, that seems very clever. I tried running a slightly modified version of your sample code but there seems to be an issue.
I have just installed the following two packages: Are these the appropriate versions to use for your module? Note, this is the digraph that was produced from the NetworkXMachine:
|
Beta Was this translation helpful? Give feedback.
-
From what I can tell it's the wrong combination of Graphviz output. I have used Variant A (using pygraphviz):import networkx as nx
import pygraphviz
import sys
import transitions
from transitions.extensions import GraphMachine
print(f"Python: {sys.version}") # >> Python: 3.12.3 | packaged by Anaconda, Inc. | (main, May 6 2024, 14:46:42) [Clang 14.0.6 ]
print(f"Networkx: {nx.__version__}") # >> Networkx: 3.2.1
print(f"PyGraphviz: {pygraphviz.__version__}") # >> PyGraphviz: 1.13
print(f"Transitions: {transitions.__version__}") # >> Transitions: 0.9.1
states = ['STANDBY', 'MOWING', 'PAUSED']
transitions = [
{'trigger': 'StartMowing', 'source': 'STANDBY', 'dest': 'MOWING'},
{'trigger': 'PauseWork', 'source': 'MOWING', 'dest': 'PAUSED'},
{'trigger': 'ContinueWork', 'source': 'PAUSED', 'dest': 'MOWING'},
{'trigger': 'CancelWork', 'source': 'PAUSED', 'dest': 'STANDBY'},
]
class NetworkXMachine(GraphMachine):
def shortest_path(self, target):
graph = nx.drawing.nx_agraph.from_agraph(self.get_graph())
path = nx.shortest_path(graph, self.state, target)
for trigger in (graph[u][v].get("label") for u, v in nx.utils.pairwise(path)):
print(f"Executing {trigger}...") # >> Executing StartMowing... >> Executing PauseWork...
self.trigger(trigger)
machine = NetworkXMachine(states=states, transitions=transitions, initial="STANDBY")
machine.shortest_path("PAUSED")
assert machine.state == "PAUSED" Variant B (using graphviz and pydot):import tempfile
import networkx as nx
import graphviz
from transitions.extensions import GraphMachine
print(f"Graphviz: {graphviz.__version__}") # >> Graphviz: 0.20.3
states = ['STANDBY', 'MOWING', 'PAUSED']
transitions = [
{'trigger': 'StartMowing', 'source': 'STANDBY', 'dest': 'MOWING'},
{'trigger': 'PauseWork', 'source': 'MOWING', 'dest': 'PAUSED'},
{'trigger': 'ContinueWork', 'source': 'PAUSED', 'dest': 'MOWING'},
{'trigger': 'CancelWork', 'source': 'PAUSED', 'dest': 'STANDBY'},
]
class NetworkXMachine(GraphMachine):
def shortest_path(self, target):
with tempfile.NamedTemporaryFile(mode="wt", delete_on_close=False) as fp:
fp.write(self.get_graph().source)
fp.close()
# using pydot here because if you use pygraphviz,
# you could also use variant A and
# nx.drawing.nx_agraph.from_agraph
graph = nx.drawing.nx_pydot.read_dot(fp.name)
path = nx.shortest_path(graph, self.state, target)
# the pydot graph seems to be slightly different organized and returns a dict of edges.
for trigger in (graph[u][v][0].get("label") for u, v in nx.utils.pairwise(path)):
print(f"Executing {trigger}...")
self.trigger(trigger)
# if you have `pygraphviz` and `graphviz` installed, you must tell transitions not to use `pygraphviz`
# by passing `use_pygraphviz=False`. Note that this will change in transitions 0.9.2 (current master)
# where `use_pygraphviz` is replaced with `graph_engine="pygraphviz|graphviz|mermaid"`
machine = NetworkXMachine(states=states, transitions=transitions, use_pygraphviz=False, initial="STANDBY")
machine.shortest_path("PAUSED")
assert machine.state == "PAUSED" |
Beta Was this translation helpful? Give feedback.
-
Thank you kindly. That all makes sense. I chose to utilise method B - I don't want to force my users to require the dependencies of graphviz (e.g. need to have the graphviz-dev package installed on Ubuntu) since I will be utilising the code in Home Assistant which tends to limit dependencies to python packages as much as possible. Once I have the integration up and running, I will link it here - for full completeness - but I think this question is resolved now. Thankyou. |
Beta Was this translation helpful? Give feedback.
I just checked the pydot documentation and found a way to process the dotstring directly: