Programming Tasks for Psychological Experiments using Presentation

This is the evolved version of the old Secret Presentation Programming Tutorial. The idea was to show the process of developing tasks, and have readers go through it themselves, so that they experience all the "Gaussian scaffolding" instead of just the sanitized end version. The book takes you through the process of constructing five "canonical" tasks: Hello World, an Emotional Stroop, an Approach-Avoidance Task, a Conditioning Task, and a Working Memory Task. Each task introduces various techniques and concepts, e.g., basic stimulus presentation, flow control, response handling, controlled randomization via arrays, manipulating visual stimuli via movement, rotation and resizing, titration, transparent layers for complex graphics, line graphics, and using subfunctions.

PDF.

Files

Final versions of the example tasks.

Errata and clarifications

Trigonometry

If the maths involved in the visual working memory task is a bottleneck, what you need is an introduction to trigonometry; see, e.g., Khan Academy. But all you really need is to consider each point as a vector in the usual 2D (X, Y)-space. You can calculate the length of the vector using Pythagoras, and its angle using the arctangent (note: not the arch tangent - the great and powerful first amongst tangents). Once you have the angle, you can add another angle to it, and then use sines and cosines, together with the length, to calculate the rotated vector's new position. This is where the point of the polygon has to go after rotation. As mentioned in the book, I hasten to add that I know this is inefficient: you can also use a rotation matrix. I chose my approach for educational purposes, and there's effectively no time constraint anyway.

Extra stuff

Using port codes for markers in psychophysiology experiments

In many situations you need to send markers to a recording device to synchronize task events with a continuously measured signal. The problem here is that every system will be somewhat different in details, but the general requirements are as follows. First, you'll need to define an Output Port in the Settings->Port tab. Add an Output Port, and then go to properties. Depending on your system, you'll see a list of ports, i.e., hardware connections to other devices. You need to select the port that's connected to the recording device and provide the correct technical specifications. Then you can close the properties and select that hardware port in the pulldown menu to associate it with your Output Port.

In PCL code, you can access that port by assigning it to a variable:

output_port oport = output_port_manager.get_port( 1 );

When you want to send a marker via that port, simply use a subfunction such as this:

sub sendCode(int v) begin
	oport.send_code(0);
	int t1 = clock.time();
	oport.send_code(v);
	loop until clock.time() > t1 + 20 begin	end;
	oport.send_code(0);
end;

This sets the port value to zero to make sure there's no bit-pattern hanging around on it. Then the value is sent to the port, which will hopefully be picked up by the recording device. Before setting the value back to zero, the program waits a brief interval (here 20 ms, which would be long enough to be safe as long as the sampling is faster than 1 / 0.02 = 50 Hz) to make sure to recording device doesn't miss the marker going up and down. The port value is set back to zero to keep it clean for subsequent markers, if the hardware involves a port that remembes the bit representation of port values until reset. Otherwise, in those cases, if you for example sent a 1 followed by a 3, bit-0 would already be high and the 3 would turn into a 2; the next 1, 2 or 3 value wouldn't be registered at all.

This is usually an extra-safe method, but note there are systems (such as the DCCN for you people) where sequences of port values starting with zero are interpreted as command sequences, which will ruin your measurement. For such systems you might have to send only the desired port value, without zeroes or delay intervals.

The marker can be sent immediately before presenting a picture or trial, or immediately before or after other events. I personally strongly prefer keeping marker codes very simple: just one marker, sent immediately before cue or stimulus presentation. I can then reconstruct all other event times, such as responses, by saving all clock times from Presentation. Other people feel that adding other codes is safer; however, note that the added complexity could actually cause potentially very difficult to solve problems, for instance if you end up using the same port value for different events. However you do it, test it very thoroughly before running real subjects.

Triggering the start of the task by MRI pulses

A different way of synchronizing trial-by-trial Presentation output and a continuous signal is by waiting for a trigger on an Input Port before starting the task. This is common in fMRI systems. What happens is, Presentation receives markers and you let it loop around waiting for the N-th scan to arrive. At the moment it receives that scan, it bursts into further action, and you save the clock time. This allows you to later set up the onsets file for the design matrix, so that you know what the modelled BOLD response for given events is at various scan times. First, you have to set up the fMRI Mode Trigger port, as with the Output Port above. In the scenario file, define the scenario_type as fMRI:

scenario_type = fMRI;
pulses_per_scan = 1;
pulse_code = 97;

The pulse_code will depend on the system; usually, only one pulse (i.e., marker) will be sent per scan, but this could differ. Setting the scenario up this way gives you access to the pulse_manager, which you can use in a subfunction as follows:

sub waitForPulse(int N) begin
	int initialPulses = pulse_manager.main_pulse_count();

	# Get first pulse
	loop until pulse_manager.main_pulse_count() > initialPulses begin end;
	first_pulse_time = clock.time();

	# Wait until the N-th pulse arrives
	loop until pulse_manager.main_pulse_count() >= initialPulses + N begin end;
	trigger_time = clock.time();
end;

The procedure is usually to start the task, and let it call the function with the desired starting pulse. After the task has definitely reached the loop, the scanner is turned on, and pulses will start coming in and being counted.

The function checks how many pulses have already been received at the time the function was called, as this isn't necessarily zero. It then waits for the first pulse to arrive after starting the function, and saves the time to a variable (here, I use a variable that would have been declared before the subroutine). It then counts pulses until it reaches the N-th pulse, after which the loop ends and the task continues.

The code then immediately reads the clock time and saves it as the trigger_time. This is redundant with the first_pulse_time but redundancy is good when your measurements are around 500 euros a pop. Other clock times saved as trial data can now be related to the trigger time, and hence to the N-th scan and hence time relative to the start of scanning, as required when specifying onsets.

Word wrapping

Presentation can automatically word-wrap long lines contained in a caption. The code below illustrates how to do that. You might have to go to Settings->Video and select a low-resolution Display Mode to notice any effects.
#Header
default_font_size = 32;

# SDL
begin;

text {
	caption = "dum";
} text0;

trial {
	trial_type = fixed;
	trial_duration = 1000;
	stimulus_event {
		picture {
			text text0;
			x = 0; y = 0;
		};
	};
} trial0;

# PCL
begin_pcl;

# Make a long caption
string longcap = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.";
text0.set_caption(longcap);

# Present text with no wordwrap
text0.set_max_text_width(0);
text0.set_max_text_height(0);
text0.redraw();
trial0.present();

# Present text with horizontal wordwrap
text0.set_max_text_width(300);
text0.set_max_text_height(0);
text0.redraw();
trial0.present();

# Present text with vertical scaling
text0.set_max_text_width(300);
text0.set_max_text_height(200);
text0.redraw();
trial0.present();