# Finite-State Machines

This chapter introduces the concept of Finite-State Machines 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](https://en.wikipedia.org/wiki/finite-statemachine) (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
 as a flowchart with a list of named boxes (states) and
arrows (transitions) between them—this type of flowchart is called a *state diagram*.

In practice,  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 an
. 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.

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:

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.

The  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.

<!-- In behavioral experiments, FSMs can be used to specify trial structure, stimulus -->
<!-- presentation, and response contingencies in a clear and reproducible way. The -->
<!-- `Bpod Finite-State Machine`_ implements an FSM using an -->
<!-- `Arduino <https://en.wikipedia.org/wiki/Arduino>`_-compatible -->
<!-- `microcontroller <https://en.wikipedia.org/wiki/microcontroller>`_, allowing for high -->
<!-- temporal fidelity not typically achievable in software alone. -->

## The StateMachine Data Model

In bpod-core, an  is represented by the
[`StateMachine`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.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`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine)
object and adding states with its [`add_state()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.add_state) method:

```python3
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:

![image](state_machines/hello_world_01__light.svg)![image](state_machines/hello_world_01__dark.svg)

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()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.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`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.states) dictionary.

```python3
fsm.states['World'] = {'timer': 1.0}
```

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

![image](state_machines/hello_world_02__light.svg)![image](state_machines/hello_world_02__dark.svg)

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`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.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:

```python3
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`](https://docs.python.org/3/builtins/stdtypes.html#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`).

![image](state_machines/hello_world_03__light.svg)![image](state_machines/hello_world_03__dark.svg)

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:

```python3
fsm.states['Hello'].actions = {'TTLOut0': 1}
fsm.states['World'].actions = {'TTLOut1': 1}
```

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

![image](state_machines/hello_world_04__light.svg)![image](state_machines/hello_world_04__dark.svg)

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()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.add_state):

```python3
from bpod_core.fsm import StateMachine

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

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

### Validation

The class [`StateMachine`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine) and related classes are [Pydantic](https://docs.pydantic.dev/latest/)
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:

```pycon
>>> 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
```

```pycon
>>> 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:

```pycon
>>> fsm.set_global_timer(index=20, duration=5)  # this validates OK
>>> with Bpod() as bpod:
...     bpod.run(fsm)
Traceback (most recent call last):
   ...
ValueError: Requested invalid Global Timer with index 20 - Bpod Finite State Machine R2.0-2.5 supports up to 16 Global Timers with indices 0-15
```

### Import and Export

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

- [`to_json()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.to_json),
  [`to_yaml()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.to_yaml) and
  [`to_dict()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.to_dict) return in-memory representations as a JSON
  string, YAML string and Python dict, respectively.
- [`to_digraph()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.to_digraph) returns a Graphviz
  [`Digraph`](https://graphviz.readthedocs.io/en/stable/api.html#graphviz.Digraph) instance which can be used to render the state diagram, for
  instance in a Jupyter notebook.
- [`to_file()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.to_file), depending on the file extension, writes
  either:
  - `.json`: a JSON serialization of the [`StateMachine`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine),
  - `.yaml`, `.yml`: a YAML serialization of the [`StateMachine`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine),
  - `.svg`, `.png`, `.pdf`: a rendered state diagram via Graphviz.
- [`from_json()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.from_json), [`from_dict()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.from_dict),
  and [`from_file()`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine.from_file) create a StateMachine from serialized data.

```python3
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
```

```python3
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](https://graphviz.org/documentation/) for installation instructions.

<a id="examples"></a>

## Example State Machines

The following examples illustrate usage and features of the [`StateMachine`](https://int-brain-lab.github.io/bpod-core/api/bpod_core.fsm/index.html.md#bpod_core.fsm.StateMachine) class.

* [Back Operator](https://int-brain-lab.github.io/bpod-core/state_machines/examples/back_operator/index.html.md)
* [Conditions](https://int-brain-lab.github.io/bpod-core/state_machines/examples/conditions/index.html.md)
* [Global Counters](https://int-brain-lab.github.io/bpod-core/state_machines/examples/global_counters/index.html.md)
* [Global Timers](https://int-brain-lab.github.io/bpod-core/state_machines/examples/global_timers/index.html.md)
* [Light Chasing](https://int-brain-lab.github.io/bpod-core/state_machines/examples/light_chasing/index.html.md)
* [TTL Triggered State Change](https://int-brain-lab.github.io/bpod-core/state_machines/examples/ttl_triggered_state_change/index.html.md)
* [Two Choice](https://int-brain-lab.github.io/bpod-core/state_machines/examples/two_choice/index.html.md)
