Skip to content

Repository files navigation

bidsreader

A Python library for reading and working with neuroimaging data stored in the BIDS (Brain Imaging Data Structure) format. Provides a structured, object-oriented interface for loading EEG and iEEG data, events, electrodes, and channel metadata, with built-in support for MNE-Python and PTSA.

Features

  • Load BIDS-compliant EEG/iEEG datasets with minimal boilerplate
  • Automatic detection of device type (EEG vs iEEG) and coordinate space
  • Load events, electrodes, channels, raw data, and epochs through a unified reader API
  • Filter trials by type across events DataFrames, MNE Raw, and MNE Epochs
  • Convert between MNE and PTSA data formats
  • Detect and convert EEG signal units (V, mV, uV, nV, etc.)
  • Custom exception hierarchy for clear, actionable error messages

Installation

Prerequisites

  • Python 3.10+
  • Access to a BIDS-formatted dataset

Install from source

git clone <repository-url>cd bidsreader
pip install -e .

Note: The project currently has no pyproject.toml or setup.py. To use it without one, add the project root to your Python path or install in development mode after creating a minimal pyproject.toml (see Development Setup).

Dependencies

Required:

PackagePurpose
mneEEG data structures and I/O
mne-bidsBIDS path resolution and reading
pandasTabular data (events, channels)
numpyNumeric operations

Optional:

PackagePurpose
ptsaPTSA TimeSeries conversion (convert_unit, mne_*_to_ptsa)
pytestRunning the test suite

Install all dependencies:

pip install mne mne-bids pandas numpy
# Optional
pip install ptsa pytest

Quick Start

See a more robust tutorial in tutorials/

Basic usage with CMLBIDSReader

frombidsreaderimportCMLBIDSReader# Initialize a reader (defaults to /data/LTP_BIDS for CML data)reader=CMLBIDSReader(subject="R1001P", task="FR1", session=0)
# Load behavioral eventsevents=reader.load_events("beh")
# Load electrode locationselectrodes=reader.load_electrodes()
# Load channel metadata (intracranial requires acquisition type)channels=reader.load_channels("monopolar")
# Load combined channel + electrode datacombined=reader.load_combined_channels("bipolar")
# Load raw EEG data (returns MNE Raw object)raw=reader.load_raw(acquisition="monopolar")
# Load epochs around eventsepochs=reader.load_epochs(tmin=-0.5, tmax=1.5, acquisition="monopolar")

Using a custom BIDS root

reader=CMLBIDSReader(
root="/path/to/your/bids/dataset",
subject="sub01",
task="rest",
session="01",
device="eeg",
)

Querying dataset metadata

reader=CMLBIDSReader(root="/data/LTP_BIDS", subject="R1001P", task="FR1")
# List all subjects in the datasetsubjects=reader.get_dataset_subjects()
# List all tasks in the datasettasks=reader.get_dataset_tasks()
# List sessions for this subjectsessions=reader.get_subject_sessions()
# List tasks for this subjectsubject_tasks=reader.get_subject_tasks()
# Get the highest session number across all subjectsmax_session=reader.get_dataset_max_sessions(outlier_thresh=100)

Changing reader fields after creation

reader=CMLBIDSReader(subject="R1001P", task="FR1", session=0)
# Switch to a different sessionreader.set_fields(session=1)
# Switch subject and taskreader.set_fields(subject="R1002P", task="catFR1")

Filtering events by trial type

frombidsreaderimportfilter_events_df_by_trial_types, filter_by_trial_types# Filter a DataFrameevents=reader.load_events("beh")
word_events, indices=filter_events_df_by_trial_types(events, ["WORD"])
# Filter across multiple data objects at once (with consistency checks)filtered_df, filtered_raw_events, filtered_epochs, event_id, idx=filter_by_trial_types(
["WORD", "STIM"],
events_df=events,
epochs=epochs,
)

Unit detection and conversion

frombidsreaderimportdetect_unit, get_scale_factor, convert_unit# Detect the unit of an MNE objectunit=detect_unit(raw) # e.g., "V"# Get conversion factorfactor=get_scale_factor("V", "uV") # 1_000_000.0# Convert data to a target unit (returns a copy by default)raw_uv=convert_unit(raw, "uV")

Converting MNE data to PTSA TimeSeries

frombidsreaderimportmne_epochs_to_ptsa, mne_raw_to_ptsa# Convert epochs (requires events DataFrame with 'sample' column)ts=mne_epochs_to_ptsa(epochs, events)
# Convert raw data (optionally select channels and time window)ts=mne_raw_to_ptsa(raw, picks=["E1", "E2"], tmin=0.0, tmax=10.0)

Architecture

Class hierarchy

BaseReader # Abstract base — BIDS path construction, metadata queries, field validation
└── CMLBIDSReader # Concrete reader for the CML (Computational Memory Lab) dataset

Module overview

ModulePurpose
basereader.pyBaseReader class — shared BIDS logic and metadata queries
cmlbidsreader.pyCMLBIDSReader — CML-specific loading and auto-detection
filtering.pyTrial-type filtering for DataFrames, MNE Raw, and Epochs
convert.pyMNE to PTSA TimeSeries conversion
units.pyUnit detection, scaling, and conversion
helpers.pyUtility functions (validation, BIDS prefix handling, bipolar electrode merging)
exc.pyCustom exception hierarchy
_errorwrap.py@public_api decorator for consistent exception wrapping

Exception hierarchy

All exceptions inherit from BIDSReaderError, so you can catch everything with a single handler:

BIDSReaderError
├── InvalidOptionError # Invalid argument value
├── MissingRequiredFieldError # Required reader field not set
├── FileNotFoundBIDSError # Expected BIDS file not found
├── AmbiguousMatchError # Multiple files matched when one expected
├── DataParseError # TSV/JSON parsing failure
├── DependencyError # Optional dependency issue
└── ExternalLibraryError # Unexpected error from MNE/pandas/etc.
frombidsreader.excimportBIDSReaderError, FileNotFoundBIDSErrortry:
events=reader.load_events()
exceptFileNotFoundBIDSError:
print("Events file not found for this subject/session")
exceptBIDSReaderErrorase:
print(f"Something went wrong: {e}")

Creating a New Reader

To support a different BIDS dataset, subclass BaseReader and implement your dataset-specific logic. Here is a step-by-step guide.

Step 1: Create your reader class

Create a new file (e.g., bidsreader/myreader.py):

importpandasaspdimportmnefrompathlibimportPathfromtypingimportOptional, Unionfrom .basereaderimportBaseReaderfrom ._errorwrapimportpublic_apifrom .helpersimportvalidate_optionfrom .excimportFileNotFoundBIDSErrorclassMyDatasetReader(BaseReader):
"""Reader for the My Dataset BIDS archive."""# Valid options for constrained fieldsVALID_DEVICES= ("eeg", "meg")
def__init__(
self,
root: Optional[Union[str, Path]] ="/data/my_dataset",
subject: Optional[str] =None,
task: Optional[str] =None,
session: Optional[str|int] =None,
space: Optional[str] =None,
acquisition: Optional[str] =None,
device: Optional[str] =None,
):
# Validate device before passing to basedevice=validate_option("device", device, self.VALID_DEVICES)
super().__init__(
root=root,
subject=subject,
task=task,
session=session,
space=space,
acquisition=acquisition,
device=device,
)
# --- Override auto-detection hooks ---def_determine_device(self) ->Optional[str]:
"""Infer device type from subject ID or dataset structure. Return None if it cannot be determined. """ifself.subjectisNone:
returnNone# Example: subjects starting with "MEG" use MEGifself.subject.startswith("MEG"):
return"meg"return"eeg"def_determine_space(self) ->Optional[str]:
"""Infer coordinate space from files on disk. Return None or raise FileNotFoundBIDSError / AmbiguousMatchError if it cannot be determined. """# Implement dataset-specific logic herereturn"MNI152NLin2009aSym"# --- Add your loading methods ---@public_apidefload_events(self) ->pd.DataFrame:
"""Load behavioral events for the current subject/session/task."""self._require(("subject", "task", "session", "device"), context="load_events")
bp=self._bp(datatype="beh", suffix="beh", extension=".tsv")
matches=bp.match()
ifnotmatches:
raiseFileNotFoundBIDSError(f"No events file found for {bp}")
returnpd.read_csv(matches[0].fpath, sep="\t")
@public_apidefload_raw(self) ->mne.io.BaseRaw:
"""Load raw continuous data."""frommne_bidsimportread_raw_bidsself._require(("subject", "task", "session", "device"), context="load_raw")
bp=self._bp(datatype=self.device)
returnread_raw_bids(bp)

Step 2: Key patterns to follow

  1. Validate constrained fields in __init__ using validate_option() before calling super().__init__().

  2. Override _determine_device() and _determine_space() to enable automatic detection. These are called lazily the first time reader.device or reader.space is accessed. Return None if detection fails — the base class will emit a warning.

  3. Use self._require(fields, context=...) at the start of each loading method to ensure the necessary fields are set before attempting file I/O.

  4. Use self._bp(**kwargs) to construct BIDSPath objects for file matching. This handles BIDS-standard path construction using the reader's current field values.

  5. Decorate all public methods with @public_api so that external exceptions (FileNotFoundError, JSONDecodeError, etc.) are automatically mapped to the BIDSReaderError hierarchy.

  6. Use self._add_bids_prefix(field, value) when you need to manually construct BIDS-prefixed path segments (e.g., "sub-001", "ses-0").

Step 3: Export your reader

Add your reader to __init__.py:

from .myreaderimportMyDatasetReader

Step 4: Write tests

Follow the patterns in tests/conftest.py for fixtures and tests/test_cmlbidsreader.py for test structure. Key patterns:

  • Use tmp_path fixtures to create temporary BIDS directory structures
  • Use skip decorators for integration tests that require real data on disk
  • Test both the happy path and error cases (missing fields, invalid options, missing files)
importpytestfrombidsreaderimportMyDatasetReader@pytest.fixturedefmy_reader(tmp_path):
returnMyDatasetReader(root=tmp_path, subject="EEG001", task="rest", session=1)
deftest_device_detection(my_reader):
assertmy_reader.device=="eeg"deftest_missing_field_raises(tmp_path):
reader=MyDatasetReader(root=tmp_path, subject="EEG001", task="rest")
reader.session=Nonewithpytest.raises(Exception):
reader.load_events()

Development Setup

Running tests

# Run all tests
python -m pytest tests/
# Run a specific test file
python -m pytest tests/test_basereader.py -v
# Run with output
python -m pytest tests/ -v -s

Integration tests that depend on real data at /data/LTP_BIDS/ are skipped automatically when that data is not available.

Creating a pyproject.toml (recommended)

If you want proper pip install -e . support, create a pyproject.toml:

[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "bidsreader"version = "0.1.0"description = "Data loader and file reader for the OpenBIDS format"requires-python = ">=3.10"dependencies = [
"mne",
"mne-bids",
"pandas",
"numpy",
]
[project.optional-dependencies]
ptsa = ["ptsa"]
dev = ["pytest"]

Then install with:

pip install -e ".[dev]"

API Reference

BaseReader

MethodDescription
set_fields(**kwargs)Set multiple reader fields at once (chainable)
get_dataset_subjects()List all subjects in the dataset
get_dataset_tasks()List all tasks in the dataset
get_subject_sessions()List sessions for the current subject
get_subject_tasks()List tasks for the current subject
get_dataset_max_sessions(outlier_thresh=None)Get highest session number across all subjects

CMLBIDSReader

Inherits all BaseReader methods, plus:

MethodDescription
is_intracranial()Returns True if device is "ieeg"
load_events(event_type="beh")Load events TSV ("beh" or device-type events)
load_electrodes()Load electrode coordinates TSV
load_channels(acquisition=None)Load channel metadata TSV (iEEG requires "monopolar" or "bipolar")
load_combined_channels(acquisition=None)Merge channel + electrode data into one DataFrame
load_coordsystem_desc()Load coordinate system JSON metadata
load_raw(acquisition=None)Load raw continuous data (returns mne.io.BaseRaw)
load_epochs(tmin, tmax, events=None, baseline=None, acquisition=None, event_repeated="merge", channels=None, preload=False)Create mne.Epochs from raw data and events

Standalone Functions

FunctionModuleDescription
filter_events_df_by_trial_types(events_df, trial_types)filteringFilter events DataFrame by trial type
filter_raw_events_by_trial_types(raw, trial_types)filteringFilter MNE Raw annotations by trial type
filter_epochs_by_trial_types(epochs, trial_types)filteringFilter MNE Epochs by trial type
filter_by_trial_types(trial_types, *, events_df, raw, epochs)filteringFilter multiple data objects with consistency checks
detect_unit(data, current_unit=None)unitsDetect or validate EEG data unit
get_scale_factor(from_unit, to_unit)unitsGet multiplicative conversion factor between units
convert_unit(data, target, *, current_unit=None, copy=True)unitsConvert EEG data to a target unit
mne_epochs_to_ptsa(epochs, events)convertConvert MNE Epochs to PTSA TimeSeries
mne_raw_to_ptsa(raw, picks=None, tmin=None, tmax=None)convertConvert MNE Raw to PTSA TimeSeries

License

TBD

About

Data loader and file reader for the OpenBIDS format

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages