Requirements & Dependencies¶
Kept in sync with environment.yml. Every dependency must be free for commercial reuse.
Strong copyleft (GPL) is disallowed; weak copyleft (LGPL) is allowed (dynamic linking).
Ask Aaron before adding anything not listed here (dependencies of listed packages are OK).
Environment: reuse the existing ssvep conda env (conda activate ssvep); add the three
new packages marked ★.
Core stack¶
| Package | Purpose | License | Commercial-safe |
|---|---|---|---|
| numpy | arrays | BSD-3 | ✅ |
| scipy | signal/spectral | BSD-3 | ✅ |
| pandas | tabular | BSD-3 | ✅ |
| scikit-learn | metrics/ML | BSD-3 | ✅ |
| matplotlib | static figures | PSF/BSD-style | ✅ |
| mne | EEG analysis | BSD-3 | ✅ |
| mne-bids | BIDS conversion | BSD-3 | ✅ |
| mne-lsl | LSL ↔ MNE, real-time | BSD-3 | ✅ |
| pylsl | LSL bindings (liblsl) | MIT | ✅ |
| pyxdf | read XDF | BSD-2 | ✅ |
| meegkit | denoising (ZapLine etc.) | BSD-3 | ✅ |
| jsonschema | validate manifests/markers | MIT | ✅ |
| pyglet ★ | stimulus flicker renderer | BSD-3 | ✅ |
| PySide6 ★ | GUI (Qt binding) | LGPL-3 | ✅ (dynamic link) |
| pyqtgraph | real-time plots | MIT | ✅ |
| pyserial | flik-o-meter (photodiode) USB serial capture | BSD-3 | ✅ |
| jupyterlab | remote/offline analysis notebooks | BSD-3 | ✅ |
| brainflow | OpenBCI Cyton acquisition → LSL | MIT | ✅ (2026-07-02, bench-verified on the Cyton). See the setuptools pin below. |
| setuptools (<81) | brainflow runtime dep (pkg_resources) |
MIT | ✅ pinned — brainflow imports pkg_resources, which setuptools 81+ dropped; upstream fix pending. |
| bids-validator-deno ★ | official BIDS validator (vendored deno runtime, no separate install) | MIT | ✅ |
Acquisition — default path¶
Default = the BrainFlow bridge (ssvep.runtime.openbci); the OpenBCI GUI's LSL output is a
fallback. Only one process can own the serial port at a time, so close the GUI before using the
bridge (they can't both hold COM5).
python -m ssvep.runtime.openbci --serial-port COM5 --check # bounded connectivity check
python -m ssvep.runtime.openbci --serial-port COM5 # stream to LSL for a session
The runtime records whatever produces an LSL EEG stream (RunRecorder.preflight resolves by
type + verify_stream matches it to the manifest). Fallback: the OpenBCI GUI's Networking →
LSL output streams EEG directly (no BrainFlow), useful when the GUI is already open.
Bench-verified 2026-07-02 (Cyton on COM5, no electrodes): both paths stream 8-ch @ 250 Hz; the runtime resolved + verified against the manifest and recorded a round-tripping XDF.
Sampling rate (OpenBCI Cyton)¶
- USB dongle (RFduino) = 250 Hz, hard cap. Empirically confirmed: requesting 500 Hz corrupts the board's reply over the radio; "1000 Hz" is accepted but only ~210 Hz actually arrives (the radio drops ~80 % of samples). Do not run the dongle above 250.
- For >250 Hz, use the OpenBCI WiFi Shield —
--board cyton-wifi --ip-address <shield-ip> --sample-rate 500|1000(native 1000 Hz). This is the only way to raise the rate. - Choosing a rate for flicker up to ~90 Hz. Sampling rate sets the Nyquist / harmonic coverage
(not the frequency resolution, which is the analysis window length = 1/T):
| fs | Nyquist | captures for a 90 Hz target | note |
|---|---|---|---|
| 250 Hz (dongle) | 125 Hz | fundamental only | 2nd harmonic (180 Hz) aliases → weak high-band decoding (matches the pilot) |
| 500 Hz (WiFi) | 250 Hz | fundamental + 2nd harmonic (180 Hz) | recommended balance for 30–90 Hz |
| 1000 Hz (WiFi) | 500 Hz | + 3rd harmonic (270 Hz) + margin | best decoding; data still trivial |
Data volume is a non-issue (1000 Hz × 8 ch × 4 B ≈ 32 KB/s). Since FBCCA decoding leans on
harmonics, 500 Hz (WiFi shield) is the recommended target for the high-frequency plan; 250 Hz
(dongle) will under-perform toward the top of the band. Set
acquisition.sfreq_hzin the design GUI to match whatever rate you actually run.
Explicitly excluded (licensing)¶
| Package | Why excluded | Use instead |
|---|---|---|
| PsychoPy | GPLv3 (strong copyleft) — conflicts with proprietary Gambit distribution | pyglet (its own backend) |
| PyQt5/6 | GPLv3 or paid commercial (Riverbank) | PySide6 (LGPL) |
| Unity | Proprietary, paid tiers / revenue caps — not commercial-free | (native pyglet now; Godot/MIT later if needed) |
Third-party hardware SDKs — NOT free-for-commercial (user-supplied, never shipped)¶
Some amplifiers need a vendor SDK/driver that is not free for commercial reuse and may not be
redistributed. These are treated as optional, user-supplied plugins: the toolbox loads a DLL the
operator installs from the vendor — we never vendor, commit, or ship vendor binaries. The permissive
core and the OpenBCI / Unicorn / pyglet paths are unaffected. .gitignore blocks *.dll/*.sys/*.bit
so a vendor binary can't be committed by accident.
| Vendor SDK | Used by | License (summary) | Redistribute? | Commercial use? |
|---|---|---|---|---|
BrainVision Amplifier SDK — ActiChamp_x64.dll (actiCHamp / LiveAmp family) |
ssvep.runtime.actichamp |
Brain Products Licensing Terms (Dec 2019): research-only, non-medical, free of charge | ❌ "The Software itself may not be sublicensed" — user downloads from BP | ❌ needs a written BP agreement (bars commercial use of the SDK and of any app that processes BP-hardware data) |
V-Amp / FirstAmp SDK — FirstAmp.dll |
future ssvep.runtime.vamp |
not yet obtained; assume similar BP terms — review on acquisition | ❌ (assume) | ❌ (assume) |
Brain Products Amplifier SDK — key obligations (full read 2026-07-13,
C:\Vision\Amplifier SDK\LicensingTerms_BrainVision_Amplifier-SDK.pdf; online: brainproducts.com
downloads.php?kid=34):
- Never ship/commit BP files (DLL /
.bit/.lib/ headers /.sysdriver). The operator installs the SDK/driver; the toolbox loads a user-supplied DLL (actichamp.find_dll()/$ACTICHAMP_DLL). - Non-commercial redistribution of our app is allowed if the BP components are unmodified and our licence (i) forbids commercial / medical / high-risk use, (ii) carries BP copyright notices + names which BP parts are used, (iii) disclaims warranty and excludes third-party liability.
- Commercial release needs a written BP agreement — for the BP-hardware modules only; otherwise drop or gate them behind a "download the SDK yourself" instruction. This is the one place the toolbox's free-for-commercial rule can't hold, so actiCHamp/V-Amp support stays quarantined as an optional plugin.
- No reverse engineering / decompiling; BP provides interface documentation on request, free of
charge → request it to ground the clean-room
actichamp/vampdrivers rather than relying on behavioural inference.
See docs/ACTICHAMP.md (clean-room rationale + this SDK licence) and CLAUDE.md §3 (licensing decision).
Lab-internal research use today is within the granted (non-commercial) licence.
Git — required, dev/build-time only (nothing here ships)¶
environment.yml pulls git from conda-forge (GPL-2.0-only — the tool itself, not a
library the app imports/links/distributes, so it sits outside the commercial-safe gate above,
same footing as the CI actions below). Two things in this repo actually run it:
tests/test_compliance_data.py (git ls-files, the COMPLIANCE R1 guard) and
ssvep.analysis.batch._git_sha() (stamps the commit into an analysis run's provenance).
It's a conda dependency, not just a README instruction to "have git installed", because cloning
the repo doesn't guarantee a system-PATH git exists for other processes to find: GitHub
Desktop and VS Code's built-in Git support both bundle their own git and don't add one to PATH
by default. conda env create gives every install a git that works regardless of what was
used to clone. (You still need some git client to clone in the first place — any of the above
is fine for that one step.)
Documentation site (build-time only — nothing here ships)¶
The website at ssvep-toolbox.org is built from docs/ by MkDocs.
These live in requirements-docs.txt, deliberately not in environment.yml: the
acquisition PCs run the ssvep env and none of them build the website, so there is no reason to
make every bench install carry a docs toolchain. Cloudflare Pages and .github/workflows/docs.yml
install from that file; to preview locally, pip install -r requirements-docs.txt into any env and
run mkdocs serve.
| Package | Purpose | License | Commercial-safe |
|---|---|---|---|
| mkdocs | static-site generator | BSD-2 | ✅ |
| mkdocs-material | theme, search, navigation | MIT | ✅ |
| mkdocstrings[python] | API reference generated from src/ssvep docstrings |
ISC | ✅ |
| mike | versioned docs (v0.9 / v1.0 / latest) |
BSD-3 | ✅ |
Versions are pinned in requirements-docs.txt: a docs build that silently changes because a
theme released a new minor is a bad trade for a site that has to stay citable. mkdocstrings reads
the source with griffe (static analysis) rather than importing it, so the docs job does not
need mne/brainflow/PySide6 — keep it that way.
Deployment is Cloudflare Pages, not GitHub Pages. That decouples site visibility from
repository visibility — the docs can be public while the source is still private ahead of release —
and means no deploy credential has to live in GitHub Actions. What CI does is run
mkdocs build --strict, so a broken link or a stale nav entry fails the PR rather than the site.
CI actions (build-time only — nothing here ships)¶
.github/workflows/ci.yml uses these GitHub Actions. They run on GitHub's runners and are not
dependencies of the toolbox — nothing here is imported, linked, or distributed — but they are
third-party code in the build path, so they are listed and licensed like everything else.
| Action | License | Why |
|---|---|---|
actions/checkout |
MIT | check out the repo |
actions/setup-python |
MIT | stock Python for the compliance job (stdlib + pytest only, no conda) |
conda-incubator/setup-miniconda |
BSD-3 | build the real ssvep env from environment.yml on the Windows job |
Pinned to major versions (checkout@v5, setup-python@v6, setup-miniconda@v3) — checkout and
setup-python are on their first Node-24 majors, so no runner falls back off the deprecated Node 20.
CI adds no new runtime or conda dependency: the compliance job installs only pytest, and the
test job builds the env this file already describes. See docs/BRANCHING.md §4 for what each job
enforces and why .gitignore alone isn't the guard.
Notes¶
- Licenses above are the maintainers' stated licenses as of writing; re-verify on install
(pin versions in
environment.yml) — a dependency changing license is a compliance event. - Dev-only tools (pytest, ruff/black, pypdf for reading REB PDFs) are not runtime deps; list
them under a dev section in
environment.yml. - Prefer bare numpy/pandas/scipy over heavier packages when simpler (Aaron's preference).
bids-validator-denois a PyPI wheel published bybids-standard(same repo/release as the JS/Denobids-validator). It ships the validator's JS bundle and pulls in adenowheel (MIT) that vendors the Deno runtime binary — so there is no separate Deno/npm/node install, and nothing is fetched to obtain the tool itself (unlikedeno run jsr:@bids/validator, which fetches the module from JSR on first run). That is not a guarantee of zero network activity: the launcher grants Deno--allow-netunconditionally, and the bundled validator will reach out for remote HED schemas when a dataset declares a HED version, or for the BIDS schema itself if invoked with--schema stable/--schema latest(the schema is otherwise bundled). It reads the dataset locally and never uploads its contents. Seedocs/BIDS.md"Notes / limits" for how to run it.
Platform gotcha — Windows software OpenGL breaks stimulus timing¶
On Windows, conda-forge's mesalib ships a software OpenGL opengl32.dll in
…\envs\ssvep\Library\bin. Conda activation puts Library\bin ahead of System32 on PATH, and
because opengl32.dll is not a protected KnownDLL, pyglet (or Qt) loads that software
rasterizer (llvmpipe) instead of the GPU driver. Software GL has no vsync, so stimulus timing
is invalid — swaps free-run and stall (on the lab rig, a 240 Hz panel "measured" ~103 Hz). Fatal
for SSVEP.
What pulls mesalib in: only the full mne conda metapackage, via
mne → vtk / pyvista → viskores → mesalib. The toolbox uses pylsl directly and imports no mne
3D visualization, so none of that stack is needed. (It is not PySide6 — that is pip-installed
here and bundles its own Qt. An earlier version of this note was wrong about that.)
Three layers of defense — the first two are automatic:
- Runtime (automatic).
import ssveppre-loads the system GPUopengl32.dllbefore pyglet/Qt can bind Mesa's, so a straymesalibno longer forces software GL (src/ssvep/_glboot.py). Vendor-agnostic (NVIDIA/AMD/Intel), and survivesconda env updatebecause it touches no files. - Install (automatic on fresh envs).
environment.ymlpinsmne-baseinstead of fullmne, sovtk/viskores/mesalibare never installed. Apply to an existing env with:conda env update -n ssvep -f environment.yml --prune(or recreate the env). - Manual fallback (rarely needed). If a software renderer still appears — e.g. an old env that
already has
mesaliband hasn't been pruned — disable the shadowing DLL:
Verify (once per acquisition PC):
python -c "from ssvep.stim import display; r=display.measure_refresh_hz(); print(r['renderer'], round(r['measured_hz'],1),'Hz')"
llvmpipe. python -m ssvep.stim.renderer --fullscreen --duration 5 also prints the GPU on its
via … line and warns loudly if a software renderer is active.