-
Notifications
You must be signed in to change notification settings - Fork 0
Protocols
An experimental protocol consists of a series of epochs, each of which can apply a different set of stimuli to, and record responses from, the devices defined by the current rig configuration. The number and behavior of epochs is controlled by a set of mostly user-editable parameters defined by the protocol.
To define a new protocol you need to create a sub-class of the SymphonyProtocol class.
Your sub-class should:
- Uniquely identify the protocol.
- Define any user-modifiable parameters and their default values.
- Override at least the prepareEpoch() method.
Identify the protocol using constant properties of the SymphonyProtocol sub-class.
properties (Constant)
identifier = 'io.github.symphony-das.MyProtocol'
version = 1
displayName = 'My Protocol'
end
The parameters of a protocol should be defined using standard MATLAB properties. A user interface is automatically generated for editing the parameters based on their default values:
properties
scale = 2.3 % double/single values are editable via a text box
numberOfEpochs = uint8(4) % integer type enables stepper controls in addition to the text box
foo = 'bar' % string values are editable via a text box
shapes = {'Ball', 'Box', 'Cone'} % a cell array of strings appears as a pop-up menu
steps = [1, 2, 3] % a vector of numeric values, editable as a comma-separated list in a text box
end
Any parameters that should not be editable by the user and not persisted should be created as hidden properties
properties (Hidden)
myHiddenProperty = 10.0
end
When defining a protocol's properties you can indicate that any number of them are derived from other properties and therefore should not be editable. This is done using MATLAB's concept of "dependent" properties:
properties
a = uint8(1)
b = uint8(2)
end
properties (Dependent = true, SetAccess = private)
c;
end
methods
function c = get.c(obj)
c = obj.a + obj.b;
end
end
You may also optionally specify additional information regarding each parameter by overriding the parameterProperty() method:
methods
function p = parameterProperty(obj, parameterName)
% Call the base method to create the property object.
p = parameterProperty@SymphonyProtocol(obj, parameterName);
switch parameterName
case 'preTime'
p.units = 'ms';
p.description = 'The leading duration of the stimulus';
case 'pulseAmplitude'
p.units = 'mV';
end
end
end
The behavior of an experimental protocol is defined by overriding methods of the SymphonyProtocol base class.
The keys points where you can customize behavior are:
-
prepareRig()
:- override this method to prepare the rig for running protocols, e.g. set device background values.
-
prepareRun()
:- override this method to do any setup before any epochs have been run, e.g. open a figure.
-
prepareEpoch()
:- override this method to add stimuli, parameters, keywords, etc. to each epoch produced by the protocol.
-
completeEpoch()
:- override this method to perform any post-epoch clean up, analysis, etc.
-
continueQueuing()
:- override this method to indicate if the protocol should continue queuing epochs.
-
continueRun()
:- override this method to indicate if the protocol should continue running.
-
completeRun()
:- override this method to perform any clean up, post-analysis, etc. after all epochs have been run.
All overriden methods should call the base class method first before any user code.
Stimuli should be generated by using stimulus generators. Stimulus generators generate stimulus object that may be added to epochs via the addStimulus() method. See the simple protocol below for an example of how to do this.
When the parameters of a protocol are edited in the user interface it is possible to show samples of the stimuli that would be generated by the current parameters. To provide sample stimuli you need to override the sampleStimuli method of SymphonyProtocol and return one or more sample stimuli. The return value should be a one-dimensional cell array of stimulus objects generated via a stimulus generator. See the simple protocol below for an example of how to do this.
If you do not wish to have any samples displayed then do not define the sampleStimuli method in your protocol. The default behavior is to return an empty cell array which will suppress the display of the sample axes. You could do this conditionally in your sampleStimuli method but the decision to show or hide the axes is only made when the protocol editor is first opened.
If a protocol requires that devices with specific names are available then it can indicate these by overriding the requiredDeviceNames method. For example, to indicate that a device named 'Device1' and another named 'Device2' are required:
function rdn = requiredDeviceNames(obj)
rdn = {'Device1', 'Device2'};
end
If the rig configuration currently chosen by the user does not have devices with any of the given names then the "Start" button will be disabled. The status message will then indicate the name of the missing device.
Whenever a protocol's parameters are changed the prepareRig() method will be called.
See [Rig Configurations](Rig Configurations) for more information.
To add an inter-epoch interval, override the queueEpoch() method and call queueInterval(durationInSeconds) after calling the base class method:
function queueEpoch(obj, epoch)
% Call the base method to queue the actual epoch.
queueEpoch@SymphonyProtocol(obj, epoch);
% Queue the inter-pulse interval after queuing the epoch.
if obj.interpulseInterval > 0
obj.queueInterval(obj.interpulseInterval);
end
end
By default a protocol will prepare epochs irrespective of the number of epochs that have completed. If your protocol cannot prepare an epoch until performing online analysis on a previous epoch or block of epochs, your protocol must explicitly wait to continue queuing until that epoch or block of epochs has completed. You may wait to continue queuing by overriding the waitToContinueQueuing() method:
function waitToContinueQueuing(obj)
% Call the base method.
waitToContinueQueuing@SymphonyProtocol(obj);
% Wait until the previous epoch is complete.
while obj.numEpochsQueued > obj.numEpochsCompleted && strcmp(obj.state, 'running')
pause(0.01);
end
end
Symphony will initially search for protocols in the directory defined by the config.protocolsDir variable of your symphonyrc script. Each protocol must be contained within a directory of the same name as the protocol class itself.
The following class implements a simple protocol which will output a one second stimulus for each epoch until the user-defined limit is reached or the user stops the protocol. It also opens a figure window to display the response of each epoch.
classdef SimpleProtocol < SymphonyProtocol
properties (Constant)
identifier = 'io.github.symphony-das.SimpleProtocol'
version = 1
displayName = 'Simple Protocol'
end
properties
numberOfEpochs = uint8(4);
end
methods
function stimuli = sampleStimuli(obj)
% Return sample stimuli based on the current paramaters.
stimuli = cell(1, obj.numberOfEpochs);
for i = 1:obj.numberOfEpochs
stimuli{i} = obj.generateStimulus();
end
end
function prepareRun(obj)
% Call the base class method which clears the figures.
prepareRun@SymphonyProtocol(obj);
% Open a figure that displays the response from the most recent epoch.
obj.openFigure('Response');
end
function stim = generateStimulus(obj)
% Construct a pulse stimulus generator.
p = PulseGenerator();
% Assign generator properties.
p.preTime = 0;
p.stimTime = 1e3;
p.tailTime = 0;
p.amplitude = 0;
p.mean = 0;
p.sampleRate = obj.sampleRate;
p.units = 'V';
% Generate the stimulus object.
stim = p.generate();
end
function prepareEpoch(obj, epoch)
% Call the base class method which sets up default backgrounds and records responses.
prepareEpoch@SymphonyProtocol(obj, epoch);
% Add a stimulus to the test device.
stim = obj.generateStimulus();
epoch.addStimulus('test-device', stim);
end
function keepQueuing = continueQueuing(obj)
% First check the base class method to make sure the user hasn't paused or stopped the protocol.
keepQueuing = continueQueuing@SymphonyProtocol(obj);
if keepQueuing
% Check if we've reached the limit.
keepQueuing = obj.numEpochsQueued < obj.numberOfEpochs;
end
end
function keepGoing = continueRun(obj)
% First check the base class method to make sure the user hasn't paused or stopped the protocol.
keepGoing = continueRun@SymphonyProtocol(obj);
if keepGoing
% Check if we've reached the limit.
keepGoing = obj.numEpochsCompleted < obj.numberOfEpochs;
end
end
end
end
Save this to SimpleProtocol.m in a folder named 'SimpleProtocol' and place the folder in the protocols directory defined by your symphonyrc script.