Metadata-Version: 2.4
Name: readmanager
Version: 1.5.0
Summary: Read, write and convert OpenBCI signal recordings (.raw/.xml/.tag)
Author: Faculty of Physics, University of Warsaw
Author-email: BrainTech <admin@braintech.pl>
License-Expression: GPL-3.0-or-later
Keywords: bci,eeg,obci,signal,readmanager
Classifier: Development Status :: 4 - Beta
Classifier: Natural Language :: English
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Environment :: Console
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.24
Provides-Extra: mne
Requires-Dist: mne>=1.5; extra == "mne"
Provides-Extra: balance
Requires-Dist: scipy; extra == "balance"
Requires-Dist: matplotlib; extra == "balance"
Provides-Extra: all
Requires-Dist: readmanager[balance,mne]; extra == "all"
Provides-Extra: test
Requires-Dist: pytest>=7.0; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-timeout; extra == "test"
Requires-Dist: readmanager[all]; extra == "test"

# readmanager

Read, write and convert [OBCI](https://gitlab.com/fuw_software/obci) signal
recordings stored in the OBCI file format (`.raw` / `.xml` / `.tag`).

## Installation

```bash
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

```python
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"))

# 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`](https://mne.tools/stable/generated/mne.io.Raw.html#mne.io.Raw.crop):

```python
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:

```python
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

```python
# 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:

```python
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:

```python
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](https://gitlab.com/fuw_software/obci) for
signal file replay and post-recording correction.  Related packages:

- [obci-server](https://gitlab.com/fuw_software/obci) — EEG acquisition server (depends on readmanager)
- [obci-desktop](https://gitlab.com/fuw_software/obci-desktop) — desktop launcher and LSL streaming
- [obci-psychopy](https://gitlab.com/fuw_software/obci-psychopy) — PsychoPy tag integration
- [SVAROG4](https://gitlab.com/fuw_software/svarog4) — Java signal viewer/recorder

## License

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

Originally developed by [BrainTech](http://www.braintech.pl) and the
Faculty of Physics, University of Warsaw.
