title | author | date |
---|---|---|
README |
a.whit ([email](mailto:nml@whit.contact)) |
September 2022 |
This package contains a state machine and model for a cursor-based behavioral task. It is intended primarily for use in experimental science. It is derived from the classic center-out task paradigm of behavioral neurophysiology -- which has most notably been employed in the study of neural codes in motor cortex. The task requires a user to move a cursor between visual targets that are positioned throughout a three-dimensional workspace.
This task framework is designed to be compatible with the pytransitions package for Python state machines.
See the examples for a quick introduction. More extensive examples and doctests are provided as comments in the source code (e.g., model.py).
In this version of the task paradigm, intervals of time and sequences of movement are divided into trials. A trial represents one attempt at a fixed behavioral objective. If the user meets all of the conditions of the objective during a trial period, then the trial is considered a success, and a rewarding outcome is typically rendered. For this task, the success conditions require a user to move the cursor from some "center" target to a randomly-selected "outer" target, and back again. As the names imply, the center target is typically placed at the origin of the 3D space, and the outer targets are positioned near the periphery. The user is expected to hold for a short interval at each target, and to wait for a short delay interval after the initial presentation of the outer target.
The package can be used as-is, so long as the Python path is set appropriately.
However, a setup.cfg
file has been provided, to facilitate package
installation via setuptools. Package installation can be accomplished via the
pip install command:
pip install path/to/delay_out_center_task
The update and user flags are common options that can be added to the command:
pip install -U --user path/to/delay_out_center_task
Once installed, the pytest framework can be used to verify that the package is functioning as expected, by running the doctests in this README.
python -m pytest path/to/delay_out_center_task
The output of this command should indicate that all tests passed.
Alternatively -- or in addition -- the unittest framework can be used to run similar, but slightly more systematic tests.
python test/test_trial_sequences.py
This task framework is intended to be forked and extended. Please contact the author if you make modifications or design new tasks.
Perhaps the best way to introduce the package and task is via an example. This example is provided in doctest format, and can be run via the following code:1
import doctest
doctest.testfile('README.md', module_relative=False)
Import the task components parts.
>>> from delay_out_center_task import Environment
>>> from delay_out_center_task import Model
>>> from delay_out_center_task import Machine
For the purpose of this example, override the timeout
trigger function, to
disable automatic state transitions, and facilitate manual interaction with the
state machine.
>>> Model.timeout = lambda s, *a, **k: None
Also disable event reporting, to improve clarity.
>>> Machine.log_event = lambda s, d: None
Initialize the task components: an environment interface, a model, and a machine.
>>> environment = Environment()
>>> model = Model(environment=environment)
>>> machine = Machine(model=model)
Upon initialization, the environment contains only a spherical cursor object.
>>> list(environment.objects)
['cursor']
Activate the state machine and start a block of trials. At the start of each block, a set of possible outer target parameters is loaded. Here, we are using the default set, which places targets at the corners and on the faces of a square centered at the origin.
>>> # Verify the current state.
>>> model.state
'inactive'
>>> # Start a block of trials.
>>> success = model.start_block()
State: intertrial
>>> # Verify that a set of target parameters was loaded.
>>> len(model.targets)
8
>>> model.targets[0]['position']
(1.0, 0.0, 0.0)
The intertrial
state is a brief delay between trials that automatically
transitions to the trial_setup
state, after a brief delay. During trial setup
a target is initialized in the environment, and a set of target parameters
(i.e., a behavioral objective for the trial) are chosen. The trial setup state
transitions automatically to the initial state in a trial sequence. For this
task, the first trial state is move_a
.2
>>> # Verify that no target sphere exists in the environment.
>>> target_index = model.target_index
>>> environment.exists('target')
False
>>> # Transition through the trial_setup state
>>> # into the move_a state.
>>> success = model.trigger('timeout')
State: move_a
State: move_a
>>> # Two state transitions are reported.
>>> # The first reported transition is incorrectly
>>> # logged as move_a instead of trial_setup.
>>> # Verify that a target sphere now exists in the
>>> # environment, and that a new target parameters
>>> # index has been randomly chosen.
>>> environment.exists('target')
True
>>> model.target_index == target_index
False
To satisfy the success conditions of the move_a
task state, the user must
move the cursor to engage the target. This causes the task to transition to the
hold_a
state. The hold state requires that the user maintain engagement with
the target for a brief interval. When the hold interval times out, the task
transitions to the delay_a
state. A cue appears at an outer target position.
The cue resembles a target sphere, but has a different color.
>>> # Transition to the hold_a state.
>>> success = model.target_engaged()
State: hold_a
>>> # Verify that no cue exists.
>>> environment.exists('cue')
False
>>> # Transition to the delay_a state.
>>> # A cue sphere appears.
>>> success = model.trigger('timeout')
State: delay_a
>>> environment.exists('cue')
True
During the delay period, the target sphere remains at the center position, and
the user is expected to engage with the target for the duration of the period.
When the period expires, the cue disappears, and the task transitions to the
move_b
state. At this point, the target is moved to the position formerly
occupied by the cue sphere.
>>> # Record the state of the target and cue before the transition.
>>> p_c = environment.get_position('cue')
>>> p_t = environment.get_position('target')
>>> # Transition to the move_b state.
>>> success = model.trigger('timeout')
State: move_b
>>> # The cue is extinguished.
>>> environment.exists('cue')
False
>>> # The target is moved to the cue position.
>>> environment.get_position('target') == p_t
False
>>> environment.get_position('target') == p_c
True
The move_b
and hold_b
task states are nearly identical to the move_a
and
hold_a
states. The same is true for move_c
and hold_c
, except that the
target is returned to the center position.
>>> # Transition to the hold_b state.
>>> success = model.target_engaged()
State: hold_b
>>> # Transition to the move_c state.
>>> success = model.trigger('timeout')
State: move_c
>>> # The target is returned to the center position.
>>> environment.get_position('target') == (0.0, 0.0, 0.0)
True
>>> # Transition to the hold_c state.
>>> success = model.target_engaged()
State: hold_c
Upon successful completion of the hold state, the task transitions to the
success
state, wherein reinforcement is typically delivered. The success
state automatically transitions to the trial_teardown
state after a brief
delay. During teardown, the target is removed from the workspace, and the task
is returned to the intertrial state.2 This completes a
successful trial.
>>> # Transition to the success state.
>>> success = model.trigger('timeout')
State: success
>>> # Transition to the trial_teardown state.
>>> success = model.trigger('timeout')
State: intertrial
State: intertrial
>>> # The target is returned to the center position.
>>> environment.exists('target')
False
A trial fails if the user does not meet a success condition for any trial state. This might occur, for example, if the user fails to engage with the target during a move period.
>>> # Transition through the trial_setup state,
>>> # and to the move_a states from intertrial.
>>> success = model.trigger('timeout')
State: move_a
State: move_a
>>> # The move state times out.
>>> success = model.trigger('timeout')
State: failure
>>> # Transition through the trial_teardown state,
>>> # to return to the intertrial state.
>>> success = model.trigger('timeout')
State: intertrial
State: intertrial
A trial might also result in failure if the user disengages from the target during a hold period.
>>> # Transition through the trial_setup state,
>>> # and to the move_a states from intertrial.
>>> success = model.trigger('timeout')
State: move_a
State: move_a
>>> # Transition to the hold_a state.
>>> success = model.target_engaged()
State: hold_a
>>> # The cursor disengages before the hold period expires.
>>> success = model.target_disengaged()
State: failure
>>> # Transition through the trial_teardown state,
>>> # to return to the intertrial state.
>>> success = model.trigger('timeout')
State: intertrial
State: intertrial
More extensive examples and doctests are provided as comments in the source code (e.g., machine.py).
When designing a behavioral experiment, research scientists often build custom implementations that reproduce common task structure and logic. Usually, these implementations are designed for a particular hardware system or graphical interface. The associated code often involves a mixture of low-level device programming and high-level task scripting. Usually, a state machine is used to organize the latter.
This package is intended to be a step toward separation of concerns in the implementation of behavioral experiments. In particular, the objective is to make the task logic and actions as independent as possible from the environment and middleware. In this way, we hope to encourage code / paradigm re-use, and to cut down on mistakes. Behavioral paradigms should be generalizable, across hardware setups. At the same time, task designers shouldn't get bogged down in low-level details each time they want to design a new experiment.
As a particular example of how this philosophy and approach might be realized,
it is worthwhile to point out that this task has been designed to be compatible
with the ros_transitions package. This means that this task can interface
immediately with any hardware or software modules built for ROS2. For
example, the ros_force_dimension package might be used to link the
cursor to a Force Dimension haptic robot, which might be displayed in a
Unity3D scene, via the ROS2-enabled unity_spheres_environment.
It is important to reiterate that -- despite this potential for extension -- this remains a mostly self-contained package, and the task itself can be tested and modified entirely in isolation. The included prototype environment facilitates such development and testing (and, in turn, defines the expectations for any environment implementation; see environment.py). The best way to get to know the task framework is to clone a copy and mess around.
Copyright 2022 Neuromechatronics Lab, Carnegie Mellon University
Contributors:
- a. whit. (nml@whit.contact)
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
Footnotes
-
Provided that the package is installed, or the Python path is otherwise set appropriately. ↩
-
Due to the idiosyncrasies of the pytransitions package, states that transition automatically are not correctly reported. Instead, the next state is logged twice. This is because state logging occurs after a state transition is complete, and automatic transitions occur during the transition. ↩ ↩2