Bpod Protocol

This page includes all code used in the fmon_task.m Bpod protocol for the 100-0 task, along with a graph describing the flow of the Bpod state machine.

The protocol is designed to be run in conjunction with Bonsai, which uses realtime video tracking to provide SoftCodes to Bpod over a serial connection. APP_SoftCode1 indicates that the mouse has crossed the choice boundary on the left side, APP_Softcode3 indicates that the mouse has crossed on the right, and APP_SoftCode2 indicates that the mouse has yet to cross the choice boundary.

State Machine Flow

graph TD
    OdorantState --> OdorLeft
    OdorantState --> OdorRight
    OdorantState --> OdorOmitLeft
    OdorantState --> OdorOmitRight
    OdorLeft --> WaitForInitPoke
    OdorRight --> WaitForInitPoke
    OdorOmitLeft --> WaitForInitPoke
    OdorOmitRight --> WaitForInitPoke
    WaitForInitPoke -->|Port3In| StateOnInitPoke
    StateOnInitPoke --> GoLeft
    StateOnInitPoke --> GoRight
    GoLeft -->|APP_SoftCode1| CorrectLeft
    GoLeft -->|APP_SoftCode3| NoReward
    GoRight -->|APP_SoftCode1| NoReward
    GoRight -->|APP_SoftCode3| CorrectRight
    CorrectLeft -->|Port1In| LeftReward
    CorrectRight -->|Port2In| RightReward
    NoReward --> ITI
    LeftReward --> Drinking
    RightReward --> Drinking
    Drinking -->|Tup, Port1Out, Port2Out| ITI
    ITI --> OdorantState

MATLAB Code

The following code chunks are an exact copy of fmon_task.m, broken down into sections for ease of reference.

Initialize Bpod and Start Bonsai

Initializes the Bpod protocol definition and executes an external Python script to start Bonsai. The Python script uses the pyautogui and pygetwindow libraries to start whichever file is loaded in Bonsai.

function fmon_task
%% Initialize Bpod
global BpodSystem
S = BpodSystem.ProtocolSettings;

%% Start Bonsai
% Run Bonsai connect Python Script
[~,~] = system('start C:\ProgramData\Anaconda3\python.exe D:\fmon-bpod\connect_gui.py');
% Wait for Bonsai to start
java.lang.Thread.sleep(1000);

Session Timer

MATLAB timer. Pulls from the “Session Length” field in “Session Info” tab of FMON-GUI.

%% Set Up Session Timer
% Get duration from GUI, or use default of 40 minutes.
if evalin('base', 'exist(''session_duration'', ''var'')')
    session_duration = evalin('base', 'session_duration');
else
    session_duration = 40;
end

persistent t  % Declaring t as global so it can be accessed outside function

% If timer from cancelled session is running, stop and delete it.
if exist('t', 'var') == 1 && isa(t, 'timer')
    if strcmp(t.Running, 'on')
        stop(t);
        disp('Previously started timer stopped.')
    end
    delete(t);
end

% Initialize session timer.
t = timer;
t.StartDelay = session_duration * 60;  % time in seconds
% timeUp is defined at end of this file.
t.TimerFcn = @(obj, event)timeUp(obj, event, session_duration);  

start(t);

Set up Reward Timings

Sets water valve timings. Pulls valve timings from the “H2O” tab of the fmon_prefs.m GUI.

%% Set Reward amounts
% Read variables from workspace, supplied by fmon_prefs.m GUI.
% If variables don't exist, set to defaults (.1 seconds)
if evalin('base', 'exist(''LeftValveTime'', ''var'')')
    LeftValveTime = evalin('base', 'LeftValveTime');
else
    LeftValveTime = 0.1;
end

if evalin('base', 'exist(''RightValveTime'', ''var'')')
    RightValveTime = evalin('base', 'RightValveTime');
else
    RightValveTime = 0.1;
end

% Time to wait before lick port out is confirmed
PortOutDelay = .5;

% Time to wait for poke after decision. On timeout, ITI begins.
PokeTimer = 5;

Generate Trials

Code to generate trial sequence. Below code is for up to 200 trials total. Percentage of omission trials is defined in the fmon_prefs.m GUI. Additionally, code to ensure that no trial type is repeated more than 3 times in a row. Finally, code to construct the ITI list, with min and max ITI times (optionally) defined in the GUI.

%% Define trials
nLeftTrials = 100;
nRightTrials = 100;
pctOmission = evalin('base', 'pct_omission') / 100;
nOmissionTrials = round((nLeftTrials + nRightTrials) * pctOmission);  % Some percentage of trials are omission trials.
nOmissionDiff = round(0.5 * nOmissionTrials);  % Half of the omission trials, to substract from left and right trials.
TrialTypes = [ones(1, nLeftTrials-nOmissionDiff) ones(1, nRightTrials-nOmissionDiff)*2 ones(1, round(nOmissionTrials/2))*3 ones(1, round(nOmissionTrials/2))*4];  % 1 = Left, 2 = Right, 3 = Omission Left, 4 = Omission Right

% Ensure that trial type never repeats more than maxRepeats times
maxRepeats = 3; % maximum repeat limit

% A function to check if any element repeats more than maxRepeats times
checkRepeats = @(v, m) any(conv(double(diff(v) == 0), ones(1, m), 'valid') == m);

% Randomly permute vector until no element repeats more than maxRepeats times
while true
    vec_perm = TrialTypes(randperm(length(TrialTypes)));
    if ~checkRepeats(vec_perm, maxRepeats)
        break
    end
end

assignin('base', 'vec_perm', vec_perm);

TrialTypes = vec_perm;
BpodSystem.Data.TrialTypes = []; % The trial type of each trial completed will be added here.
MaxTrials = length(TrialTypes);%nLeftTrials + nRightTrials + nOmissionTrials;

%% Build ITI list
% Read variables from workspace, supplied by fmon_prefs GUI.
if evalin('base', 'exist(''min_iti'', ''var'')')
    min_iti = evalin('base', 'min_iti');
else
    min_iti = 1;
end

if evalin('base', 'exist(''max_iti'', ''var'')')
    max_iti = evalin('base', 'max_iti');
else
    max_iti = 5;
end

% Create ITI list
iti_list = round((max_iti-min_iti) .* rand(1,length(TrialTypes)) + min_iti);

Trial Outcome Plots

Generates a visualization of trial type and outcome that updates after the completion of each trial.

BpodSystem.ProtocolFigures.OutcomePlotFig = figure('Position', [50 540 1000 250],'name','Outcome plot','numbertitle','off', 'MenuBar', 'none', 'Resize', 'off');
BpodSystem.GUIHandles.OutcomePlot = axes('Position', [.075 .3 .89 .6]);
TrialTypeOutcomePlot(BpodSystem.GUIHandles.OutcomePlot,'init',TrialTypes);

Begin Trial Loop

Beginning of for loop iterating over trials. Additionally, definitions of serial messages to be sent to olfactometer and final valves. The ‘switch’ control statement changes the definitions of StateOnInitPoke and OdorantState according to which TrialType is currently being executed.

%% Main trial loop
for currentTrial = 1:MaxTrials
    
    % Valve Module serial messages, 1 = Odor, 2 = Omission, 3 = Reset
    LoadSerialMessages('ValveModule1', {['B' 15], ['B' 195], ['B' 0]});  % Left valves: 15 = Odor, 195 = Omission, 0 = Reset
    LoadSerialMessages('ValveModule2', {['B' 15], ['B' 195], ['B' 0]});  % Right valves: 15 = Odor, 195 = Omission, 0 = Reset
    LoadSerialMessages('ValveModule3', {['B' 3], ['B' 0]});  % Final Valves
    
    % Determine trial-specific state matrix fields
    switch TrialTypes(currentTrial)
        case 1
            StateOnInitPoke = 'GoLeft';
            OdorantState = 'OdorLeft';
        case 2
            StateOnInitPoke = 'GoRight'; 
            OdorantState = 'OdorRight';
        case 3
            StateOnInitPoke = 'GoLeft'; 
            OdorantState = 'OdorOmitLeft';
        case 4
            StateOnInitPoke = 'GoRight'; 
            OdorantState = 'OdorOmitRight';
    end

States

Definitions of each state in the experiment. The structure of each state is straightforward:

  • ‘Timer’ sets a timer in seconds (‘Tup’ means timer has expired)
  • ‘StateChangeConditions’ takes argument pairs of events and the resulting state change.
    • e.g. {‘Tup’, ‘WaitForInitPoke’} means when the timer has expired, the state changes to WaitForInitPoke.
    • Each state can take an arbitrary number of ‘StateChangeConditions’ argument pairs.
    % Initialize new state machine description
    sma = NewStateMachine(); 
    
    % State definitions    
    sma = AddState(sma, 'Name', 'Reset', ...
        'Timer', .5,...
        'StateChangeConditions', {'Tup', OdorantState},...
        'OutputActions', {'ValveModule1', 3, 'ValveModule2', 3, 'ValveModule3', 2});  % Reset all valves to 0V
    
    sma = AddState(sma, 'Name', 'OdorLeft', ...
        'Timer', 1,...
        'StateChangeConditions', {'Tup', 'WaitForInitPoke'},...
        'OutputActions', {'ValveModule1', 1, 'ValveModule2', 2});  % Left odor, right omission

    sma = AddState(sma, 'Name', 'OdorRight', ...
        'Timer', 1,...
        'StateChangeConditions', {'Tup', 'WaitForInitPoke'},...
        'OutputActions', {'ValveModule1', 2, 'ValveModule2', 1});  % Left omission, right odor
    
    sma = AddState(sma, 'Name', 'OdorOmit', ...
        'Timer', 1,...
        'StateChangeConditions', {'Tup', 'WaitForInitPoke'},...
        'OutputActions', {'ValveModule1', 2, 'ValveModule2', 2});  % Both omission valves open to mask audio cue

    sma = AddState(sma, 'Name', 'WaitForInitPoke', ...
        'Timer', 0,...
        'StateChangeConditions', {'Port3In', StateOnInitPoke},...  % Wait for initiation port poke
        'OutputActions', {});

    sma = AddState(sma, 'Name', 'GoLeft', ...
        'Timer', .1,...
        'StateChangeConditions', {L_sector, 'CorrectLeft', R_sector, 'NoReward'},...
        'OutputActions', {'ValveModule3', 1});  % Final valves open
    
    sma = AddState(sma, 'Name', 'GoRight', ...
        'Timer', .1,...
        'StateChangeConditions', {L_sector, 'NoReward', R_sector, 'CorrectRight'},...
        'OutputActions', {'ValveModule3', 1});  % Both Final valves open
    
    sma = AddState(sma, 'Name', 'GoLeftOmit', ...
        'Timer', .1,...
        'StateChangeConditions', {L_sector, 'CorrectLeft', R_sector, 'NoReward'},...
        'OutputActions', {'ValveModule3', 1});  % Left Final valve opens
    
    sma = AddState(sma, 'Name', 'GoRightOmit', ...
        'Timer', .1,...
        'StateChangeConditions', {L_sector, 'NoReward', R_sector, 'CorrectRight'},...
        'OutputActions', {'ValveModule3', 1});  % Right Final valve opens

    sma = AddState(sma, 'Name', 'CorrectLeft', ...
        'Timer', PokeTimer,...
        'StateChangeConditions', {'Tup', 'ITI', 'Port1In', 'LeftReward'},...
        'OutputActions', {});  % On decision, reset all valves

    sma = AddState(sma, 'Name', 'CorrectRight', ...
        'Timer', PokeTimer,...
        'StateChangeConditions', {'Tup', 'ITI', 'Port2In', 'RightReward'},...
        'OutputActions', {});  % On decision, reset all valves

    sma = AddState(sma, 'Name', 'NoReward', ... 
        'Timer', PokeTimer,...
        'StateChangeConditions', {'Tup', 'ITI', 'Port1In', 'ITI', 'Port2In', 'ITI', 'Port3In', 'ITI'},...
        'OutputActions', {'ValveModule1', 3, 'ValveModule2', 3, 'ValveModule3', 2}); % On decision, reset all valves
    
    sma = AddState(sma, 'Name', 'LeftReward', ...
        'Timer', LeftValveTime,...
        'StateChangeConditions', {'Tup', 'Drinking'},...
        'OutputActions', {'ValveState', 1, 'ValveModule1', 3, 'ValveModule2', 3, 'ValveModule3', 2});  % On left poke give water & reset valves.
    
    sma = AddState(sma, 'Name', 'RightReward', ...
        'Timer', RightValveTime,...
        'StateChangeConditions', {'Tup', 'Drinking'},...
        'OutputActions', {'ValveState', 2, 'ValveModule1', 3, 'ValveModule2', 3, 'ValveModule3', 2});  % On right poke give water & reset valves.
    
    sma = AddState(sma, 'Name', 'Drinking', ...
        'Timer', 5,...
        'StateChangeConditions', {'Tup', 'ITI', 'Port1Out', 'ConfirmPortOut', 'Port2Out', 'ConfirmPortOut'},... 
        'OutputActions', {});
    
    sma = AddState(sma, 'Name', 'ConfirmPortOut', ...
        'Timer', PortOutDelay,...
        'StateChangeConditions', {'Tup', 'ITI', 'Port1In', 'Drinking', 'Port2In', 'Drinking'},...
        'OutputActions', {});
    
    sma = AddState(sma, 'Name', 'ITI', ... 
        'Timer', iti_list(currentTrial),...
        'StateChangeConditions', {'Tup', 'exit'},...
        'OutputActions', {}); 

End Trial Loop

Code to save data from the most recent trial. This code is Bpod boilerplate and should not be modified unless the user knows exactly what they are doing.

T = BpodTrialManager;
    T.startTrial(sma)
    RawEvents = T.getTrialData;
    if ~isempty(fieldnames(RawEvents)) % If trial data was returned
        BpodSystem.Data = AddTrialEvents(BpodSystem.Data,RawEvents); %  Computes trial events from raw data
        BpodSystem.Data.TrialSettings(currentTrial) = S; % Adds the settings used for the current trial to the Data struct (to be saved after the trial ends)
        BpodSystem.Data.TrialTypes(currentTrial) = TrialTypes(currentTrial); % Adds the trial type of the current trial to data
        UpdateOutcomePlot(TrialTypes, BpodSystem.Data);
        SaveBpodSessionData; % Saves the field BpodSystem.Data to the current data file
    end
    HandlePauseCondition; % Checks to see if the protocol is paused. If so, waits until user resumes.
    if BpodSystem.Status.BeingUsed == 0
        return
    end
end

Update Outcome Plot

This function updates the trial outcome plot while the session is running. Outcome plot displays trial types and whether mouse gave a correct or incorrect choice.

function UpdateOutcomePlot(TrialTypes, Data)
    global BpodSystem
    Outcomes = zeros(1,Data.nTrials);
    for x = 1:Data.nTrials
        if ~isnan(Data.RawEvents.Trial{x}.States.Drinking(1))
            Outcomes(x) = 1;
        else
            Outcomes(x) = 3;
        end
    end
    TrialTypeOutcomePlot(BpodSystem.GUIHandles.OutcomePlot,'update',Data.nTrials+1,TrialTypes,Outcomes);

timeUp Function

When session duration, as defined in the “Session Info” tab of the GUI, expires, the timeUp function is executed. This function stops the Bpod protocol, stops Bonsai, then runs a data ouput script to summarize and save the session’s data.

%% Execute when time is up:
function timeUp(obj, event, duration)
    disp(num2str(duration) + " minutes have elapsed! The session has ended.");  % Print to console, maybe make this an alert
    RunProtocol('Stop');  % Stop the protocol
    java.lang.Thread.sleep(1000);
    [~,~] = system('start C:\ProgramData\Anaconda3\python.exe D:\fmon-bpod\disconnect_gui.py'); % Stop Bonsai
    disp('Running data output script...');
    run('D:\fmon-bpod\fmon_data_output_aw.m'); % Run data output script

Bpod Resources