Experiment example (Psychtoolbox): Cued left/right button press

Description

A motor experiment for the KIT MEG system. A low-contrast arrow appears at the fixation dot and the participant presses a button with the hand it points to: left box = left hand, right box = right hand (any button on the box). It gives lateralized movement-related fields, cue-evoked responses, accuracy and reaction times. Because the only visual event is a small, low-contrast arrow on a mean-grey screen, it is suitable for participants with epilepsy.

Code: experiments/psychtoolbox/button_press_lr (start with main.m).

Left and right arrow cues with the red fixation dot

The two cues (magnified). Default: 3 deg arrow, grey 77 on background 128.

Left and right cues are equally frequent, in a constrained random order (never more than three of the same hand in a row). Cue onsets are jittered uniformly between SOA min and max, and the run length is still exact (the SOAs are evenly spaced and shuffled).

Timeline of cues, response windows, presses and trigger pulses

Each cue is shown for 0.5 s; presses within the 1.5 s response window are scored. Every press, wherever it falls, gets its own trigger.

Responses and triggers

Button presses

The RESPONSEPixx boxes are read through the DataPixx digital inputs, with the lab’s mapping from getButtonColor.m: DIN bits 0-4 are the right box (red, yellow, green, blue, white) and bits 5-9 the left box, and a pressed button reads as 1.

  • Hardware timestamps. In production the DataPixx DIN log records every input change with a DataPixx hardware timestamp, so press times do not depend on how often MATLAB checks the buttons. If the log cannot be set up, the code falls back to reading the inputs once per frame (din_method in the logs).

  • One clock for RTs. At every trigger frame a DataPixx marker records the frame time on the same hardware clock. Reaction times and press-to-trigger delays are therefore measured on one clock.

  • Marker check. Each marker is checked against the running offset between the DataPixx clock and the Psychtoolbox clock. A marker that latched one frame early (after a missed flip) is detected and replaced, and the number of replacements is logged (n_marker_mismatches).

  • Rehearsal. The rehearsal and debug presets use the keyboard instead: F = left hand, J = right hand, with Psychtoolbox key-queue timestamps.

Every press is classified against the most recent cue:

  • first: the first press in the response window. This is the trial’s response, scored correct or wrong hand, with its RT.

  • extra: a further press in the same window.

  • outside: any press with no open window (before the first cue or after the window closed).

Trigger protocol

Single-channel DataPixx pixel mode, same RGB codes as trigger_template.m.

Event

KIT channel

MNE channel

Pixel RGB

When

cue_left

224

MISC 001

[4 0 0]

First frame of a left arrow

cue_right

225

MISC 002

[16 0 0]

First frame of a right arrow

press_left

226

MISC 003

[64 0 0]

First free frame after a left-box press

press_right

227

MISC 004

[0 1 0]

First free frame after a right-box press

run_start

228

MISC 005

[0 4 0]

Start of the 2 s baseline

run_end

229

MISC 006

[0 16 0]

End of the last SOA (completed runs only)

Timing robustness on the stimulus PC

  • 30 trigger-free warm-up frames (0.25 s, cfg.warmupFrames) follow SPACE before the run-start pulse. The first flips after the start screen can return a bad timestamp on the stimulus PC (PTB “beamposition timestamping computed an impossible stimulus onset”); the warm-up keeps that away from every trigger frame.

  • Every timing check outside tolerance is logged in the *_ok columns and printed as a warning. Timing never stops a run.

  • A pulse whose flip timestamps give less than half a frame (impossible for a 1-frame pulse) is a timestamp error, not a trigger error: it is counted in RunInfo.n_timestamp_anomalies in beh.json and never stops the run.

The run is a continuous frame loop at 120 Hz. Each trigger lasts one frame, and two trigger frames are never adjacent, so pulses on the same channel can never merge. A press trigger therefore goes on the first frame that:

  • is not next to another trigger frame, and

  • does not precede a cue or run_end frame.

That is normally 1-2 frames (8-17 ms) after the press. The exact delay from the press to its trigger frame is logged as trigger_delay. The onset-filling script subtracts it, so a press row’s onset is the moment the button went down. A press in the last two frames before run_end gets no trigger. It stays in the press log but not in events.tsv.

Safety and design limits

Hard limits in cfg.safety of button_lr_config.m, enforced in the launcher and at the start of every run:

Parameter

Default / limit

Why

Arrow contrast

0.4 / 0.1-0.6

Low contrast; mean grey background never changes.

Arrow size

3 deg / 0.4-6 deg

Central cue (under 1 % of the screen at the 6 deg limit).

Cue duration

0.5 s / 0.2-2.0 s

Response window

1.5 s / at least 0.5 s and at least the cue

Must end at least 0.5 s before the next cue.

SOA (jittered)

3-4 s / 1.5-10 s

At most 0.67 cues per second; time for motor fields to settle.

Run length

at most 15 min

The screen is otherwise static. Psychtoolbox splash screens are disabled and the screen fades to black over 2 s before the window closes, so set the projector desktop background to black.

Running the experiment

The console prints a progress line after each trial (Trial 17/80 done, 63 left, about 3 min 41 s to go). A run stopped early keeps the standard file names with RunStatus aborted in beh.json; events.tsv has exactly one row per trigger actually sent, so its onsets can be filled from the MEG recording and the completed trials analysed.

cd experiments/psychtoolbox/button_press_lr
main              % launcher (production preset selected)
main('validate')  % headless self-test, no Psychtoolbox needed
Launcher with run parameters and planned run length

The launcher recomputes the run length whenever a value changes (press Enter or Tab). Preview cues shows both arrows. A production start asks for confirmation that KIT recording is running and both boxes are connected.

Presets

Preset

Responses

Display and checks

Output folder

Production

Response boxes (DIN log)

DataPixx triggers, full screen, 120 Hz, timing logged, never fatal

data/

Full rehearsal

Keyboard F / J

No DataPixx, full screen

data_rehearsal/

Debug

Keyboard F / J

Windowed, 50 px trigger patch, faults logged only

data_rehearsal/

run length = 2 s baseline + nTrials x (SOA min + SOA max) / 2

Cues

Run length (SOA 3-4 s)

Note

40

2 min 22 s

80

4 min 42 s

Default (40 per hand)

120

7 min 02 s

200

11 min 42 s

Scripted launch (limits still apply):

spec = struct('subject', 'P01', 'session', '01', 'mode', 'debug');
spec.params = struct('nTrials', 10);
main(spec)

Before the session

  1. Connect both response boxes and test them (see the response-box page).

  2. Run main('validate') after any change to the code or configuration.

  3. Check channels 224-229 with test_all_meg_channels_triggers.m if the setup or wiring changed.

  4. Explain the task; a short debug run with the keyboard is a good practice block.

  5. Start KIT acquisition, then press SPACE.

Saved data (BIDS)

data/
  dataset_description.json, README
  sub-P01/ses-01/meg/sub-P01_ses-01_task-buttonlr_run-01_events.tsv / .json
  sub-P01/ses-01/beh/sub-P01_ses-01_task-buttonlr_run-01_beh.tsv / .json
  stimuli/task-buttonlr/cue-left_size-300deg_contrast-040_h-<hash>.png (and right)
  sourcedata/sub-P01/ses-01/beh/..._desc-presses_beh.tsv        (every press)
  sourcedata/sub-P01/ses-01/beh/..._desc-snapshot_beh.mat, ..._desc-matlabconsole_log.txt

events.tsv has one row per trigger pulse, sorted by pulse time.

Column

Content

onset, sample

n/a until filled from the recording (onset = pulse - trigger_delay).

duration

Cue rows: measured arrow duration. Other rows: 0.

trial_type, value, channel

Event name and KIT channel (value = channel).

hand

Cue rows: the cued hand. Press rows: the hand that pressed.

presentation_index

Cue number; for press rows, the trial whose window contained the press (0 = outside).

response_type

Press rows: first, extra or outside.

correct

Cue rows: 1/0 for the response, n/a if none. Press rows: 1/0 in a window.

response_time

Press minus cue onset on the DataPixx clock (s).

button

Box and colour (or keyboard key).

stim_file

Cue image in stimuli/.

onset_from_run_start

Trigger frame time minus run-start frame time (stimulus PC).

trigger_delay

0 for cues and run markers; press-to-trigger-frame delay for presses.

hardware_emitted

1 when DataPixx pixel mode was active.

events.json describes every column. It also holds:

  • "TriggerMode": "single_channel", the channel table and the expected pulse count per channel;

  • the trigger definitions and the onset rule;

  • the response device, response clock and button mapping;

  • the cue geometry;

  • RunStatus and RunComplete.

beh.tsv has one row per cue: planned and actual onset, cue duration, pulse check, the response (hand, button, RT, correct), the number of presses in the window, and the DataPixx marker. beh.json adds a summary (correct, wrong hand, missed, median RT). The desc-presses table in sourcedata lists every press, including any whose trigger was never drawn because the run ended or was aborted first.

Preprocessing

python experiments/psychtoolbox/button_press_lr/fill_events_onsets.py \
    --events data/sub-P01/ses-01/meg/sub-P01_ses-01_task-buttonlr_run-01_events.tsv \
    --raw    <recording>.con \
    --out    <BIDS root>/sub-P01/ses-01/meg/sub-P01_ses-01_task-buttonlr_run-01_events.tsv

The script matches every logged pulse one-to-one with the recorded pulses (lab detector, MISC 001-MISC 008) and sets onset = pulse - trigger_delay. It refuses to write anything on any mismatch. Run it after MNE-BIDS conversion.

  • Motor fields: epoch on press_left/press_right rows with response_type == first and correct == 1. These onsets are the button presses themselves.

  • Cue responses: epoch on cue_left/cue_right rows and add the projector lag (+8.36 ms).

Validation

main('validate') checks:

  • the trigger map, presets and limits, and 9 refused settings;

  • DIN decoding: rising edges, held buttons, masking and polarity;

  • press classification (first/extra/outside, correct, RT);

  • cue geometry, direction and contrast;

  • 120 random schedules;

  • every output file for a completed and an aborted run.

During development the frame loop was exercised against a simulated 120 Hz Psychtoolbox and DataPixx with a scripted participant. The participant made a correct response, a wrong-hand response, a miss, an extra press and an out-of-window press, in both DIN-log and keyboard mode, and also during an abort. The simulation checked that:

  • every pulse lasted one frame, and no two trigger frames were adjacent;

  • each arrow was visible for exactly 60 frames;

  • the pulse sequence matched events.tsv;

  • RTs and trigger delays were exact, even with every third DataPixx marker latched a frame early;

  • the onset-filling script put press onsets within 0.33 ms of the true press times (1 ms sampling).

Confirm on the rig with a recorded rehearsal before the first participant.