eeg2edf
Readers for proprietary clinical EEG formats: lossless converters to EDF+
with a JSON sidecar carrying the metadata EDF+ has nowhere to put, and
MNE-Python readers that open the same recordings directly as mne.io.Raw.
| Format | Command | MNE reader | Notes |
|---|---|---|---|
Nihon Kohden EEG-1100 / EEG-1200A (.EEG + .21E/.PNT/.LOG/.PTN) |
nk2edf |
read_raw_nihon_kohden |
one EDF+C per clip; .LOG events become annotations |
Nicolet / Nervus .e |
nicolet2edf |
read_raw_nicolet |
one EDF+C per stored segment; --concat for the vendor timeline |
Micromed VWR / Brain-Quick .vwr |
vwr2edf |
read_raw_micromed |
montage, events and trigger tracks decoded |
All three converters are lossless: the EDF digital range is the stored range, so sample values round-trip exactly. The MNE readers hand MNE the same stored integers with their exact calibration.
Why
These formats are readable only through vendor software that is discontinued, Windows-only, or dependent on COM DLLs that no longer ship. Each reader here was written by reverse-engineering the container against real recordings and verifying the output independently — see each tool's notes in docs/ for the evidence.
Install
pip install eeg2edf # converters; numpy is the only dependency
pip install "eeg2edf[mne]" # plus the MNE-Python readers
Convert to EDF+
nk2edf INPUT.EEG OUTDIR
nicolet2edf INPUT.e OUTDIR
vwr2edf INPUT.vwr OUTDIR
eeg2edf INPUT OUTDIR # picks the converter from the extension
Every tool takes --list to report what a file contains without converting, and
--no-sidecar to skip the JSON. Per-tool options are in
docs/nk2edf.md, docs/nicolet2edf.md and
docs/vwr2edf.md.
eeg2edf-bipolar-mtg is a small extra: it emits an EDFbrowser .mtg montage
from an SEEG EDF.
Read with MNE-Python
import eeg2edf
from eeg2edf.mne import apply_montage, get_sidecar, montage_names
raw = eeg2edf.read_raw("FILE.EEG") # .EEG, .e or .vwr; data read lazily
raw = eeg2edf.read_raw("FILE.EEG", block="all") # every Nihon Kohden clip, joined
raw = eeg2edf.read_raw("SAMPLE.e", segment=2) # one Nicolet segment
raw.annotations # every event, with its source fields as .extras
meta = get_sidecar(raw) # the eeg2edf-sidecar/1 dict the converter writes
montage_names(raw) # the vendor display montages, e.g. ['ETEST', 'EMU1']
bipolar = apply_montage(raw, "EMU1")
raw = eeg2edf.read_raw("FILE.EEG", montage="auto") # the montage the .LOG names
What MNE has a field for goes there:
| From the recording | In MNE |
|---|---|
| clip start | info["meas_date"] (vendor wall clock, labelled UTC as MNE's EDF reader does) |
| sex, birth date | info["subject_info"] — never a name or record number |
| device | info["device_info"] |
| filter settings, notch (Nicolet) | info["highpass"], info["lowpass"], info["line_freq"] |
| channel units and resolution | each channel's cal and unit; ECG/EOG/EMG, DC (misc) and the NK event word (stim) typed |
| events | raw.annotations, with type, source, GUID and timestamp in extras and the event's channel in ch_names |
segment joins (Nicolet, NK block="all", VWR acquisition cuts) |
BAD boundary / EDGE boundary annotations |
Everything else — the vendor montages, per-channel references and filters,
segment start times, age, events that fall between segments — is the sidecar
itself, stored as JSON in info["description"]. It survives copy(), crop(),
pick() and a FIF save/load, and get_sidecar(raw) reads it back. It
describes the channels as they were read: picking or renaming channels later
does not rewrite it.
Other reader options: ch_types="seeg" (or a dict) overrides the guessed
channel types; positions="standard_1020" ("colin27_1020" from MNE 1.13)
sets sensor positions — off by default, because SEEG contact names such as A1
collide with 10-20 names; include_trends=True adds Nicolet's derived trend
channels, held onto the EEG rate. See each reader's docstring.
The sidecar
Each conversion writes OUTPUT.json beside OUTPUT.edf following the
eeg2edf-sidecar/1 schema — clip, patient, channel, trace, event and segment
records. SIDECAR.md is the normative spec.
eeg2edf.edfcommon builds it and is shared by all three converters and the
MNE readers.
The schema has an external consumer: bellanes-lab reads it to recover per-file montage and reference information. Nothing imports across the two repositories — the contract is the JSON — but changing the schema means checking that reader.
The sidecar never carries a patient name or medical record number. The EDF+
patient field is limited to sex and birth date, which affect interpretation;
--patient overrides it.
Tests
Fully synthetic — no recordings needed.
pip install -e ".[test]"
pytest
tests/synth.py writes small recordings in all three formats from the layouts
the readers document; the MNE tests hold each reader to its converter's EDF.
Releasing
Releases go to PyPI from GitHub Actions (.github/workflows/release.yml) by
trusted publishing — no API token
is stored anywhere.
One-time setup:
- On PyPI and
TestPyPI, add a pending
publisher: project
eeg2edf, ownerBellaNes-Systems, repositoryeeg2edf, workflowrelease.yml, environmentpypi(TestPyPI:testpypi). - In the GitHub repository settings, create the environments
pypiandtestpypi(optionally requiring a reviewer forpypi).
Each release: bump __version__ in src/eeg2edf/__init__.py, add a
CHANGELOG.md entry, merge, then publish a GitHub Release tagged
v<version>. The workflow checks the tag matches, builds, uploads to TestPyPI,
then to PyPI.
Licensing
Apache-2.0 (LICENSE), except src/eeg2edf/micromed/, which is
BSD-3-Clause (src/eeg2edf/micromed/LICENSE) because it contains portions
derived from libvwr, Copyright (C) Franco Milicchio. That notice must be
retained when redistributing those portions, and ships in the wheel. See
NOTICE.
Release files for eeg2edf 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| eeg2edf-0.1.0.tar.gz | 90.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| eeg2edf-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 162.5 kB
Release files / eeg2edf-0.1.0.tar.gz
| Download URL | eeg2edf-0.1.0.tar.gz |
|---|---|
| Size | 90.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
48b8657a10ba91b74e7870ddfa7bdd92271d539fd064001de85b208daf0a2713
|
|
BLAKE2b-256 checksum How to use checksums |
c179380f20ec151840f451a12d523b9e60843830cbf58dc2ee8ecadcde785783
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / eeg2edf-0.1.0-py3-none-any.whl
| Download URL | eeg2edf-0.1.0-py3-none-any.whl |
|---|---|
| Size | 71.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9fd7a0236a390f0f0efa082cd0c1c6a6f9fcd70c3afb28abc2604fd51c515d6f
|
|
BLAKE2b-256 checksum How to use checksums |
d52f4132460cfab8eb3a9593580f934f19be64d6681d793c5870331e1504feff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log