Skip to content

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_hz in 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 SDKActiChamp_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 SDKFirstAmp.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 / .sys driver). 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/vamp drivers 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-deno is a PyPI wheel published by bids-standard (same repo/release as the JS/Deno bids-validator). It ships the validator's JS bundle and pulls in a deno wheel (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 (unlike deno 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-net unconditionally, 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. See docs/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:

  1. Runtime (automatic). import ssvep pre-loads the system GPU opengl32.dll before pyglet/Qt can bind Mesa's, so a stray mesalib no longer forces software GL (src/ssvep/_glboot.py). Vendor-agnostic (NVIDIA/AMD/Intel), and survives conda env update because it touches no files.
  2. Install (automatic on fresh envs). environment.yml pins mne-base instead of full mne, so vtk / viskores / mesalib are never installed. Apply to an existing env with: conda env update -n ssvep -f environment.yml --prune (or recreate the env).
  3. Manual fallback (rarely needed). If a software renderer still appears — e.g. an old env that already has mesalib and hasn't been pruned — disable the shadowing DLL:
    conda activate ssvep
    Rename-Item "$env:CONDA_PREFIX\Library\bin\opengl32.dll" opengl32.dll.disabled
    

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')"
Expect a hardware GPU (e.g. NVIDIA GeForce …) and a rate near the panel's nominal — not 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.