Creating a Custom Experiment: Match Port Example¶
This guide explains how to create a custom experiment in Ethopy by examining the match_port.py
example, which implements a 2-Alternative Forced Choice (2AFC) task.
Understanding States and Experiments in Ethopy¶
What is a State?¶
A State in Ethopy represents a distinct phase of an experiment with specific behaviors and transitions. States are fundamental building blocks of the experimental flow and follow these principles:
- Encapsulated Logic: Each state handles a specific part of the experiment (e.g., waiting for a response, delivering a reward)
- Well-defined Transitions: States define clear rules for when to transition to other states
- Shared Context: All states share experiment variables through a shared state dictionary
- Lifecycle Methods: States implement standard methods (
entry()
,run()
,next()
,exit()
) for predictable behavior
The State pattern allows complex behavioral experiments to be broken down into manageable, reusable components that can be combined to create sophisticated experimental flows.
Why Use States?¶
States provide several advantages in experimental design:
- Modularity: Each state handles one specific aspect of the experiment
- Reusability: States can be reused across different experiments
- Maintainability: Easier to debug and modify individual states without affecting others
- Readability: Experimental flow becomes clearer when broken into distinct states
- Robustness: State transitions are explicitly defined, reducing the chance of unexpected behavior
Required Components for Experiments¶
Every Ethopy experiment requires:
- Condition Tables: DataJoint tables that define the experimental parameters
- Entry and Exit States: Special states required by the StateMachine
- Base Experiment Class: Inherits from both
State
andExperimentClass
- Custom States: Implementation of experiment-specific states
The StateMachine in Ethopy automatically finds all state classes that inherit from the base experiment class and handles the flow between them based on their next()
method returns.
Overview of the Match Port Experiment¶
The Match Port experiment challenges animals to correctly choose between ports based on stimuli.
It implements: - Adaptive difficulty using staircase methods - Reward and punishment handling - Sleep/wake cycle management - State machine architecture for task flow
Defining Database Tables with DataJoint¶
Each experiment requires defining conditions tables that store parameters. Here's how the Match Port experiment defines its table:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
This table definition:
- Uses the @experiment.schema
decorator to associate with the experiment database
- Creates a part table MatchPort
under the parent Condition
table (all parameters of the experiements are part table of the Condition
table)
- Defines fields with default values and data types
- Documents the purpose with a comment at the top
for more details check datajoint documentation
Creating the Base Experiment Class¶
The main experiment class sets up the foundation for all states and defines required parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Key components:
- cond_tables
: Lists the condition tables this experiment uses and define/store the parameters of the experiment.
- required_fields
: Specifies which fields must be provided in the task file
- default_key
: Sets default values for parameters
required_fields + default_key must have all the parameters from the Condition.MatchPort table
- entry()
: Common initialization for all states. The entry function initializes the state by logging the state transition in the Trial.StateOnset in the experiment schema, resetting the response readiness flag, and starting a timer to track the duration of the state.
Creating Individual State Classes¶
Each state is implemented as a class inheriting from the base Experiment
class. Let's analyze in detail the Trial
state, which manages stimulus presentation and response detection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Let's break down the Trial state's functionality in detail:
1. entry()
method¶
1 2 3 4 |
|
self.resp_ready = False
: Resets the response readiness flag at the beginning of each trialsuper().entry()
: Calls the parent class's entry method which:- Records the current state in the logger
- Logs a state transition event with timestamp
- Starts the state timer
self.stim.start()
: Initializes the stimulus for the current trial, activating any hardware or software components needed
2. run()
method¶
1 2 3 4 5 6 7 8 9 |
|
This method is called repeatedly while in the Trial state:
self.stim.present()
: Updates the stimulus presentation on each iteration, which might involve:- Moving visual elements
- Updating sound playback
-
Refreshing displays
-
self.beh.get_response(self.start_time)
: Checks if the animal has made a response since the trial started - Returns a boolean value indicating response status
-
The
start_time
parameter allows measuring response time relative to trial start -
Response readiness check:
self.beh.is_ready(...)
: Determines if the animal is in position and ready for the stimulus- Uses
trial_ready
parameter from current condition to check timing requirements - If the animal is ready AND we haven't yet marked the trial as ready:
- Sets
self.resp_ready = True
to indicate the animal is in position - Calls
self.stim.ready_stim()
to potentially modify the stimulus (e.g., changing color, activating a cue)
- Sets
3. next()
method¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
This method determines the next state based on the animal's behavior and timing:
- Early Withdrawal Check:
if not self.resp_ready and self.beh.is_off_proximity()
: Checks if the animal left before being ready-
If true → transition to "Abort" state
-
Incorrect Response Check:
elif self.has_responded and not self.beh.is_correct()
: Animal responded but to the wrong port-
If true → transition to "Punish" state
-
Correct Response Check:
elif self.has_responded and self.beh.is_correct()
: Animal responded correctly-
If true → transition to "Reward" state
-
Timeout Check:
elif self.state_timer.elapsed_time() > self.stim.curr_cond["trial_duration"]
: Trial ran too long-
If true → transition to "Abort" state
-
Experiment Stop Check:
elif self.is_stopped()
: Checks if experiment has been externally requested to stop-
If true → transition to "Exit" state
-
Default Case:
else: return "Trial"
: If none of the above conditions are met, stay in the Trial state
4. exit()
method¶
1 2 |
|
self.stim.stop()
: Stops the stimulus presentation- Cleans up resources
- Resets hardware components if necessary
- Prepares for the next state
State Machine Flow¶
The experiment implements a state machine that flows through these main states:
Entry
→ Initial setupPreTrial
→ Prepare stimulus and wait for animal readinessTrial
→ Present stimulus and detect responseReward
orPunish
→ Handle correct or incorrect responsesInterTrial
→ Pause between trialsHydrate
→ Ensure animal drink the minimum reward amountOfftime
→ Handle sleep periodsExit
→ Clean up and end experiment
How to Create Your Own Experiment¶
To create your own experiment:
- Define your condition table with DataJoint
- Create a base experiment class that inherits from
State
andExperimentClass
- Specify
cond_tables
,required_fields
, anddefault_key
- Implement the
entry()
method for common state initialization - Create individual state classes for each stage of your experiment
- Implement the state machine flow through the
next()
methods
By following this pattern, you can create complex behavioral experiments that handle stimulus presentation, animal responses, and data logging in a structured manner.