Creating Experiments¶
This tutorial walks through creating custom respiratory tracking experiments with respyra, from running a built-in config to composing a fully custom experiment flow.
Running built-in configs¶
The fastest path — run a pre-defined experiment with no code changes:
# Built-in config by short name
respyra-task --config demo
respyra-task --config validation_study
# Or use a file path
respyra-task --config experiments/demo.py
The --config flag accepts three forms:
Form |
Example |
Resolution |
|---|---|---|
Short name |
|
Imports |
Dotted path |
|
Imports the module directly |
File path |
|
Loads the |
Every config module must export a module-level variable named CONFIG — an ExperimentConfig instance.
Note
Config files are standard Python modules and are executed when loaded. Only use config files from trusted sources.
Creating a custom config¶
The recommended workflow: start from the base defaults and override what you need using dataclasses.replace().
1. Create a config file¶
Create a file in experiments/ (or anywhere you like):
# experiments/my_study.py
"""My first custom experiment."""
from dataclasses import replace
from defaults import CONFIG as _BASE
from respyra.configs.experiment_config import TrialConfig
from respyra.configs.presets import slow_steady
# 5 cycles at 0.1 Hz = 50 seconds per trial
MY_CONDITION = slow_steady(n_cycles=5)
CONFIG = replace(
_BASE,
name="My Study",
timing=replace(_BASE.timing, tracking_duration_sec=50.0),
display=replace(_BASE.display, fullscr=True),
trial=TrialConfig(
conditions=[MY_CONDITION] * 4,
n_reps=1,
method="sequential",
),
)
The experiments/defaults.py file provides sensible base parameters — from defaults import CONFIG works because load_config temporarily adds the file’s parent directory to sys.path.
2. Run it¶
respyra-task --config experiments/my_study.py
What you can override¶
Each sub-config groups related parameters. Override any of them with nested replace() calls:
from dataclasses import replace
from defaults import CONFIG as _BASE
CONFIG = replace(
_BASE,
# Change the experiment name
name="Slow Breathing Study",
# Change timing
timing=replace(_BASE.timing,
baseline_duration_sec=15.0,
tracking_duration_sec=60.0,
),
# Change display
display=replace(_BASE.display,
fullscr=True,
monitor_size_pix=(2560, 1440),
),
# Change the belt
belt=replace(_BASE.belt, connection="usb"),
)
Using condition presets¶
The respyra.configs.presets module provides factory functions for common breathing paradigms:
from respyra.configs.presets import slow_steady, perturbed_slow, mixed_rhythm
# Factory functions — customize parameters
easy = slow_steady(freq_hz=0.1, n_cycles=3) # 30 s trial
hard = slow_steady(freq_hz=0.15, n_cycles=4) # 26.7 s trial
perturbed = perturbed_slow(feedback_gain=2.0) # 2x visual gain (default is 1.5x)
mixed = mixed_rhythm(freq_slow=0.1, freq_fast=0.25) # multi-segment
# Pre-built constants — standard defaults
from respyra.configs.presets import SLOW_STEADY, PERTURBED_SLOW, MIXED_RHYTHM
Building from scratch¶
For full control, use ConditionDef and SegmentDef directly:
from respyra.core.target_generator import ConditionDef, SegmentDef
# Graded gain levels for dose-response designs
conditions = [
ConditionDef("gain_1.0", [SegmentDef(0.1, 3)], feedback_gain=1.0),
ConditionDef("gain_1.5", [SegmentDef(0.1, 3)], feedback_gain=1.5),
ConditionDef("gain_2.0", [SegmentDef(0.1, 3)], feedback_gain=2.0),
ConditionDef("gain_2.5", [SegmentDef(0.1, 3)], feedback_gain=2.5),
]
# Fast breathing with multiple segments
fast_protocol = ConditionDef("fast_ramp", [
SegmentDef(0.2, 2), # 10 s at 0.2 Hz
SegmentDef(0.3, 3), # 10 s at 0.3 Hz
SegmentDef(0.4, 4), # 10 s at 0.4 Hz
])
Use integer cycle counts per segment to ensure phase continuity at boundaries — the waveform loops seamlessly.
Counterbalanced designs¶
For studies requiring counterbalancing across sessions, define a build_conditions() function and pass it to TrialConfig:
# experiments/my_counterbalanced_study.py
from dataclasses import replace
from defaults import CONFIG as _BASE
from respyra.configs.experiment_config import TrialConfig
from respyra.configs.presets import perturbed_slow, slow_steady
SLOW_4 = slow_steady(n_cycles=4)
PERTURBED_4 = perturbed_slow(n_cycles=4, feedback_gain=2.0)
BLOCK_SIZE = 6
def build_conditions(session_num):
"""Counterbalance starting condition across sessions.
Odd sessions (1, 3): slow_steady block first.
Even sessions (2, 4): perturbed block first.
"""
if int(session_num) % 2 == 1:
return [SLOW_4] * BLOCK_SIZE + [PERTURBED_4] * BLOCK_SIZE
else:
return [PERTURBED_4] * BLOCK_SIZE + [SLOW_4] * BLOCK_SIZE
CONFIG = replace(
_BASE,
name="Counterbalanced Study",
timing=replace(_BASE.timing, tracking_duration_sec=40.0),
trial=TrialConfig(
conditions=build_conditions(1), # default for Session 1
n_reps=1,
method="sequential",
build_conditions=build_conditions, # called at runtime with actual session
),
)
When build_conditions is set, the runner calls it at runtime with the session number entered in the participant dialog, overriding the static conditions list.
See experiments/validation_study.py for a complete working example.
Custom experiment flows¶
For experiments that deviate from the standard flow (e.g., skipping baseline, adding custom phases, or running multiple calibrations), import individual phase functions from respyra.core.runner:
from respyra.core.runner import (
ExperimentState,
connect_belt,
setup_display,
run_participant_dialog,
run_range_calibration,
run_baseline,
run_countdown,
run_tracking,
show_trial_feedback,
show_end_screen,
)
from respyra.configs.experiment_config import ExperimentConfig
cfg = ExperimentConfig(...)
# Compose your own flow
belt = connect_belt(cfg)
from psychopy import core
from respyra.core.data_logger import DataLogger, create_session_file
from collections import deque
win, stimuli = setup_display(cfg)
exp_info = run_participant_dialog(cfg)
filepath = create_session_file(exp_info["participant"], exp_info["session"])
logger = DataLogger(filepath, columns=cfg.data_columns)
state = ExperimentState(
belt=belt, win=win, logger=logger,
clock=core.Clock(), buffer=deque(maxlen=cfg.trace_buffer_size),
stimuli=stimuli,
)
# Example: skip baseline, run calibration, then just tracking
run_range_calibration(state, cfg)
for trial_num in range(1, 6):
# ... run_countdown, run_tracking, etc.
pass
Each phase function takes an ExperimentState and ExperimentConfig, mutates the state, and returns status flags. See the API reference for full signatures.
Configuration reference¶
ExperimentConfig¶
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Experiment name (shown in dialogs) |
|
|
See below |
Belt connection parameters |
|
|
See below |
PsychoPy window settings |
|
|
See below |
Waveform display |
|
|
See below |
Target dot appearance and feedback |
|
|
See below |
Phase durations |
|
|
See below |
Range calibration |
|
|
See below |
Trial structure and conditions |
|
|
|
Output directory for CSV files |
|
|
|
Key to abort the experiment |
|
|
10 standard columns |
Column names for the output CSV |
BeltConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
Connection type: |
|
|
BLE device selection strategy |
|
|
Sampling interval in ms (100 = 10 Hz) |
|
|
Sensor channels (1 = Force in Newtons) |
DisplayConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
Full-screen mode (set |
|
|
PsychoPy monitor profile name |
|
|
Physical screen width in cm |
|
|
Viewing distance in cm |
|
|
Screen resolution |
|
|
PsychoPy coordinate system |
|
|
Background color (black) |
TraceConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
Trace area (left, bottom, right, top) |
|
|
Initial force range (overridden after calibration) |
|
|
Waveform color |
|
|
Trace border color |
|
|
Seconds of signal visible on screen |
DotConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
Dot radius (height units) |
|
|
Offset right of trace edge |
|
|
Good tracking color |
|
|
Poor tracking color |
|
|
Moderate tracking color (trinary mode) |
|
|
|
|
|
Good/bad cutoff in Newtons |
|
|
Mid/bad cutoff in Newtons |
|
|
Error at which dot is fully red |
TimingConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
Range calibration duration |
|
|
Baseline per trial |
|
|
Countdown per trial |
|
|
Tracking per trial |
RangeCalConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
Fraction of range used for target amplitude |
|
|
Lower percentile for outlier rejection |
|
|
Upper percentile for outlier rejection |
|
|
Sensor floor warning threshold |
|
|
Sensor ceiling warning threshold |
TrialConfig¶
Field |
Default |
Description |
|---|---|---|
|
|
List of |
|
|
Repetitions per condition |
|
|
|
|
|
Optional function |