Skip to content

Latest commit

 

History

History
100 lines (66 loc) · 5.95 KB

README.md

File metadata and controls

100 lines (66 loc) · 5.95 KB

signals

Simple, elegant and flexible stimulus presentation in MATLAB.

Wouldn't it be nice if you could describe the presentation of stimuli in your experiment with simple, concise code? i.e.

  • Something like making a grating drift should be as straightforward as its mathematical description: grating.phase = 2*pi*t*temporalFreq.
  • It should be free from the messy details of how it actually gets presented: no unpredictable loops, tangles of event-handlers, tricky state management nor low-level graphics rendering.
  • ...and yet, still benefit from easy data-logging, parameterisation and remote control.

This is the goal of signals.

Hello, grating!

Here's how you could define a 3Hz drifting grating patch to be presented for half a second (with intervening 1 second blank periods), using signals:

function driftingGrating(t, events, pars, visStim)
grating = vis.grating(t);    % we want a Gabor grating patch
grating.phase = 2*pi*t*3; % with it's phase cycling at 3Hz

stimOff = events.newTrial.delay(0.5); % stimOff occurs 0.5s after new trial starts
events.endTrial = stimOff.delay(1);  % next trial should start 1s after stimOff
grating.show = events.newTrial.to(stimOff);  % stimulus visible between trial onset & stimOff

visStim.grating = grating; % add the stimulus to experiment
end

Now let's present our grating 15 times:

log = exp.runTrials(@driftingGrating, 15);

Note that we didn't specify a position for the grating, nor its spatial frequency, so it will always appear using some defaults (e.g. as defined in vis.gabor(); positioned directly ahead with 1 cyc/°). In fact, we would probably like all our stimulus attributes parameterised so they can be varied, by experiment, by trial, by whatever:

function driftingGrating2(t, events, pars, visStim)
grating = vis.grating(t);    % we want a gabor grating patch
grating.azimuth = pars.azimuth;
grating.altitude = pars.altitude;
grating.spatialFreq = pars.spatialFreq;
grating.phase = 2*pi*t*pars.temporalFreq; % now it's cycling at whatever pars.temporalFreq is

stimOff = events.newTrial.delay(pars.stimDuration); % parameterise stimulus duration
events.endTrial = stimOff.delay(pars.isi);  % parameterise period between stimuli
grating.show = events.newTrial.to(stimOff);  % stimulus visible between trial onset & stimOff

visStim.grating = grating;
end

Now, we're going to need some values for those parameters before we can actually run an experiment. One way is to pass in a MATLAB struct with fields corresponding to each named parameter. But, a simple GUI for configuring them first would be nice:

paramValues = exp.promptForParams(@driftingGrating2);
log = exp.runTrials(@flashedGrating2, paramValues);

The exp.promptForParams function actually calls your presentation definition just to infer what parameters it requires. It will then show a (blocking) GUI requesting those parameters, and return your final choices in an appropriate struct. Finally, we use them to run the experiment presentation, now fully parameterised.

Working with signals

The signals framework is built around the paradigm of functional reactive programming, which can simplify problems that primarily involve dealing with change over time. A signal is an object that represents a value that changes over time. Furthermore, you can apply transformations to signals to derive a new signal whose values are obtained by applying an operation to the values of its input signals.

You can use most of the standard MATLAB operations on signals, with intuitive results, e.g.

% if x and y are signals,
z = x + y;    % z is a new signal that updates with x and y as their sum
c = 2*cos(y); % c will always be twice the cosine of y
posx = x > 0; % posx updates with x, true if x > 0, false otherwise

In each case, these expressions return a new signal whose value will update as any of the source signals change.

Note: these operations actually use the signal mapping functions TODO: make link to below. E.g. sig1 + sig2 is shorthand for sig1.plus(sig2) (or equivalently plus(sig1, sig2)), which ultimately evaluates to sig1.map2(@plus, sig2). Thus, here the plus function is being called on each signal's value.

Useful signal transformations

signal.delay(period) returns a new signal that is a time delayed version of signal. I.e. each time signal's value changes, the resultant signal's value will change to the same value, but period seconds later. period can be a constant or a time-varying signal.

from.to(otherSignal) returns a signal that will go true when from goes true, then false when otherSignal goes true.

signal.at(sampler) returns a new signal that takes the current value from signal each time sampler takes a new true value.

pred.if(trueSig, falseSig) returns a signal that is takes the current value of trueSig when pred is true, or falseSig otherwise.

cond(pred1, value1, pred2, value2, ...) returns a signal that takes the value from the first valueX (with X in 1...N) signal whose corresponding predicate predX is true.

what.keepWhen(pred) returns a signal that takes each new value from what whenever pred is true. i.e. pred acts as a gate to filter values from what.

armOn.setTrigger(releaseOn) TODO

Mapping signals

signal.map(f) returns a new signal whose values result from mapping values in signal using the function f.

inp1.map(inp2, ..., f) returns a new signal where its values result from mapping values from a set of input signals inpX (with X in 1...N) using the function f. Note that the resultant signal updates if any of the source signal change.

signal.scan(f, seed) returns a new signal where its values result from iteratively scanning each new value in signal through function f together with the last resulting value. i.e. this allows you to create a signal which iteratively updates based on the current value and each new piece of information. seed defines the intial value of the result signal.