Architecture¶
Guiding shape: "A now, C-shaped boundary"¶
We start as one native desktop application (fastest path to increment 1, the stimulus builder + runtime), but we adopt the boundaries of a more distributed design from day one so nothing becomes a dead end:
- The timing-critical stimulus + LSL engine is an isolated "agent" — a component with a narrow, message-based control surface, so UI work can never stall the render loop, and so it can later run as a separate process or be reimplemented in another engine.
- All decoding/IO logic lives in a UI-agnostic core (plain functions over arrays + manifests). No GUI import ever reaches into it.
Consequence: adding a browser-based analysis frontend later (served from the Canadian compute server for remote users) is additive — it consumes the same core + manifest. So is swapping the renderer for a game engine (see "Engine portability").
Why not fuller web-first or two-frontends now: both add real infra (web server, container, protocol) before the first timing-critical deliverable exists. We buy their optionality cheaply via the boundary instead of paying their cost up front. Revisit when remote browser analysis becomes a priority.
The manifest is the spine¶
┌─────────────┐ emits ┌──────────────────────┐
│ stim builder│──manifest─▶│ run manifest │◀─ single source of truth
└─────────────┘ │ (schemas/*.json) │
└──────────┬───────────┘
stamps into each recording │ reads directly
┌─────────────────────┼─────────────────────┐
▼ ▼
┌────────────────┐ LSL (EEG + markers) ┌──────────────────┐
│ runtime/runner │─────────────────────────▶ │ analysis + report│
│ record → XDF │ │ metrics.json → │
└────────────────┘ │ figures/report │
│ save/push (Canada only) └──────────────────┘
▼
local disk → NCIL NAS → compute server
Every value analysis would otherwise reverse-engineer (frequencies, montage, trial/block structure, render method, marker schema) is a manifest lookup. This deletes the back-filling that dominated the pilot.
Components (src/ssvep/)¶
| Package | Responsibility | Depends on | Notes |
|---|---|---|---|
classification/ |
Validated engine: CCA/FBCCA/TRCA, PSD/SNR, metrics, viz. Pure (n_ch, n_times) array API. |
numpy, scipy, scikit-learn, matplotlib | This is ssvep_core from the handoff, lifted as-is (renamed from the too-generic core). Same code offline + online. |
io/ |
manifest ⇄ (arrays, labels) adapter; XDF read (pyxdf/mne-lsl); BIDS conversion (mne-bids); provenance stamping; consent-flag model. |
mne, mne-bids, pyxdf, jsonschema | The thin adapter that replaces the pilot's study-specific glue. |
stim/ |
pyglet flicker renderer (vsync-locked, gamma/LUT-aware, dropped-frame detection); builder data-model that validates renderability against display.refresh_hz; emits manifest. Runs as the stimulus agent. Integrates the flik-o-meter photodiode for timing validation (USB capture + analysis in stim/photodiode.py; --measure-flicker presentation; LSL-sync link still to come). |
pyglet, pyserial | Timing-critical. Validate every build; photosensitivity warnings here. |
runtime/ |
Session runner: resolve/verify LSL streams (OpenBCI Cyton 8-ch via BrainFlow/OpenBCI→LSL), push structured markers, record EEG+markers → XDF, stamp manifest, save + push to the NCIL server (push.py, SFTP via the system OpenSSH client — opt-in, consent-gated, copy-never-move; docs/PUSH.md). Always-available abort; break-time adverse-symptom checkpoints. |
pylsl, mne-lsl | Only runs built, saved, versioned protocols. |
analysis/ |
Offline pipelines (epoch → decode → sweep windows), canonical metrics.json, report generation — including a reproducible stimulus description (screen-layout figure + trial-timeline diagrams, derived from the manifest) — and the derivatives layer (derivatives.py): where reports live (sub-XXX/ses-YYY/), the subject→session→run index rebuilt from disk, and the derivatives dataset_description.json. |
classification, io | One metrics.json feeds every table/figure/number (report principle). The index is derived, never accumulated — see docs/BIDS.md. |
ui/ |
PySide6 unified GUI with Design / Run / Analyze modes; pyqtgraph real-time plots. Identical behaviour for live vs file input. | PySide6, pyqtgraph | Talks to the stim agent over its control surface; never imports render internals. |
Real-time path¶
classification.decoding.classify / cca_scores / fbcca_scores already take a single
(n_ch, n_times) window — the online loop calls them per LSL buffer; calibration-free
methods need no warm-up. TRCA stays gated behind trca_feasible(). The online harness
(buffering, windowing, marker sync) is new; the math is the validated handoff code.
Auth / access model (remote-only)¶
- Local acquisition app: no live auth; operator selects their name from the admin whitelist (config file, not GUI-editable). That operator id is stamped into provenance so we track who collected each dataset. Works fully offline.
- Remote analysis (future web frontend): GitHub-ID OAuth checked against the whitelist,
served only from the Canadian compute server behind TLS + CA/US IP allowlist. This sits
on top of the lab's existing server security, never weakening it — the general layering
principle is in
docs/DATA_GOVERNANCE.md.
Engine portability (Unity / Godot — future, optional)¶
The stim/ agent is the only engine-specific layer. Its contract to the rest of the system
is exactly: consume a manifest, render the specified flicker with correct timing, emit the
structured LSL markers. Any future renderer that honours that contract drops in without
touching core/io/runtime/analysis.
- Unity: not used here — proprietary/paid-tier licensing conflicts with the commercial-free requirement, it offers no SSVEP timing advantage over a tight pyglet loop, and no one on the build team knows it.
- Godot (MIT): the license-clean candidate if a rich game engine ever becomes core to Gambit. A Godot stimulus front-end would implement the same manifest+marker contract.
Data flow & residency (summary; mechanism in docs/DATA_GOVERNANCE.md, Dalhousie-specific bindings in docs/COMPLIANCE.md)¶
Acquire on the Windows PC → write XDF locally → (optionally) BIDS-convert → push to NCIL NAS (Canada) → available to the compute server via NFS for remote offline analysis. No step touches a foreign/US cloud, and git never holds data.