pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.68k stars 530 forks source link

Checking possible transitions #424

Closed Panos26 closed 4 years ago

Panos26 commented 4 years ago

I am using the transitions module in ROS environment where the "inputs" will be from messages so I am not the one that controls the machine. I want it to check every possible transition every time it gets new data. I tried using the get_triggers but since it returns a list of strings I can't use the possible transitions from there. The solution I've come up with is this:

states = ['Off','Manual','Ready']
transitions = [
            { 'trigger': 'off_2_manual', 'source': 'Off', 'dest': 'Manual', 'conditions': 'trans_1'},
            { 'trigger': 'manual_2_off', 'source': 'Manual', 'dest': 'Off', 'conditions': 'trans_2'},
            { 'trigger': 'off_2_ready', 'source': 'Off', 'dest': 'Ready', 'conditions': 'trans_3'},
            { 'trigger': 'ready_2_off', 'source': 'Ready', 'dest': 'Off', 'conditions': 'trans_4'}
  ]

self.machine  = Machine(model=self, states=self.states,transitions=self.transitions, initial='Off')
self.check_for_transitions()

def check_for_transitions(self):
            try: self.manual_2_off()
            except: pass
            try: self.manual_2_off()
            except: pass
            try: self.off_2_ready()
            except: pass
            try: self.ready_2_off()
            except: pass

Is there a better way? *PS VScode keeps underlining self.manyal_2_off and self._transition_ saying instance of class has no such member but it works fine...is this a problem?

Thanks for the great package

potens1 commented 4 years ago

Hello, I was wondering, why do you have to check the possible transitions instead of triggering it. I mean, it sounds to me that you are doing yourself the state machine there instead of using it. If your trigger is coming from message, then, you just to have a mapping between the original input and the triggers, and then pass the string the the fsm.

i.e.: with something like that: the choice of command is random, but it works and does not crash the program. I've disable the conditions since I wanted to keep it short (kinda).

from transitions import Machine, MachineError
import random

class MyRobot:
    states = ["Off", "Manual", "Ready"]
    transitions = [
        {
            "trigger": "off_2_manual",
            "source": "Off",
            "dest": "Manual",
            # "conditions": "trans_1",
        },
        {
            "trigger": "manual_2_off",
            "source": "Manual",
            "dest": "Off",
            # "conditions": "trans_2",
        },
        {
            "trigger": "off_2_ready",
            "source": "Off",
            "dest": "Ready",
            # "conditions": "trans_3",
        },
        {
            "trigger": "ready_2_off",
            "source": "Ready",
            "dest": "Off",
            # "conditions": "trans_4",
        },
    ]

    def __init__(self):
        self.machine = Machine(
            model=self,
            states=self.states,
            transitions=self.transitions,
            initial="Off",
        )

    def process_input(self, input_data):
        map_cmd = {
            'foo': 'off_2_manual',
            'bar': 'manual_2_off',
            'crux': 'off_2_ready',
            'baz': 'ready_2_off'
        }
        try:
            self.trigger(map_cmd[input_data])
        except (MachineError, KeyError):
            print("Nope, that's an invalid command")

if __name__ == "__main__":
    walle = MyRobot()
    cmds = ['foo', 'bar', 'crux', 'baz']
    for cpt in range(10):
        walle.process_input(random.choice(cmds))
        print(walle.state)
    # a last one that does not exist at all
    walle.process_input('grow')

I hope it helps, and I don't miss the point.

ykurtbas commented 4 years ago

You could use the get transitions from the machine but it is busted if you do not specify a trigger.

states=['solid', 'liquid', 'gas', 'plasma']
transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
]
machine = Machine(lump, states=states, transitions=transitions, initial='liquid')

From the Machine

    def get_transitions(self, trigger="", source="*", dest="*"):
        """ Return the transitions from the Machine.
        Args:
            trigger (str): Trigger name of the transition.
            source (str): Limits removal to transitions from a certain state.
            dest (str): Limits removal to transitions to a certain state.
        """
machine.get_transitions(source='liquid')

You would expect to see Transition('luquid', 'gas')

But the extending and chaining of transitions are causing non-sense results

machine.get_transitions(source='liquid')
[<Transition('liquid', 'solid')@4584687888>, <Transition('liquid', 'liquid')@4584688912>, <Transition('liquid', 'gas')@4584776912>, <Transition('liquid', 'plasma')@4584777616>, <Transition('liquid', 'gas')@4584777936>]

now all of a sudden we can transform from liquid -> solid

:man_shrugging: :confused:

I might code up a little extension to find the available states from a given state with simple statements, better than triggering things to 'test'

ykurtbas commented 4 years ago

I work with enums so my method is a bit longer but since you are working with strings this should work with one line python magic.

     transitions = [
        ['a2b', 'a', 'b'],
        ['b2c', 'b', 'c'],
        ['c2d', 'c', 'd'],
        ['restart', ['a', 'b', 'c'], 'a'],
        ['shortcut', 'a', 'd'],
    ]

    def get_available_transitions(self, src) -> []:
        return [i[0] for i in list(filter(lambda x: src in x[1], self.transitions))]

running

print(ob.get_available_transitions('a'))
['a2b', 'restart', 'shortcut']

print(ob.get_available_transitions('b'))
['b2c', 'restart']

print(ob.get_available_transitions('d'))
[]
ykurtbas commented 4 years ago

from the documentation btw

If you need to know which transitions are valid from a certain state, you can use _gettriggers:

aleneum commented 4 years ago

I want it to check every possible transition every time it gets new data ... I tried using the get_triggers but since it returns a list of strings I can't use the possible transitions from there.

why not? you could get the trigger with getattr from your model (or more convenient: lump.trigger(<trigger_name>). This method is also added to the model. It seems like this hasn't been added to the Readme.md yet) and call it:

from transitions import Machine

class Lump:
    pass

states = ['solid', 'liquid', 'gas', 'plasma']
transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
]

lump = Lump()
machine = Machine(lump, states=states, transitions=transitions, initial='liquid')
triggers = machine.get_triggers(lump.state)
print('possible transitions: {}'.format(triggers))  # >>> possible transitions: ['to_solid', 'to_liquid', 'to_gas', 'to_plasma', 'evaporate']
getattr(lump, triggers[0])()
# or lump.trigger(triggers[0])
print(lump.state)  # >>> solid

... You would expect to see Transition('luquid', 'gas') ... But the extending and chaining of transitions are causing non-sense results

I am not completely certain what you refer to with 'chaining and extending' but the result is fine. As you see in the example above auto transitions are also valid transitions which will be returned by get_transitions since they are valid (see Stackoverflow).

So I would expect 1 (evaporate) plus 4 (auto transitions) equals 5 transitions:

trans_list = machine.get_transitions(source=lump.state)
print('There are {} possible transitions'.format(len(trans_list)))  # >>> There are 5 possible transitions

If you do not want to use auto transitions you can disable them by passing auto_transitions=False to Machine. Furthermore, get_triggers will not evaluate conditions. If you want to check whether a trigger is actually possible, you might want to have a look here.

aleneum commented 4 years ago

Since there is no feedback, I assume this is solved or not relevant anylonger. I will close this issue for now. Feel free to comment though. If a feature request or bug report arises, I will gladly reopen the issue.