Use this Skill for PsychoPy experiment design: stimulus presentation, response collection, TTL synchronization, BIDS event files, and reaction time analysis.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use this Skill for PsychoPy experiment design: stimulus presentation, response collection, TTL synchronization, BIDS event files, and reaction time analysis.
One-line summary: Design and run neuroscience experiments with PsychoPy: visual/auditory stimuli, TTL triggers for EEG/fMRI, BIDS event file generation, and RT analysis.
When to Use This Skill
When designing visual or auditory cognitive paradigms (oddball, N-back, Stroop, etc.)
When synchronizing stimulus delivery with EEG/fMRI acquisition via TTL pulses
When generating BIDS-compatible events.tsv files for neuroimaging data
When collecting keyboard/response-box reaction times with millisecond precision
When deploying experiments online via Pavlovia
When analyzing timing data from PsychoPy log files
Coder: Full Python scripting via psychopy.visual, psychopy.core, psychopy.event
For neuroscience experiments, Coder provides fine-grained timing control.
Timing Precision
Stimulus timing in PsychoPy can be specified in:
Frames: Most precise (e.g., 60 Hz monitor → 16.67 ms/frame)
Seconds: Convenient but limited by monitor refresh
Critical timing: win.flip() synchronizes to vertical retrace (VBL).
BIDS Events Format
BIDS (Brain Imaging Data Structure) events.tsv requires:
Column
Description
onset
Stimulus onset time relative to first acquisition (s)
duration
Stimulus duration (s)
trial_type
Condition label
response_time
Reaction time (s), NaN if no response
response
Key pressed or NaN
TTL Synchronization
Parallel port pulse (8-bit trigger code) sent to EEG amplifier:
Code 1-255 marks event type
Duration typically 5-10 ms
Environment Setup
Install Dependencies
# PsychoPy is best installed as standalone or via pip in a fresh environment
pip install psychopy>=2023.2 pandas>=2.0 numpy>=1.24 matplotlib>=3.7 scipy>=1.11
# For parallel port triggers (EEG)
pip install pyparallel # Linux# pip install pywin32 # Windows (parallel port)
Verify Installation
import psychopy
from psychopy import core, visual, event
print(f"PsychoPy version: {psychopy.__version__}")
# Do NOT open a window in headless environments
Core Workflow
Step 1: Experiment Design and Trial List
import numpy as np
import pandas as pd
from pathlib import Path
defcreate_oddball_trial_list(n_standards=120, n_oddballs=30, seed=42):
"""
Create a randomized auditory oddball trial list.
Standard tone: 1000 Hz (80% probability)
Oddball tone: 2000 Hz (20% probability)
Returns
-------
pd.DataFrame with columns: trial_num, trial_type, frequency_Hz, isi_s
"""
rng = np.random.default_rng(seed)
# Create trial types
trials = (["standard"] * n_standards + ["oddball"] * n_oddballs)
# Constraint: no more than 2 consecutive oddballs; at least 2 standards between oddballswhileTrue:
rng.shuffle(trials)
# Check constraint: no two consecutive oddballs
valid = Truefor i inrange(len(trials) - 1):
if trials[i] == "oddball"and trials[i+1] == "oddball":
valid = Falsebreakif valid:
break# Inter-stimulus intervals (jittered 0.8–1.2 s)
isi_list = rng.uniform(0.8, 1.2, len(trials))
trial_df = pd.DataFrame({
"trial_num": range(1, len(trials) + 1),
"trial_type": trials,
"frequency_Hz": [1000if t == "standard"else2000for t in trials],
"isi_s": isi_list,
"trigger_code": [10if t == "standard"else20for t in trials],
})
trial_df["onset_s"] = trial_df["isi_s"].cumsum().shift(1).fillna(0)
print(f"Trial list: {len(trial_df)} trials")
print(f" Standards: {(trial_df['trial_type']=='standard').sum()}")
print(f" Oddballs: {(trial_df['trial_type']=='oddball').sum()}")
print(f" Total duration: {trial_df['onset_s'].max():.0f}s ({trial_df['onset_s'].max()/60:.1f} min)")
# Save trial list
trial_df.to_csv("oddball_trials.csv", index=False)
return trial_df
trials = create_oddball_trial_list()
print("\nFirst 10 trials:")
print(trials.head(10).to_string(index=False))
Step 2: PsychoPy Experiment Script
"""
Auditory Oddball Experiment — PsychoPy Coder Mode
Run in a PsychoPy environment (not headless CI).
Replace 'SIMULATE = True' with 'SIMULATE = False' for real experiments.
"""
SIMULATE = True# Set False for actual experimentimport numpy as np
import pandas as pd
from pathlib import Path
ifnot SIMULATE:
from psychopy import core, visual, sound, event, prefs
prefs.hardware["audioLib"] = ["ptb", "sounddevice"]
defrun_oddball_experiment(trial_df, participant="P01", session=1, simulate=True):
"""
Run auditory oddball experiment.
Parameters
----------
trial_df : pd.DataFrame
Trial list from create_oddball_trial_list()
participant : str
session : int
simulate : bool
If True, simulate timing without opening windows
"""if simulate:
print("SIMULATION MODE: No window will open")
results = []
for _, trial in trial_df.iterrows():
rt = np.random.exponential(0.35) + 0.15if np.random.rand() < 0.90else np.nan
results.append({
"trial_num": trial["trial_num"],
"trial_type": trial["trial_type"],
"frequency_Hz": trial["frequency_Hz"],
"onset_s": trial["onset_s"],
"response_time": rt,
"response": "space"ifnot np.isnan(rt) else"none",
"correct": not np.isnan(rt) if trial["trial_type"] == "oddball"else np.isnan(rt),
})
results_df = pd.DataFrame(results)
save_results(results_df, participant, session)
return results_df
else:
# Real experiment codefrom psychopy import core, visual, sound, event
win = visual.Window([1280, 720], fullscr=True, units="norm")
fixation = visual.TextStim(win, text="+", height=0.1)
clock = core.Clock()
results = []
for _, trial in trial_df.iterrows():
# Present fixation
fixation.draw()
win.flip()
# Play tone (duration 100ms)
tone = sound.Sound(trial["frequency_Hz"], secs=0.1, stereo=True)
tone.play()
onset_time = clock.getTime()
# Send TTL trigger# parallel_port.setData(trial["trigger_code"])# core.wait(0.005)# parallel_port.setData(0)# Collect response
event.clearEvents()
response, rt = None, np.nan
core.wait(trial["isi_s"] - 0.1)
keys = event.getKeys(keyList=["space", "escape"], timeStamped=clock)
if keys:
key, key_time = keys[0]
if key == "escape":
break
response = key
rt = key_time - onset_time
results.append({
"trial_num": trial["trial_num"],
"trial_type": trial["trial_type"],
"onset_s": onset_time,
"response_time": rt,
"response": response or"none",
})
win.close()
results_df = pd.DataFrame(results)
save_results(results_df, participant, session)
return results_df
defsave_results(results_df, participant, session):
"""Save results in BIDS-compatible events.tsv format."""
output_dir = Path(f"sub-{participant}/ses-{session:02d}/beh")
output_dir.mkdir(parents=True, exist_ok=True)
# BIDS events file
bids_events = results_df.rename(columns={
"onset_s": "onset",
"response_time": "response_time",
})
bids_events["duration"] = 0.1# stimulus duration
bids_events["trial_type"] = bids_events["trial_type"]
bids_cols = ["onset", "duration", "trial_type", "response_time", "response"]
bids_events[bids_cols].to_csv(
output_dir / f"sub-{participant}_ses-{session:02d}_task-oddball_events.tsv",
sep="\t", index=False, float_format="%.4f"
)
print(f"Saved BIDS events to {output_dir}")
results = run_oddball_experiment(trials, simulate=SIMULATE)
print(f"\nHit rate (oddball): {results[results['trial_type']=='oddball']['response_time'].notna().mean():.2%}")
print(f"FA rate (standard): {results[results['trial_type']=='standard']['response_time'].notna().mean():.2%}")
# Pavlovia deployment checklist (not executable — reference guide)
checklist = {
"1. Python → JavaScript": "PsychoPy Builder auto-converts; avoid raw Python in Coder",
"2. Stimuli files": "Upload to Pavlovia GitLab repo; use relative paths",
"3. Timing accuracy": "Online timing ±10ms (vs. ±1ms local) — use frames not seconds",
"4. Response keys": "Browser keyboard events; test all target keys",
"5. Data download": "Pavlovia auto-saves CSV to project; download via GUI or API",
"6. Ethical compliance": "Add consent form as first component; store no PII",
}
for step, note in checklist.items():
print(f"{step}: {note}")
Troubleshooting
Error: psychopy.visual.Window fails to open (in CI/headless)
from psychopy import core
# Use globalClock for all timing
global_clock = core.Clock()
# Reset at experiment start, use global_clock.getTime() for all onsets
Version Compatibility
Package
Tested versions
Known issues
psychopy
2023.2, 2024.1
Sound backend varies by OS; test ptb > sounddevice > pygame