Finite-State Machines

This chapter introduces the finite-state machine (FSM) concept and explains how to create, validate, visualize, import, and export state machines with bpod-core.

What is a Finite State Machine?

A finite-state machine (FSM) is a model of computation made up of a finite number of states and transitions between those states. At any given moment, the machine is in exactly one state, and certain events cause it to move, or transition, to another state. Think of the finite-state machine as a flowchart with a list of named boxes (states) and arrows (transitions) between them—this type of flowchart is called a state diagram.

digraph { S1 [label="State 1"]; S2 [label="State 2"]; S1 -> S2 [label="Event"]; }

Fig. 1 A state diagram.

In practice, finite-state machines are used to model a wide range of systems where behavior depends on a sequence of events, making the logic easier to design and understand. A simple light switch offers an intuitive example of a finite-state machine. It has only two states—Off and On—and two events that trigger transitions between them: flip up and flip down. At any given moment, the switch is in exactly one state, and performing the corresponding flip moves the system to the other.

digraph { a [label="Off"]; b [label="On"]; a -> b [label="flip up"]; a -> b [style="invis"]; b -> a [label="flip down"]; }

Fig. 2 Flip up, flip down—ad infinitum.

While the light switch illustrates a finite-state machine with no clearly defined start or end, many real-world processes have natural beginnings and endings. The scientific publication process provides an example. It begins in the Draft state, progresses through the Review state, and—after a few revision—(hopefully) concludes with a Publication. In a state diagram, the entry to the finite-state machine is typically indicated by a filled circle, while the exit is shown with a double circle:

digraph { s [label="", shape=circle, style=filled, fillcolor=black, width=0.25]; x [label="", shape=doublecircle, style=filled, fillcolor=black, width=0.125]; a [label="Draft"]; b [label="Review"]; c [label="Publication"]; s -> a a -> b [label="submittal"]; a -> b [style="invis"]; b -> a [label="R&R"]; b -> c [label="approval"]; b -> x [label="rejection"]; c -> x }

Fig. 3 If only the reviewers ever agreed …

Finite-state machines can be a powerful tool in the design of behavioral experiments. In this context, they can be used to specify trial structure, stimulus presentation, and response contingencies in a clear and reproducible way. Each state represents a specific phase of the experiment, and transitions are triggered by events such as a subject’s action or a timer. Timers are particularly useful to control the duration of states and the timing of their associated output actions—such as turning on a light, sounding a buzzer, or delivering a reward.

digraph { s [label="", shape=circle, style=filled, fillcolor=black, width=0.25]; x [label="", shape=doublecircle, style=filled, fillcolor=black, width=0.125]; a [label="Stimulus"]; b [label="Wait"]; c [label="Reward"]; d [label="Buzzer"]; e [label="End"]; s -> a a -> b [label="timeout"]; b -> c [label="lever pressed"]; b -> d [label="timeout"]; c -> e [label="timeout"]; d -> e [label="timeout"]; e -> x [label="timeout"]; { rank=same; c; d; } }

Fig. 4 A simple trial sequence implemented as a finite-state machine.

The finite-state machine pictured above represents a single trial in a behavioral experiment. It heavily relies on timers to define both the duration of states and their associated output actions. The Stimulus ends automatically when its timer expires, moving the subject into the Wait state. From there, the trial can proceed in two ways: if the subject performs the required action (pressing a lever) within the allotted time of the Wait state, the machine transitions to Reward; otherwise, a Buzzer signals a missed opportunity. The durations of both the reward and the buzzer are again governed by their respective timers, after which either state transitions to the trial’s End state and, finally, to the trial’s exit.

Key Concepts

State

A specific configuration of the system at a given moment.

Event

A trigger that causes the system to transition from one state to another.

Transition

The movement of the system from one state to another in response to an event.

Output action

A controlled action that occurs with the onset of the state.

Timer

A mechanism that, after a set interval, generates an event which may trigger a transition.

The StateMachine Data Model

In bpod-core, an FSM is represented by the StateMachine class. It defines states, state transitions, and the output actions assigned to each state. It also introduces Bpod-specific concepts such as state timers, global timers, conditions, and global counters. Finally, it provides tools for validation, visualization, and importing/exporting to and from other formats.

Creating a State Machine

A state machine is created by instantiating a StateMachine object and adding states with its add_state() method:

from bpod_core.fsm import StateMachine

fsm = StateMachine()
fsm.add_state(name='Hello')

The commands above create a state machine with a single state named Hello. The very first state that is added to a state machine automatically becomes the entry state — this is where execution begins. Every state comes with a state timer, which can be used to generate timer-based events. By default, this timer is set to 0 s, meaning the state will immediately trigger a timeout event. The created state machine can be visualized using the following state diagram:

../_images/hello_world_01.svg

Fig. 5 Well hello!

See the section Import and Export for details on how such state diagrams are generated. To make things a bit more interesting, let’s add a second state named World, this time with a 1 s state timer. Normally, we could just call the add_state() method again. However, for the sake of demonstration, we’ll use a different approach by adding a new entry directly to our state machine’s states dictionary.

fsm.states['World'] = {'timer': 1}

Now our state machine contains two states: Hello and World. However, they are not yet connected, which is obvious in the state diagram below:

../_images/hello_world_02.svg

Fig. 6 That doesn’t look right.

As you can see, we’re missing a transition from Hello to World. Fortunately, transitions can be added later by directly modifying the fields of the StateMachine instance. Let’s add a transition from Hello to World, triggered by the end of Hello’s state timer. While we’re at it, we’ll also change Hello’s state timer to 1.5 s. Finally, we’ll add a transition from World to the special exit state:

fsm.states['Hello'].transitions = {'Tup': 'World'}
fsm.states['Hello'].timer = 1.5
fsm.states['World'].transitions = {'Tup': '>exit'}

The end of a state timer is signaled by the Tup event (short for time up). A state’s transitions are defined in a Python dict, where keys are the triggering events (e.g. Tup), and values are the transition targets (either another state’s name or an operator such as >exit or >back).

../_images/hello_world_03.svg

Fig. 7 Getting there.

At this point, our state machine is functional: it moves from Hello to World after 1.5 seconds, and then exits after 1 more second. However, it still doesn’t do anything, because we haven’t defined any output actions yet. Let’s fix that by adding actions to each state. Actions define what happens when a state is active, such as turning on an output channel:

fsm.states['Hello'].actions = {'BNC1': 1}
fsm.states['World'].actions = {'BNC2': 1}

And with that, our Hello, World! example is complete:

../_images/hello_world_04.svg

Fig. 8 Our finite-state machine is complete.

In the previous examples, we first added bare-bones states and then modified them afterward (changing timers, adding transitions, assigning actions). This was done purely for demonstration purposes, so you could see that states behave like regular Python objects and can be manipulated at any time. In practice, however, you would usually choose the more straightforward approach: specify everything a state needs (its timer, transitions, and actions) directly in the call to add_state():

from bpod_core.fsm import StateMachine

fsm = StateMachine()
fsm.add_state(name='Hello', timer=1.5, transitions={'Tup': 'World'}, actions={'BNC1': 1})
fsm.add_state(name='World', timer=1.0, transitions={'Tup': '>exit'}, actions={'BNC2': 1})

Using this simple concept you can create arbitrarily complex patterns and behavioral sequences. See the section Example State Machines for more examples.

Take-Home Messages

Adding states

Use add_state() with parameters name, timer, transitions, and actions.

Modifying states

You can create and modify states by directly manipulating the fields of StateMachine.

Entry state

The first state you add is always the entry point of the state machine.

State Timers

Every state has a timer (default 0 s), which triggers the Tup events on expiry.

Transitions

States define transitions in a Python dict, mapping events to targets.

Actions

States can perform output actions (e.g., activate a port or channel) while active.

Validation

The class StateMachine and related classes are Pydantic models. All parameters are strictly typed and come with defined value ranges and constraints. Field values are coerced to their respective types and validated both at creation and assignment:

Listing 1 Pydantic complaining when trying to add a state with an invalid timer.
>>> fsm.add_state(name='MyState', timer=-1)
Traceback (most recent call last):
   ...
pydantic_core._pydantic_core.ValidationError: 1 validation error for StateMachine.add_state
timer
  Input should be greater than or equal to 0 [type=greater_than_equal, input_value=-1, input_type=int]
    For further information visit https://errors.pydantic.dev/2.11/v/greater_than_equal
Listing 2 Assignments are validated as well
>>> fsm.add_state(name='MyState', timer=1)
>>> fsm.states['MyState'].actions = 42
Traceback (most recent call last):
   ...
pydantic_core._pydantic_core.ValidationError: 1 validation error for State
actions
  Input should be a valid dictionary [type=dict_type, input_value=42, input_type=int]
    For further information visit https://errors.pydantic.dev/2.11/v/dict_type

This validation mechanism helps catch errors early in the design phase of an experiment. More detailed validation is performed at runtime, when the specific constraints of the hardware are known:

Listing 3 A ValueError is raised when attempting to run a state machine that exceeds the hardware’s capabilities.
>>> fsm.set_global_timer(index=20, duration=5)  # this validates OK
>>> bpod = Bpod()
>>> bpod.send_state_machine(fsm)
Traceback (most recent call last):
   ...
ValueError: Too many global timers in state machine - hardware supports up to 16 global timers

Import and Export

There are several convenient methods to serialize and visualize state machines:

  • to_json(), to_yaml() and to_dict() return in-memory representations as a JSON string, YAML string and Python dict, respectively.

  • to_digraph() returns a Graphviz Digraph instance which can be used to render the state diagram, for instance in a Jupyter notebook.

  • to_file(), depending on the file extension, writes either:

    • .json: a JSON serialization of the StateMachine,

    • .yaml, .yml: a YAML serialization of the StateMachine,

    • .svg, .png, .pdf: a rendered state diagram via Graphviz.

  • from_json(), from_dict(), and from_file() create a StateMachine from serialized data.

Listing 4 A roundtrip from StateMachine to JSON and back to StateMachine
from bpod_core.fsm import StateMachine

# create a state machine and serialize it as a JSON string
fsm1 = StateMachine()
fsm1.add_state(name='Pi', timer=3.1415)
json_string = fsm1.to_json()

# create a second, identical state machine from the JSON string
fsm2 = StateMachine.from_json(json_string)
assert fsm2 == fsm1
Listing 5 Importing a StateMachine from a JSON file and exporting its state diagram as a PNG file.
from bpod_core.fsm import StateMachine

fsm = StateMachine.from_file('state_machine.json')
fsm.to_file('state_machine.png')

Note

Rendering diagrams depends on the Graphviz system libraries. See the Graphviz documentation for installation instructions.

Example State Machines

The following examples illustrate usage and features of the StateMachine class.