Skip to content

States

Pynapse has an internal state machine that logs all events and keeps track of the current state. New triggers coming in are filtered through this state machine, so that only the slot methods associated with the current state can run.

There is a special 'Always' state - slot methods in the Always state can trigger on any polling loop. This is a useful state to add user mode controls (start/pause/stop).

States are 'classes' in the Python code that have the special comment #StateID = ? at the end of the class definition. The number defined here is the id number associated with this state. This value will be timestamped and stored with the data in the data tank. If the parser finds a ? , it will automatically assign a number for you. Otherwise enter an integer to lock the StateID in place, for example #StateID = 555.

See Working with StateIDs for more information.

Slot Methods for Responding to State Changes

These state slots capture state machine changes. They are available as method definitions inside Pynapse states, including the Always state. Write a method with this name to react to the corresponding event.

Slot name Event
s_State_change Triggers on any state change
s_State_enter Triggers once when the state begins
s_State_exit Triggers once when the state ends
s_State_timeout Triggers when the state timeout is reached
Example

Turn on an output only while in a particular state.

class StartTrial:    # StateID = ?
    # turn on MyOutput when entering state
    def s_State_enter():
        p_Output.MyOutput.turnOn()

    # when MyInput passes 'Time to Active', switch to ActiveState
    def s_MyInput_active():
        p_State.switch(ActiveState)

    # turn off MyOutput when exiting state
    def s_State_exit():
        p_Output.MyOutput.turnOff()

class ActiveState:    # StateID = ?
    # when MyInput passes 'Time to Pass', switch to PassState
    def s_MyInput_pass():
        p_State.switch('PassState')

    def s_MyInput1_fail():
        p_State.switch(FailState)

In this example, use s_State_change() to track order of state execution. Suppose there are many states that exit to TargetState. If the state that exited to TargetState is TargetOldState, we want to do something.

class Always:    # StateID = 0
    def s_State_change(newstateidx, oldstateidx):
        print('new state', newstateidx, 'old state', oldstateidx)
        if newstateidx == TargetState and oldstateidx == TargetOldState:
            print('do something')

State Timeouts

The Pynapse state machine has a built-in Timer that is used as a timeout within the current state that moves to another state if it fires. Timeouts are usually set in the s_State_enter() slot method but can be set anywhere in the State. Timeouts can also be canceled.

Example

If the user fails to press a button (MyInput) within five seconds, we want to move to a NoTrial state and wait there for ten seconds before starting a new trial.

class StartTrial:    # StateID = ?

    # if no input is received after 5 seconds, switch to NoTrial state
    def s_State_enter():
        p_State.setTimeout(5, NoTrial)

    def s_MyInput_rise():
        p_State.switch(TrialState)

class NoTrial:    # StateID = ?
    # wait 10 seconds, return to StartTrial
    def s_State_enter():
        p_State.setTimeout(10, StartTrial)

class TrialState:    # StateID = ?
    # turn on an output
    def s_State_enter():
        p_Output.MyOutput.turnOn()

Methods

All state methods have the form p_State.{METHOD}. Type p_ in the Pynapse Code Editor and let the code completion do the work for you.

State Control

switch

p_State.switch(newstate)

Tell Pynapse to move to a new state. All states are python 'classes'. The input to switch can be either the class or the string class name you want to switch to. See the example below

Inputs Type Description
newstate class or string Name of new class to switch to

Note

The function that contains the switch command does not exit immediately after switching the internal state. This can have unintended consequences, particularly if you are using the Sync to State Change option for outputs or timers. Best practice is to use the switch command last, right before the function exits.

See Synchronizing Events for information.

Example

Switch between states when MyInput goes high.

class StartTrial:    # StateID = ?

    def s_MyInput_active():
        p_State.switch(ActiveState)

class ActiveState:    # StateID = ?
    def s_MyInput_pass():
        # you can also switch states with a string name
        p_State.switch('PassState')

    def s_MyInput_fail():
        p_State.switch(FailState)

setTimeout

p_State.setTimeout(secs, stateOnTimeout)

Switch to a default state after a certain period of time.

Inputs Type Description
secs float Timeout duration in seconds
stateOnTimeout class or string New of the class to switch to

Important

There can only be one active timeout per state. If you need to set a new timeout within the state, use the cancelTimeout method first.

Example

Toggle between the FirstState and SecondState until MyInput rises in FirstState.

class FirstState:    # StateID = ?

    # if no input is received in 5 seconds, switch to SecondState
    def s_State_enter():
        p_State.setTimeout(5, SecondState)

    def s_MyInput_rise():
        p_State.switch(EndState)

class SecondState:    # StateID = ?

    # wait 5 seconds, return to FirstState
    def s_State_enter():
        p_State.setTimeout(5, FirstState)

class EndState:    # StateID = ?
    def s_State_enter():
        print('done')

cancelTimeout

p_State.cancelTimeout()

Cancel the current timeout. Can be called anywhere within the state.

Example

Give the subject 15 seconds to press MyInput 10 times. If successful, cancel the state timeout and give the subject unlimited time to reach 20 presses before moving to the success state.

class TrialState:    # StateID = ?
    def s_State_enter():
        # reset counter
        p_Global.count.write(0)

        # if target isn't reached in 15 seconds switch to DefaultState
        p_State.setTimeout(15, DefaultState)

    def s_MyInput_rise():
        # increment counter
        p_Global.count.inc()

        # if we reached our first target, cancel timeout
        if p_Global.count.read() == 10:
            p_State.cancelTimeout()
        elif p_Global.count.read() == 20:
            p_State.switch(SuccessState)

class DefaultState:    # StateID = ?
    def s_State_enter():
        print('default')

class SuccessState:    # StateID = ?
    def s_State_enter():
        print('success')

Status

isCurrent

p_State.isCurrent(stname)

Check if the current state is the given name. This is useful if you have a lot of States defined but want to do similar actions in multiple states for a given slot method. You can move the logic into the Always state and avoid repeating yourself. See the example below.

Or if you want to

Inputs Type Description
stname class or string Name of state to check
Example

In a long list of states, we want to turn the MyOutput on in just two of them.

class Always:   #StateID = 0
    def s_MyInput1_rise():
        if p_State.isCurrent(State8) or p_State.isCurrent(State20):
            p_Output.MyOutput.turnOn()

In this second example, the target state is dynamically set by a global variable. When MyInput2 turns on, the slot method is captured in the Always state and only continues (turns on Output2) if the current state matches the target state. This target state can be set on the user interface or somewhere else in the code using the Globals asset. This could also be tied to a Control asset.

    class Always:   #StateID = 0
        def s_MyInput2_rise():
            if p_State.isCurrent(p_Global.target_state.read()):
                print('current state is the target state set in the user interface')
                p_Output.MyOutput2.turnOn()

isNotCurrent

p_State.isNotCurrent(stname)

Check if the current state is not given name. This is useful if you have a lot of States defined but don't want to include the same identical slot method in all of them except a small number of states. You can include this logic check within the Always state. See the example below.

Inputs Type Description
stname class or string Name of state to check
Example

When MyInput turns on, turn on MyOutput in all states unless we're in the DontStim state.

class Always:   #StateID = 0
    def s_MyInput_rise():
        if p_State.isNotCurrent(DontStim):
            p_Output.MyOutput.turnOn()