pytransitions / transitions

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

Does this project have any automatic traversal of states? #576

Closed Kiskadee-dev closed 1 year ago

Kiskadee-dev commented 2 years ago

I was looking to find if this project had a feature similar to the automatic transitions but going through the intermediary states instead of warping to the final state

for example:

A -> B -> C -> D

So from A, would call something alike traverse_to_D() and receive either True or False if the target state was reached sucessfully, instead of throwing an error saying we can't access D from A, because we can if we manually reach each state using the triggers

Since the state machine ends up like a graph with complex conditions, it could benefit from having a pathfinding to simplify the proccess of traversal

the-moog commented 2 years ago

Looking at the source code for core.py, does what you are asking not already exist with the loop=True parameter to .add_ordered_transitions()?

e.g.

In [1]: 
from transitions.extensions import GraphMachine
class ABCD:
    pass

states = list(ABCD.__name__)

abcd_machine = GraphMachine(model=ABCD(),
                            states=states,
                            use_pygraphviz=False,  # pygraphviz does not render in Jupyter
                            initial = states[0])

abcd_machine.add_ordered_transitions(states, loop=False)
abcd_machine.get_graph()

Out[1]:

abcd

Though after reviewing the source I then found feature documentation is rather buried in the README.md

Kiskadee-dev commented 2 years ago

When it's linear its alright, but once lets say, B gets a bifurcation of transitions where C(1) is unavailable due to some conditions but it has C(2) available and both ends at D, we could benefit from automagically calling traverse_to_D()

Similar to A* pathfinding but simplified, where the costs are the conditions, if there is a path, we can traverse to the target even if the graph is complex.

There are many use cases, I was automating a few UI tests and was missing a feature to do these automated changes of states so I could focus only on defining valid paths instead of manually switching between many of them.

jorritsmit commented 2 years ago

I use this run function that i call every x seconds:

def run(self):
    a = self.get_triggers(self.state)
    triggers = [x for x in a if not x.startswith("to_")]
    for trig in triggers:
        if self.trigger(trig):
            break

and this as the init of the statemachine:

        super().__init__(
            self,
            states=self.states,
            initial=self.initial,
            transitions=self.trans,
            auto_transitions=True,
            ignore_invalid_triggers=True,
        )

The auto triggers is not needed for your case, but the ignore_invalid_triggers here is crucial, because otherwise you'll be bombarded with errors. This run function just checks all conditional transitions for the current state (but skips the auto_transitions) and when the first one succeeds it breaks

Kiskadee-dev commented 2 years ago

@jorritsmit You gave me a few ideas,

using this simple state machine described below..

class SM:
    def __init__(self):
        self.states = [
            State(name="dummy", on_enter=['echo']),
            State(name="idle", on_enter=['echo']),
            State(name="jumping", on_enter=['echo']),
            State(name="walking", on_enter=['echo']),
            State(name="running", on_enter=['echo'])
        ]
        self.transitions = [
            {'trigger': 'idle', 'source': 'dummy', 'dest': 'idle'},
            ['jumping', 'idle', 'jumping'],
            ['walking', 'idle', 'walking'],
            ['running', 'walking', 'running']
        ]

        self.machine = Machine(self, states=self.states, transitions=self.transitions, initial='dummy')

    def echo(self):
        print("Echo: "+self.state)
        time.sleep(3)

if we wanted to find a valid path from the dummy state to any other state, we could use something like this

def traverse(current_state, target_state):
    looked_states = []
    path = []
    def search(current_state=current_state, target_state=target_state):
        #Prevent loops
        looked_states.append(current_state)

        #Get the list of transitions we can travel to
        a = sm.machine.get_triggers(current_state)
        triggers = [x for x in a if not x.startswith("to_")]

        #TODO: Filter the list of transitions by condidions

        #Return true if we're in the target state
        if current_state == target_state:
            print("**Reached target state**")
            path.append(current_state)
            return True

        #Otherwise check the rest of the list recursively
        print(current_state+" -> "+str(triggers))
        for next_state in triggers:
            if not next_state in looked_states:
                print("Looking: "+next_state)
                if search(next_state, target_state):
                    path.append(current_state)
                    return True
        return False
    search()
    path.reverse()
    return path[1::]

which would return a valid path

image

The big let down is that by using get_triggers the state name must be exactly the same as the trigger, and i think this isn't very cool

aleneum commented 1 year ago

The big let down is that by using get_triggers the state name must be exactly the same as the trigger, and i think this isn't very cool

you could combine get_triggers with get_transitions. The traversal part of search could look like this:

        #Otherwise check the rest of the list recursively
        print(current_state+" -> "+str(triggers))
        for event in triggers:
            for trans in sm.machine.get_transitions(trigger=event, source=current_state):
                if trans.dest not in looked_states:
                    print("Looking: " + trans.dest)
                    if search(trans.dest, target_state):
                        path.append(current_state)
                        return True
        return False

transitions 0.9.0 also supports model.may_<trigger> to check whether a transition can be executed based on the transition's conditions. But this would not suffice for your use case because sm.may_walking() would take the current state of sm into account.

aleneum commented 1 year ago

Since there hasn't been feedback for 14 days I will close this issue for now. Feel free to comment nevertheless if this issues has not been solved for you. If necessary, I will reopen the issue again.