Full Experiment: Breath Tracking Task

Script: respyra/scripts/breath_tracking_task.py

What it demonstrates: The complete respiratory tracking experiment with calibration, multi-condition trials, and data logging.

How to run

# Default configuration
respyra-task

# Built-in config by short name
respyra-task --config demo
respyra-task --config validation_study

# Custom config file
respyra-task --config experiments/my_study.py

To customize the experiment, see the Creating Experiments tutorial.

Architecture overview

The experiment is built from composable phase functions in respyra.core.runner:

  1. Belt connection (connect_belt) — connects before PsychoPy import (Windows BLE requirement)

  2. Display setup (setup_display) — monitor profile, window, pre-created stimuli

  3. Participant dialog (run_participant_dialog) — collects participant ID and session number

  4. Range calibration (run_range_calibration) — 15 s of deep breaths to establish breathing range

  5. Trial loop — for each condition:

    • run_baseline (10 s) → run_countdown (3 s) → run_tracking (30 s) → show_trial_feedback

  6. Cleanup — belt stop, file close, window close (in finally block)

All phases share state via an ExperimentState dataclass.

Key code patterns

Belt-before-PsychoPy import order

from respyra.core.runner import connect_belt, setup_display

# 1. Connect belt BEFORE importing PsychoPy
belt = connect_belt(cfg)

# 2. Now safe to import PsychoPy and set up display
win, stimuli = setup_display(cfg)

Configuration-driven setup

All parameters come from an ExperimentConfig:

from respyra.configs.experiment_config import load_config
from respyra.core.runner import run_experiment

cfg = load_config("demo")  # or a file path, or an ExperimentConfig instance
run_experiment(cfg)

Pre-created stimuli

All visual stimuli are created once by setup_display before the frame loop:

win, stimuli = setup_display(cfg)
# stimuli["trace"], stimuli["trace_border"], stimuli["phase_title"],
# stimuli["status_text"], stimuli["countdown_text"], stimuli["target_dot"]

Gain perturbation

The displayed waveform is perturbed; the dot color reflects the visual (compensated) error:

from respyra.core.runner import apply_gain

# Display: apply gain to the visual trace
stimuli["trace"].draw(apply_gain(buffer, feedback_gain, range_center))

# Target position uses true force; dot color uses visual error
target_force = target_gen.get_target(tracking_t)
error = target_force - force  # true (physical) error
visual_force = center + feedback_gain * (force - center)
compensated_error = target_force - visual_force  # visual error (drives dot color)

Incremental logging with DataLogger

logger = DataLogger(filepath, columns=cfg.data_columns)
# Inside frame loop:
logger.log_row(
    timestamp=round(elapsed, 4),
    frame=frame_count,
    force_n=round(force, 4),
    phase='tracking',
    condition=condition_name,
    ...
)

PsychoPy TrialHandler for condition ordering

trial_list = [{'condition': c.name} for c in conditions]
trials = data.TrialHandler(
    trialList=trial_list, nReps=cfg.trial.n_reps, method=cfg.trial.method,
)
for trial in trials:
    condition_name = trial['condition']
    ...