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
? , 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.
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')
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.
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()
All state methods have the form
p_ in the Pynapse Code Editor
and let the code completion do the work for you.
Tell Pynapse to move to a new state. All states are python 'classes'. The input to
be either the class or the string class name you want to switch to. See the example below
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.
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)
Switch to a default state after a certain period of time.
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.
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')
Cancel the current timeout. Can be called anywhere within the state.
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')
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
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()
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.
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()