Protocol Creation

Project Structure

Training data and code are organized into projects. The project folder structure is created automatically when a new project is initialized.

village_projects/
└── projectName/
    ├── code/
    ├── media/
    └── data/
  • code/: All executable Python scripts for tasks and training logic.

  • media/: (Optional) Audio, video, or image assets used by behavioral tasks.

  • data/: All experimental outputs — raw and processed data files and videos.

The first time you run the Training Village, a village-demo-project is created automatically with working example code you can use as a reference.


Code Organization

A training protocol consists of one or more Python task scripts plus a mandatory training_protocol.py file. The training protocol runs automatically every time a subject finishes a session and contains the logic to advance or regress the subject through the training stages — changing tasks, adjusting parameters, or both.

Typical code structures

# Example 1: Multiple sequential tasks
code/
├── habituation.py
├── lick_teaching.py
├── simple_task.py
├── final_task.py
└── training_protocol.py
# Example 2: One task with progressive difficulty
code/
├── behavioral_task.py
└── training_protocol.py

In Example 1, each script corresponds to a distinct training stage:

  1. habituation — animals get acquainted with the operant box.

  2. lick_teaching — animals learn to interact with the behavioral ports.

  3. simple_task — a simplified version of the final task.

  4. final_task — the full experimental task.

In Example 2, a single task is used throughout, but training_protocol.py adjusts its parameters after every session to increase difficulty progressively.

Both approaches can be combined freely. In addition to task scripts and the training protocol, a typical code/ folder contains helper modules for plotting, sound or video generation, and direct hardware functions:

code/
├── __init__.py
├── habituation.py
├── follow_the_light.py
├── training_protocol.py
├── session_plot.py
├── subject_plot.py
├── online_plot.py
├── direct_functions.py
├── sound_functions.py
├── LICENSE
└── README.md

The Training Protocol

The training protocol must live in a file named exactly training_protocol.py inside your code/ folder. It defines a class called TrainingProtocol that inherits from TrainingProtocolBase:

from village.classes.training import TrainingProtocolBase

class TrainingProtocol(TrainingProtocolBase):
    """
    Defines the automated training logic for the project.

    Runs every time a session ends and determines:
    - Which task the subject will run next
    - How training parameters are updated based on performance
    - How long the subject must wait before its next session (refractory period)

    Required methods:
    - __init__
    - default_training_settings
    - update_training_settings

    Optional method:
    - gui_tabs
    """

    def __init__(self) -> None:
        super().__init__()

default_training_settings()

This method defines all training variables and their initial values. It is called once when a new subject is created. The variables defined here are accessible from within any task via self.settings.<variable_name>.

For example, you might have a variable like delay or stim_size. As training progresses, these can be adjusted to fine-tune the difficulty of the task.

Warning

Every variable you want to use in your tasks must be defined here. This way, each subject ends up with its own list of settings tied to its training. Every subject starts out with these default values, and its settings change as training progresses.

After creation, a subject’s settings can be modified in three ways: manually from the SUBJECTS tab or from the TASKS tab when launching a task manually; in real time from within a running task; or automatically by update_training_settings() at the end of each session.

    def default_training_settings(self) -> None:
        """
        Define all initial training parameters for new subjects.

        Required parameters:
        - next_task (str): Name of the first task the subject will run.
        - refractory_period (int): Seconds the subject must wait between sessions.
        - minimum_duration (int): Seconds before door 2 opens (subject may leave).
        - maximum_duration (int): Seconds before the task stops automatically.

        Any additional task-specific parameters can be defined below.
        """

        # Required parameters
        self.settings.next_task = "Habituation"
        self.settings.refractory_period = 3600 * 4   # 4 hours between sessions
        self.settings.minimum_duration = 600          # 10 min minimum session length
        self.settings.maximum_duration = 900          # 15 min maximum session length

        # Task-specific parameters
        self.settings.reward_amount_ml = 0.08
        self.settings.stage = 1
        self.settings.light_intensity_high = 255      # Port light intensity (0–255)
        self.settings.light_intensity_low = 50
        self.settings.trial_types = ["left_easy", "right_easy",
                                     "left_hard", "right_hard"]
        self.settings.punishment_time = 1             # seconds
        self.settings.iti_time = 2                    # inter-trial interval, seconds
        self.settings.response_time = 10              # seconds before timeout

next_task determines the first task run for a newly created subject. refractory_period controls how long a subject must wait after finishing a session before it is allowed back into the operant box — important in multi-animal setups to prevent individual animals from monopolizing access. minimum_duration is when door 2 opens and the animal can choose to leave; maximum_duration is when the task stops unconditionally and the system waits for the animal to return home (door 2 is already open at this point, so maximum_duration ≥ minimum_duration always).


update_training_settings()

This method runs automatically at the end of every session. It receives the subject’s full session history as a DataFrame and updates whichever settings should change based on performance. The updated values are stored back into subjects.csv and used in the subject’s next session.

Warning

update_training_settings() always starts from the settings the subject had at the moment the session ended — including any values that were changed manually before or during the session. Only the variables explicitly reassigned inside update_training_settings() will be overwritten; all others will retain whatever value they had when the session finished. Keep this in mind if you change a setting manually and do not want it to persist: make sure update_training_settings() resets it explicitly.

Available attributes:

  • self.subject — name of the current subject

  • self.last_task — name of the task that just finished

  • self.df — DataFrame with all historical session data for this subject

    def update_training_settings(self) -> None:

        """This example auto-advances the subject once it shows consistent performance:

            - After "Habituation": once there are at least 3 sessions and the last
            one had 100+ trials, it moves the subject to "FollowTheLight" and lowers
            the reward to 0.07 ml.

            - After "FollowTheLight": once there are at least 2 sessions, and both the
            last two hit ≥85% correct and ≥100 trials, it advances the subject to
            stage = 2 (still the same task) and lowers the reward further to 0.05 ml."""

        if self.last_task == "Habituation":
            df_habituation = self.df[self.df["task"] == "Habituation"]

            if len(df_habituation) >= 3:
                trials_last_session = df_habituation.iloc[-1]["trial"].iloc[-1]

                if trials_last_session >= 100:
                    self.settings.next_task = "FollowTheLight"
                    self.settings.reward_amount_ml = 0.07

        elif self.last_task == "FollowTheLight":
            df_ftl = self.df[self.df["task"] == "FollowTheLight"]

            if len(df_ftl) >= 2:
                perf_last = df_ftl.iloc[-1]["correct"].mean()
                perf_prev = df_ftl.iloc[-2]["correct"].mean()
                trials_last = df_ftl.iloc[-1]["trial"].iloc[-1]
                trials_prev = df_ftl.iloc[-2]["trial"].iloc[-1]

                if (perf_last >= 0.85 and perf_prev >= 0.85 and
                        trials_last >= 100 and trials_prev >= 100):
                    self.settings.stage = 2
                    self.settings.reward_amount_ml = 0.05

gui_tabs() (optional)

If your protocol has many variables, this method lets you organize them into named tabs in the GUI panel that appears when launching a task manually. Variables not assigned to any tab are placed in a default General tab. You can also use the reserved "Hide" tab name to suppress a variable from the GUI entirely.

You can additionally restrict the allowed values for any variable, which causes a dropdown menu to appear instead of a free-text field.

    def gui_tabs(self) -> None:

        self.gui_tabs = {
            "Port_variables": ["reward_amount_ml",
                               "light_intensity_high",
                               "light_intensity_low"],
            "Other_variables": ["stage",
                                "trial_types",
                                "punishment_time",
                                "iti_time",
                                "response_time"],
        }

        # Restrict allowed values — renders as a dropdown in the GUI
        self.gui_tabs_restricted = {
            "trial_types": ["left_easy", "right_easy", "left_hard", "right_hard"],
        }

Summary

Every TrainingProtocol class must implement three methods:

Method

When it runs

Purpose

__init__

At import

Initialize the class

default_training_settings

When a new subject is created

Define initial parameter values

update_training_settings

After every session ends

Update parameters based on performance

The optional gui_tabs method controls how parameters are displayed when launching a task manually from the GUI.