Experiment example (Psychtoolbox): Flipped squares, photosensitivity-safe

Description

A passive visual pattern-reversal experiment for the KIT MEG system. A low-contrast checkerboard on a mean-luminance grey background fades in, then reverses polarity continuously (light and dark checks swap) for the whole block, then fades out. The participant fixates a static red dot. There is no task and no response box.

The task was designed for a participant with epilepsy and is epilepsy-safe by default (small central square, every limit below). For a participant without epilepsy the launcher also offers a full-screen checkerboard (see Participant profile).

  • Pattern A: top-left check light.

  • Pattern B: the same checkerboard with light and dark swapped.

Both patterns contain exactly the same luminances in equal areas, so a reversal changes only the spatial arrangement of light and dark, never the mean luminance. Each reversal evokes a pattern-reversal visual evoked field (the MEG counterpart of the clinical pattern-reversal VEP, P100m), with minimal photic stimulation.

Code: experiments/psychtoolbox/flipped_squares (start with main.m; see Files).

Pattern A and pattern B checkerboards on a grey background with a red fixation dot

The two polarities with default settings: 4 x 4 checks, Michelson contrast 0.40, 10 deg (2.5 deg checks), on grey 128. The red fixation dot is visible throughout the run.

Presentation sequence

After the operator presses SPACE, one continuous block runs:

  1. run start trigger (KIT 228), then 2 s of grey with the fixation dot;

  2. pattern A fades in over 1 s (raised cosine; trigger KIT 227 on the first fade frame);

  3. half a second later the first reversal to B, then a reversal every 0.5 s (2 reversals per second), strictly alternating A and B. Every reversal is abrupt (one frame) and carries the channel of the new polarity: KIT 225 for a reversal to B, KIT 224 for a reversal to A;

  4. half a second after the last reversal the pattern fades out over 1 s (trigger KIT 226 on the first fade frame);

  5. 2 s of grey, then the run end trigger (KIT 229, completed runs only).

A completed run therefore contains nReversals + 4 pulses (244 by default), each with exactly one row in events.tsv.

Timeline of baseline, fade-in, reversals, fade-out and the corresponding trigger pulses

Polarity of the top-left check (A up, B down) and the trigger pulses for a short example block of 8 reversals. Each trigger is a single 120 Hz frame.

Frame-by-frame contrast during the fade-in and around one reversal

Frame-level view. Left: the 1 s fade-in (120 frames); the pattern reaches full contrast 1 s after the fade-in trigger. Right: a reversal swaps the polarity from one frame to the next; the trigger is on the first frame of the new polarity and cleared on the next frame.

The fades scale only the check contrast around the background, so the mean luminance stays constant during them as well. Reversals are abrupt, as in the clinical pattern-reversal VEP, so the evoked response is sharply time-locked to each reversal trigger. Reversals are scheduled on a fixed grid relative to the run-start frame (2 s + fade + k x 0.5 s), so timing errors never accumulate across a run. Between reversals nothing is redrawn.

Photosensitivity safety

Important

This protocol is intended for a participant with epilepsy. Obtain the responsible clinician’s approval of the stimulus (use *Preview stimuli*) and of the safety limits before the session, and record that approval with the study protocol. The limits are a conservative reading of published guidance; they are not a clinical guarantee.

Published guidance on visually induced seizures (Epilepsy Foundation expert consensus, Harding et al. 2005; updated review, Fisher et al. 2022, Epilepsia; ITU-R BT.1702 and UK broadcast guidance) identifies the main risk factors as luminance changes at 3 or more per second, oscillating patterns with 5 or more light-dark stripe pairs, flashing areas above roughly 10 to 25 % of the visual field, high luminance, and saturated red flashes. The experiment stays far from each of them:

Property

Default / hard limit

How it is kept safe

Mean luminance

constant

Light and dark checks are symmetric around the grey background and an even number of checks gives equal areas. Pattern A, pattern B and the grey screen therefore have the same mean luminance: there is no full-field flash at a reversal or a fade.

Contrast (Michelson)

0.40 / 0.05 to 0.60

Never black/white. Pixel values 77 / 128 / 179 at the default.

Reversal rate

2/s / 0.5 to 2 per second

Each reversal changes the luminance of every check once, so at most 2 luminance changes per second at any point (below 3/s). Rates must give a whole number of 120 Hz frames (2, 1.5, 1.2, 1 … per second).

Pattern

4 x 4 / at most 8 x 8; each check at least 1 deg

At most 4 light-dark pairs per side (below 5); at most 0.5 cycles/deg.

Size

10 deg (about 5 % of the screen) / at most 12 deg (about 7 %)

Central field, well below the 10-25 % area at which risk rises. Full screen is refused for this profile.

Fade in / fade out

1 s / 0 or 1 frame to 3 s

The pattern appears and disappears gradually at the start and end of the block.

Fixation dot

static red, 0.2 deg

Never changes, so it is not a red flash.

Run length

at most 15 min

Split longer sessions into runs with breaks.

Window open/close

2 s fade to black

Psychtoolbox splash/test screens are disabled (VisualDebugLevel 1); the screen fades grey to black before closing, after the end screen and after an abort. Set the projector desktop background to black.

The limits are defined once in cfg.safety in flipped_squares_config.m. They are not editable from the launcher. flipped_squares_check_params.m enforces them in the launcher (Start stays disabled) and again at the start of every run, so scripted calls cannot bypass them. Non-numeric, non-double, fractional or out-of-range values are refused, and so is a ramp shorter than one frame, a reversal rate above 2 per second and a rate that is not a whole number of frames. The Preview stimuli button only renders stimuli that pass the limits.

Note

Compared with the earlier onset/offset design (pattern shown 0.5 s every 2 s), the pattern is now on screen for the whole block. The luminance change rate per check is the same (at most 2 per second) and contrast, size and spatial frequency are unchanged, but the exposure to the pattern is longer: include this in the clinician’s review.

ESC or Q stops the run at once: the pattern is replaced by grey on the next frame, the trigger pixel is cleared, and the screen fades to black.

Participant profile

The launcher’s Participant box sets two run parameters that are saved with every run (TaskParameters in events.json, Parameters and ParticipantProfile in beh.json):

Setting

Effect

Participant has (or may have) photosensitive epilepsy (epilepsySafe = 1, default)

Every limit in the safety table applies; the pattern is a central square of at most 12 deg. Full screen cannot be selected.

Unticked (epilepsySafe = 0)

The operator must confirm in a dialog that the participant does not have epilepsy or a history of photosensitivity. Full screen can then be ticked.

Full-screen checkerboard (fullScreen = 1)

The board fills the whole screen (about 57 x 34 deg on the PROPixx), centred on the fixation dot with a check corner at the centre. The check size is set directly (default 2.5 deg, 1 to 20 deg); square size and checks per side are ignored. Because the board is mirror-symmetric with opposite polarity about the vertical centre line, light and dark areas are exactly equal on the 1920-pixel-wide projector and the mean luminance still never changes. Contrast (<= 0.6), reversal rate (<= 2/s) and the 1 deg minimum check size still apply.

flipped_squares_check_params.m refuses fullScreen = 1 together with epilepsySafe = 1, so a scripted run cannot select full screen for the epilepsy profile either. The production confirmation dialog repeats the profile and pattern before the run starts.

Warning

A full-field reversing checkerboard is a strong pattern stimulus. Use the full-screen setting only for participants without epilepsy or photosensitivity.

Conditions

There is no randomization: reversals strictly alternate A -> B -> A, and the number of reversals must be even, so there are exactly nReversals / 2 reversals to each polarity. Reversal 1 is to B (the block starts on A), so odd reversals are to B and even ones to A.

MEG setup

Trigger protocol

Single-channel DataPixx pixel mode, with the same RGB codes as trigger_template.m and the Arabic language localizer. The mapping is defined once in flipped_squares_trigger_map.m.

Event

KIT channel

MNE channel

Pixel RGB

When

reversal_to_a

224

MISC 001

[4 0 0]

First frame of pattern A after a reversal

reversal_to_b

225

MISC 002

[16 0 0]

First frame of pattern B after a reversal

pattern_fade_out

226

MISC 003

[64 0 0]

First frame of the fade-out

pattern_fade_in

227

MISC 004

[0 1 0]

First frame of the fade-in

run_start

228

MISC 005

[0 4 0]

First frame of the baseline

run_end

229

MISC 006

[0 16 0]

End of the final grey of a completed run

off

none

none

[0 0 0]

Every other frame

A completed run contains nReversals reversal pulses (224 + 225, half each), one pattern_fade_in, one pattern_fade_out, one run_start and one run_end: 244 pulses for the default 240 reversals. An aborted run has no fade-out and no run_end; events.tsv always lists exactly the pulses that were sent. Each pulse lasts one refresh (8.33 ms at 120 Hz); the script checks every pulse width from the flip timestamps; a pulse off by more than half a frame is logged and printed as a warning.

Channels 224 to 229 must be wired and checked on the KIT before the first session (test_all_meg_channels_triggers.m pulses all of 224-231). The trigger marks the start of frame scan-out, not the moment the participant sees the image: see Display and trigger timing for the measured projector lag (+8.36 ms). Reversals are abrupt, so the new polarity is fully on screen on the trigger frame.

DataPixx and display safety

Warning

The background of this task is grey (128), not black. In pixel mode a grey top-left pixel would raise trigger lines. The code therefore draws the trigger rectangle explicitly black on every frame that is not an intended trigger, including text screens and recovery frames.

  1. DataPixx is opened, set to DLP sequence program 0, with pixel mode disabled while the instruction screen is visible.

  2. Pixel mode is enabled only after the operator presses SPACE, while the trigger pixel on screen is black.

  3. Pixel mode is disabled before the end screen is drawn.

  4. On abort or crash, the stimulus is cleared to grey with a black trigger pixel and pixel mode is disabled before any file is written.

  5. The mouse cursor is hidden on the stimulus screen and parked in its centre, so it can never cover the trigger pixel.

  6. 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.

  7. Production requires a measured refresh of 120 +/- 0.5 Hz. Every reversal interval or trigger pulse outside one frame of its target is logged in the *_ok columns and printed as a warning. Timing never stops a run.

  8. 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 hardware pulse is still there for fill_events_onsets.py.

Geometry

Degrees are converted to pixels with the KIT projector geometry measured on 2026-09-06 (60 cm viewing distance, 65 x 37 cm projected image), using the full screen width so windowed debug runs show the same sizes. The realized square size in pixels and degrees is saved with every run. Update viewingDistanceCm / screenWidthCm in the config if the setup changes. The PROPixx is specified with a linear gamma (displayGamma = 1); if a photometer measurement shows otherwise, enter the measured exponent and the pixel values are recomputed in luminance units.

Running the experiment

Launcher

cd experiments/psychtoolbox/flipped_squares
main              % opens the launcher (production preset selected)

The planned run length is recomputed whenever a value changes (press Enter or Tab after typing), together with the number of reversals to A and B, the fade and baseline times, the luminance changes per second, check size and square size. The Checks box lists every reason the run cannot start.

  • Subject / Session / Run: BIDS labels (letters and digits only; a sub- or ses- prefix is accepted). The next free run number for the chosen subject, session and preset is suggested automatically; an existing run is never overwritten.

  • Participant: epilepsy-safe (default) or, after confirmation, without epilepsy, which enables the full-screen checkbox (see Participant profile).

  • Run parameters: number of reversals, reversals per second, contrast, square size, checks per side (square only), check size (full screen only) and fade in/out. The allowed range is shown next to each field.

  • Preview stimuli shows both polarities at the projector geometry (the operator monitor’s size and luminance differ from the PROPixx).

  • Start is disabled while anything is invalid. A production start asks for confirmation that KIT recording is running.

Presets

Preset

DataPixx

Display and checks

Output folder

Production

ON

Full screen, 120 Hz required, sync tests on, timing logged only

data/

Full rehearsal

OFF

Full screen, refresh not enforced

data_rehearsal/

Debug

OFF

Windowed, sync tests skipped, visible 50 x 50 px trigger patch

data_rehearsal/

Operator keys: SPACE starts the run, ESC or Q stops it at any time and saves the partial data. The participant never presses anything.

The console prints a progress line every 20 reversals (Reversal 40/240, about 1 min 44 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 reversals shown can be analysed.

Run length

run length = 2 s grey + fade in + (nReversals + 1) / rate + fade out + 2 s grey

Reversals

Rate

Run length

Note

240

2 /s

2 min 07 s

Default (1 s fades)

400

2 /s

3 min 27 s

600

2 /s

5 min 07 s

1200

2 /s

10 min 07 s

Maximum number of reversals

Scripted launch

Passing a struct bypasses the launcher; the safety limits still apply.

spec = struct('subject', 'P01', 'session', '01', 'mode', 'debug');
spec.params = struct('nReversals', 20);   % optional overrides, plain doubles
main(spec)                             % run = next free run number

% participant without epilepsy, full-screen board
spec.params = struct('epilepsySafe', 0, 'fullScreen', 1, 'checkSizeDeg', 2.5);
main(spec)

main('validate')                       % headless self-test (no PTB needed)

Psychtoolbox on Windows

As in the Arabic localizer, PsychStartup is called before Screen so the GStreamer and libusb DLLs are found even when startup.m omits it.

Before the session

  1. Clinician approval of the stimulus and the safety limits.

  2. Set the stimulus computer’s desktop background on the projector screen to black.

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

  4. Run a Debug or Full rehearsal run on the stimulus computer.

  5. Verify KIT channels 224 to 229 with test_all_meg_channels_triggers.m if the setup or wiring changed, then record a short production test run and check its pulse counts (see Preprocessing).

  6. Start KIT acquisition, confirm the participant is fixating, press SPACE.

Saved data (BIDS)

Output is written below the experiment folder in a BIDS tree (data/ for production, data_rehearsal/ for the other presets; both Git-ignored):

data/
  dataset_description.json
  README
  sub-P01/ses-01/meg/sub-P01_ses-01_task-flippedsquares_run-01_events.tsv
  sub-P01/ses-01/meg/sub-P01_ses-01_task-flippedsquares_run-01_events.json
  sub-P01/ses-01/beh/sub-P01_ses-01_task-flippedsquares_run-01_beh.tsv
  sub-P01/ses-01/beh/sub-P01_ses-01_task-flippedsquares_run-01_beh.json
  stimuli/task-flippedsquares/checks-4_contrast-398_px-156_gamma-100_pattern-a.png
  stimuli/task-flippedsquares/checks-4_contrast-398_px-156_gamma-100_pattern-b.png
  sourcedata/sub-P01/ses-01/beh/sub-P01_ses-01_task-flippedsquares_run-01_desc-snapshot_beh.mat
  sourcedata/sub-P01/ses-01/beh/sub-P01_ses-01_task-flippedsquares_run-01_desc-matlabconsole_log.txt

All files are written when the run is initialized and at completion, abort or crash (not during the block, so disk writes can never delay a reversal). RunStatus in both JSON sidecars is initialized, running, completed, aborted or crashed.

events.tsv (meg/)

One row per trigger pulse, in time order: run_start, pattern_fade_in, one row per reversal (reversal_to_b, reversal_to_a, …), pattern_fade_out, run_end.

Column

Content

onset

n/a from the stimulus computer; filled in preprocessing (seconds from the first sample of the MEG recording).

duration

Reversal rows: measured time to the next reversal (to the fade-out for the last one); n/a if the run stopped first. Fade rows: realized fade length. Run markers: 0.

trial_type

run_start, pattern_fade_in, reversal_to_a, reversal_to_b, pattern_fade_out, run_end.

value

Integer event id = KIT channel (224 to 229); unique per trial_type.

sample

n/a; filled in preprocessing.

channel

KIT trigger channel, read by the lab’s single-channel sanity check.

pattern

Polarity on screen from this event on (a or b; for the fade-out, the polarity fading out); n/a on run markers.

stim_file

Image path relative to the dataset stimuli/ folder.

onset_from_run_start

Flip time of the frame carrying the pulse minus that of the run-start frame (stimulus-computer clock), used to verify the recorded triggers.

presentation_index

Reversal number (0 for fades and run markers).

planned_duration, ramp_duration

Configured reversal period or fade length; realized fade (whole frames, 0 on reversal rows).

contrast

Realized Michelson contrast after 8-bit rounding.

hardware_emitted

1 if DataPixx pixel mode was active for this pulse (production), 0 in rehearsals.

events.json documents every column and carries everything needed to process the run without the MATLAB files:

Key

Content

TriggerMode

single_channel (required by the KIT pipelines).

TriggerChannels

For each KIT channel: MNE channel name, trial_type and pixel RGB.

ExpectedTriggerCounts

Number of pulses each channel must contain for this run.

TriggerDefinitions

Which frame each trigger marks (fade in, reversal, fade out, run start, run end).

TriggerPulseFrames, MeasuredRefreshRateHz

Pulse length in frames and the measured display refresh.

TriggerToProjectorLagSeconds

0.00836 s, the measured trigger-to-projector delay (logged, never applied to any time in the table).

RunStatus, RunComplete

completed/aborted/crashed…; true only when run_end was sent.

TaskParameters

The run parameters, including epilepsySafe and fullScreen.

StimulusPresentation

Operating system, Psychtoolbox version and code version.

As in the Arabic localizer, onset and sample are written as n/a because only the recording knows them; the file becomes validator-ready once they are filled (next section).

beh.tsv (beh/)

One row per planned reversal, including ones not reached in an aborted run: new polarity, trigger code, status, planned and measured time (relative to run start and as absolute Psychtoolbox VBL times), error in ms, measured interval to the next reversal, trigger-pulse duration, the two timing checks and missed flips. beh.json describes each column and stores the full parameter set, derived timing, safety limits, realized stimulus values (pixel values, contrast, size in px and deg), display information, hardware state, trigger map and the fade-in/fade-out times (RunInfo).

sourcedata/

The .mat snapshot contains everything above plus the RNG states; the console log is a per-run MATLAB diary that is never appended to.

Preprocessing

Filling onset and sample

fill_events_onsets.py detects the pulses on MISC 001-MISC 008 with the lab’s detect_pulses_on_channel (the detector used by the single-channel sanity check), checks that the recorded sequence of channels is identical to the logged sequence, and writes the events table with onset and sample filled:

python experiments/psychtoolbox/flipped_squares/fill_events_onsets.py \
    --events data/sub-P01/ses-01/meg/sub-P01_ses-01_task-flippedsquares_run-01_events.tsv \
    --raw    <path>/sub-P01_ses-01_task-flippedsquares_run-01_meg.con \
    --out    <BIDS root>/sub-P01/ses-01/meg/sub-P01_ses-01_task-flippedsquares_run-01_events.tsv
  • Every row with hardware_emitted = 1 is matched one-to-one, in order, to a recorded pulse on its channel (all five channels). If any pulse is missing or extra, nothing is written and the message names the first mismatch and the per-channel counts.

  • ExpectedTriggerCounts in events.json gives the same counts for the lab’s 1-kit_count_sanity_check_single_channel.py; a missing run_end (RunComplete: false) means the run was not completed.

  • The recorded intervals are compared with onset_from_run_start; a difference above 2 ms is reported as a warning.

  • Rows with hardware_emitted = 0 (rehearsals) stay n/a.

  • MNE-BIDS write_raw_bids writes its own _events.tsv to the same path, so run this after conversion and write to the BIDS meg/ folder.

Epoching

sample counts from the first sample of the file. MNE event arrays include first_samp, and the projector lag should be added when millisecond timing matters:

import mne
import numpy as np
import pandas as pd

raw = mne.io.read_raw_kit(con_path, preload=True)
ev = pd.read_csv(filled_events_tsv, sep="\t")
ev = ev[ev.trial_type.isin(["reversal_to_a", "reversal_to_b"])]

lag = round(0.00836 * raw.info["sfreq"])   # trigger -> projector
events = np.c_[ev["sample"].astype(int) + raw.first_samp + lag,
               np.zeros(len(ev), int), ev["value"].astype(int)]
epochs = mne.Epochs(raw, events,
                    event_id={"reversal_to_a": 224, "reversal_to_b": 225},
                    tmin=-0.1, tmax=0.4, baseline=(None, 0), preload=True)
evoked = epochs.average()   # pattern-reversal response, both polarities

With 2 reversals per second the next reversal follows 0.5 s later, so keep epochs within -0.1 to 0.4 s. Both polarities give the same reversal response and are usually averaged together; comparing reversal_to_a with reversal_to_b is a check for polarity-specific effects. The fade-in and fade-out rows (227, 226) mark the start and end of the block and are not used for averaging. Apply the projector lag to every trigger type.

Validation

main('validate') runs self_test.m without Psychtoolbox. It checks:

  • the trigger map (codes, unique channels, run start separate);

  • that the presets and safety limits respect the guidance above, and that the default run length is 2 min 07 s;

  • that 20 unsafe or invalid parameter sets are refused (excessive contrast, reversal rate, size or check count, 4 reversals/s, rates that are not a whole number of frames, odd checks, odd or fractional reversal counts, integer or logical types, sub-frame or over-long fades, runs over 15 min);

  • the participant profile: full screen refused with epilepsySafe = 1, allowed with 0, square settings ignored in full screen, contrast and minimum check size still enforced;

  • the images: exact polarity flip, identical mean luminance for A, B and the background, realized contrast, check count (also with gamma 2.2), and for the full-screen board at several screen and check sizes, with a check corner at the screen centre;

  • schedules: strict alternation starting with a reversal to B, balance, fixed reversal grid, planned run end equal to the launcher run length;

  • BIDS labels, file names and the content of every output file for a completed and an aborted run: one row per pulse, time order, alternating reversals, fade-out after the last reversal, run_end only when complete, ExpectedTriggerCounts equal to the table.

The run loop was also exercised end to end against a simulated 120 Hz Psychtoolbox clock, with and without fades: every pulse lasted one frame and carried the correct code (228, 227, then 225/224 alternating, 226, 229), reversals fell exactly 60 frames apart, the fade-in and fade-out each took 120 frames with monotonic contrast, nothing was redrawn between reversals, and an aborted run saved exactly the pulses that were sent.

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

Files

File

Role

main.m

Launcher, run loop, drawing, DataPixx and cleanup.

flipped_squares_config.m

Presets, default parameters, safety limits, geometry.

flipped_squares_check_params.m

Parameter and safety validation, planned run length.

flipped_squares_trigger_map.m

Event names, KIT channels and RGB codes.

flipped_squares_make_images.m

Builds the two checkerboards in luminance units.

flipped_squares_build_schedule.m

Alternating reversal sequence and planned block times.

flipped_squares_build_events.m

events.tsv table and events.json sidecar.

flipped_squares_write_outputs.m

Writes events, beh and snapshot for each save.

flipped_squares_bids_*.m, flipped_squares_next_run.m

BIDS labels, paths, dataset files, next free run.

flipped_squares_write_tsv.m, flipped_squares_write_json.m

TSV/JSON writers (n/a for missing values).

self_test.m

Headless test suite.

fill_events_onsets.py

Fills onset/sample from the KIT recording.

The figures on this page are generated by make_figures.py (the launcher image is a screenshot).

Open decisions before collection

Warning

  • Clinician sign-off on stimulus, limits, run length and number of runs.

  • Photometer measurement of the grey, light and dark luminance on the PROPixx (cd/m2) and of the display gamma.

  • The number of reversals needed per run and the number of runs.

  • The passive design has no behavioural attention check; consider eye tracking or a fixation check if attention must be documented.

References

  • Harding G, Wilkins AJ, Erba G, Barkley GL, Fisher RS (2005). Photic- and pattern-induced seizures: expert consensus of the Epilepsy Foundation of America Working Group. Epilepsia 46(9):1423-1425. https://doi.org/10.1111/j.1528-1167.2005.31305.x

  • Fisher RS, Acharya JN, Baumer FM, et al. (2022). Visually sensitive seizures: an updated review by the Epilepsy Foundation. Epilepsia 63(4):739-768. https://doi.org/10.1111/epi.17175