Skip to main content

readmanager

Read, write and convert OBCI signal recordings stored in the OBCI file format (.raw / .xml / .tag).

Installation

pip install readmanager            # core (numpy only)
pip install readmanager[mne]       # + MNE-Python conversion
pip install readmanager[balance]   # + Wii Balance Board analysis (scipy)
pip install readmanager[all]       # everything

Requires Python 3.10+.

Quick start

from obci_readmanager.signal_processing.read_manager import ReadManager

# Open a recording (three files: .raw, .xml, .tag)
mgr = ReadManager(
    "recording.obci.xml",
    "recording.obci.raw",
    "recording.obci.tag",
)

# Access signal parameters
print(mgr.get_param("sampling_frequency"))
print(mgr.get_param("channels_names"))
print(mgr.get_param("eeg_system_symbol"))   # e.g. "EEG_10_20" — cap layout, if recorded

# Get all samples as a numpy array (channels x samples)
samples = mgr.get_samples()

# Get samples for a single channel
fp1 = mgr.get_channel_samples("Fp1")

# Get samples in microvolts (applies gain and offset calibration)
uv = mgr.get_microvolt_samples()

# Iterate over tags (event markers)
for tag in mgr.iter_tags():
    print(tag["name"], tag["start_timestamp"])

Cropping for memory-efficient partial reads

For long recordings that don't fit comfortably in RAM, crop() restricts all subsequent reads to a half-open [from, to) window. No I/O happens at crop time — bounds are stored and every later read honours them, so discarded samples are never loaded from disk. The semantics mirror mne.io.Raw.crop:

mgr = ReadManager("recording.obci.xml", "recording.obci.raw", "recording.obci.tag")

# Keep seconds 30..90 of the recording
mgr.crop(30.0, 90.0)

mgr.duration            # 60.0
mgr.get_samples()       # only the 60 s window — no full-file load
mgr.iter_samples()      # streamed in 1024-sample chunks via the proxy
mgr.save_to_file(out_dir, "trimmed")  # writes only the cropped signal

After crop, an explicit offset passed to get_samples is interpreted inside the cropped view (offset 0 == the crop start), again matching MNE. Tag timestamps are also shifted to the cropped timeline: tags outside the window are dropped and surviving onsets are measured from the new t=0, so mgr.get_mne_raw() after a narrow crop produces an MNE Raw with annotations correctly aligned to its data.

Crop bounds are clamped against the per-channel sample count derived from the data file size, not the .xml sampleCount field (which conflates total values vs. per-channel count on some recordings).

Smart tags

Smart tags extract signal segments aligned to event markers:

from obci_readmanager.signal_processing.smart_tags_manager import SmartTagsManager
from obci_readmanager.signal_processing.tags.smart_tag_definition import SmartTagDurationDefinition

# Define: 1 second of signal after each "stimulus" tag
tag_def = SmartTagDurationDefinition(
    start_tag_name="stimulus",
    start_offset=0.0,
    end_offset=0.0,
    duration=1.0,
)

smart_mgr = SmartTagsManager(
    tag_def,
    "recording.obci.xml",
    "recording.obci.raw",
    "recording.obci.tag",
)

for smart_tag in smart_mgr.iter_smart_tags():
    data = smart_tag.get_samples()  # channels x samples for this epoch
    print(data.shape)

MNE-Python conversion

# ReadManager -> MNE Raw (eager — loads everything into RAM)
raw_mne = mgr.get_mne_raw()

# MNE Raw -> ReadManager
mgr2 = ReadManager.from_mne(raw_mne)

# Smart tags -> MNE Epochs
epochs = smart_mgr.get_mne_epochs()

Lazy MNE bridge (preload=False)

For multi-hour recordings, get_mne_raw(preload=False) returns a real mne.io.BaseRaw subclass instead of a fully materialised RawArray. Sample reads are deferred until MNE actually requests a span — slicing, get_data(start, stop), filtering, plotting — matching the mne.io.read_raw_edf(path, preload=False) idiom:

raw_lazy = mgr.get_mne_raw(preload=False)
chunk = raw_lazy.get_data(start=0, stop=1000)   # only 1000 samples hit RAM
raw_lazy.crop(tmin=10.0, tmax=20.0)             # MNE shifts annotations
raw_lazy.load_data()                            # opt-in preload at any time

Channel parity with the eager path is preserved: the synthetic OBCI_STIM channel is included and populated lazily from a precomputed event index, so raw_lazy.ch_names == raw_mne.ch_names. Annotations are absolute (relative to recording start), so MNE's own Raw.crop/Raw.set_annotations machinery handles tag-time shifting.

Channel type heuristic

When converting to MNE, get_mne_raw() and friends need to assign an MNE channel type ('eeg', 'eog', 'emg', 'ecg', 'bio', 'stim', 'misc') to every channel. If you pass an explicit channel_types list, it's used as-is; otherwise readmanager applies a name-based heuristic via chtype_heuristic(name) from the mne_utils submodule.

The heuristic recognises, in order of priority:

Rule Example inputs Returns
Substring eog EOG Left Horiz, VEOG, EOG Fp1-M2 eog
Substring emg EMG Chin1, EMG Ant Tibia-0 emg
Substring ecg / ekg ECG ECGI, EKG_lead1 ecg
Substring resp / sao2 / spo2 Resp Thermistor, SaO2 SaO2 bio
Substring stim / trig / marker / status / sync or prefix sti STIM, Trigger, STI 014, STATUS stim
Tokenised 10-05 position lookup Fp1, C3, EEG F3-CLE, Fp1-M2, F3-CAR eeg
Fall-through anything else (Channel_42, Aux1, Photo) misc

The 10-05 position check tokenises the channel name on any non-alphanumeric boundary and matches each token against MNE's standard_1005 montage position set. This catches prefixed/suffixed variants common in EDF polysomnography recordings (EEG F3-CLE, EEG Fp1-M2) without false-positives on short position names embedded in unrelated words (e.g. audio1, Data1, misc3).

Non-EEG substring rules take priority over the position lookup, so EOG Fp1-M2 (an EOG reference channel using Fp1/M2 as the reference montage) is correctly typed as eog rather than eeg.

If your recording has channels the heuristic doesn't classify to your taste, pass an explicit list:

raw = mgr.get_mne_raw(channel_types=["eeg"] * 32 + ["ecg", "stim"])

See the chtype_heuristic docstring for the full decision order and edge cases, and test/signal_processing/test_chtype_heuristic.py for the complete parametrised test matrix.

OBCI file format

An OBCI recording consists of three files:

File Content
.obci.raw Binary signal data (channels interleaved, float64 by default)
.obci.xml Recording metadata: channel names, sampling frequency, gains, offsets
.obci.tag Event markers in XML format (name, timestamp, duration, description)

Part of the OBCI ecosystem

readmanager is used by OBCI for signal file replay and post-recording correction. Related packages:

License

GNU General Public License v3 or later (GPLv3+).

Originally developed by BrainTech and the Faculty of Physics, University of Warsaw.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

readmanager-2.0.0.tar.gz (67.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

readmanager-2.0.0-py3-none-any.whl (85.9 kB view details)

Uploaded Python 3

File details

Details for the file readmanager-2.0.0.tar.gz.

File metadata

  • Download URL: readmanager-2.0.0.tar.gz
  • Upload date:
  • Size: 67.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for readmanager-2.0.0.tar.gz
Algorithm Hash digest
SHA256 b412d9648b14828f28f0a5f1add174539ee987dc882e91c8d6db60152a7a8519
MD5 8653dab4d2a46c991e1687fd30b6c15e
BLAKE2b-256 cf4ce69f79544fbda7130eda11aa8eca866c9c4c0b46b6309612da104378ac74

See more details on using hashes here.

File details

Details for the file readmanager-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: readmanager-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 85.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for readmanager-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7cee8e91e270838f1356bfae146fe1f1fccae79db22da3ee9128aec6a1628fa5
MD5 5b9e7407965d98762ab193f67ab95f31
BLAKE2b-256 399bbaffcda5bce660615888c6e530c7e4d3839376909552b4d971775e69cc8d

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

1.6.3

2 files

1.6.2

2 files

1.6.1

2 files

1.6.0

2 files

1.5.1

2 files

1.5.0

2 files

1.4.0

2 files

1.3.0

2 files

1.2.3

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.3

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page