bidsreader 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bidsreader-0.1.0/.github/workflows/publish.yml +34 -0
- bidsreader-0.1.0/.github/workflows/test.yml +21 -0
- bidsreader-0.1.0/.gitignore +9 -0
- bidsreader-0.1.0/LICENSE +21 -0
- bidsreader-0.1.0/PKG-INFO +494 -0
- bidsreader-0.1.0/README.md +461 -0
- bidsreader-0.1.0/bidsreader/__init__.py +15 -0
- bidsreader-0.1.0/bidsreader/_errorwrap.py +50 -0
- bidsreader-0.1.0/bidsreader/basereader.py +208 -0
- bidsreader-0.1.0/bidsreader/cmlbidsreader.py +269 -0
- bidsreader-0.1.0/bidsreader/convert.py +57 -0
- bidsreader-0.1.0/bidsreader/exc.py +23 -0
- bidsreader-0.1.0/bidsreader/filtering.py +178 -0
- bidsreader-0.1.0/bidsreader/helpers.py +148 -0
- bidsreader-0.1.0/bidsreader/units.py +287 -0
- bidsreader-0.1.0/bidsreader.egg-info/PKG-INFO +494 -0
- bidsreader-0.1.0/bidsreader.egg-info/SOURCES.txt +31 -0
- bidsreader-0.1.0/bidsreader.egg-info/dependency_links.txt +1 -0
- bidsreader-0.1.0/bidsreader.egg-info/requires.txt +14 -0
- bidsreader-0.1.0/bidsreader.egg-info/top_level.txt +1 -0
- bidsreader-0.1.0/pyproject.toml +50 -0
- bidsreader-0.1.0/setup.cfg +4 -0
- bidsreader-0.1.0/tests/__init__.py +0 -0
- bidsreader-0.1.0/tests/conftest.py +86 -0
- bidsreader-0.1.0/tests/test_basereader.py +295 -0
- bidsreader-0.1.0/tests/test_cmlbidsreader.py +369 -0
- bidsreader-0.1.0/tests/test_convert.py +85 -0
- bidsreader-0.1.0/tests/test_errorwrap.py +106 -0
- bidsreader-0.1.0/tests/test_exc.py +78 -0
- bidsreader-0.1.0/tests/test_filtering.py +114 -0
- bidsreader-0.1.0/tests/test_helpers.py +251 -0
- bidsreader-0.1.0/tests/test_units.py +600 -0
- bidsreader-0.1.0/tutorials/bidsreader_tutorial.ipynb +960 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: pip install build
|
|
17
|
+
- run: python -m build
|
|
18
|
+
- uses: actions/upload-artifact@v4
|
|
19
|
+
with:
|
|
20
|
+
name: dist
|
|
21
|
+
path: dist/
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
needs: build
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment: pypi
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/download-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: pytest tests/ -v --tb=short
|
bidsreader-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Computational Memory Lab, University of Pennsylvania
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bidsreader
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Data loader and file reader for the OpenBIDS format
|
|
5
|
+
Author: Computational Memory Lab
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pennmem/bidsreader
|
|
8
|
+
Project-URL: Repository, https://github.com/pennmem/bidsreader
|
|
9
|
+
Project-URL: Issues, https://github.com/pennmem/bidsreader/issues
|
|
10
|
+
Keywords: bids,eeg,ieeg,neuroscience,mne
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: mne>=1.0
|
|
22
|
+
Requires-Dist: mne-bids>=0.14
|
|
23
|
+
Requires-Dist: numpy>=1.23
|
|
24
|
+
Requires-Dist: pandas>=1.5
|
|
25
|
+
Provides-Extra: ptsa
|
|
26
|
+
Requires-Dist: ptsa; extra == "ptsa"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: bidsreader[dev,ptsa]; extra == "all"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# bidsreader
|
|
35
|
+
|
|
36
|
+
A Python library for reading and working with neuroimaging data stored in the [BIDS (Brain Imaging Data Structure)](https://bids.neuroimaging.io/) 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.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- Load BIDS-compliant EEG/iEEG datasets with minimal boilerplate
|
|
41
|
+
- Automatic detection of device type (EEG vs iEEG) and coordinate space
|
|
42
|
+
- Load events, electrodes, channels, raw data, and epochs through a unified reader API
|
|
43
|
+
- Filter trials by type across events DataFrames, MNE Raw, and MNE Epochs
|
|
44
|
+
- Convert between MNE and PTSA data formats
|
|
45
|
+
- Detect and convert EEG signal units (V, mV, uV, nV, etc.)
|
|
46
|
+
- Custom exception hierarchy for clear, actionable error messages
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
### Prerequisites
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- Access to a BIDS-formatted dataset
|
|
54
|
+
|
|
55
|
+
### Install from source
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone <repository-url>
|
|
59
|
+
cd bidsreader
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **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](#development-setup)).
|
|
64
|
+
|
|
65
|
+
### Dependencies
|
|
66
|
+
|
|
67
|
+
**Required:**
|
|
68
|
+
|
|
69
|
+
| Package | Purpose |
|
|
70
|
+
|------------|----------------------------------|
|
|
71
|
+
| mne | EEG data structures and I/O |
|
|
72
|
+
| mne-bids | BIDS path resolution and reading |
|
|
73
|
+
| pandas | Tabular data (events, channels) |
|
|
74
|
+
| numpy | Numeric operations |
|
|
75
|
+
|
|
76
|
+
**Optional:**
|
|
77
|
+
|
|
78
|
+
| Package | Purpose |
|
|
79
|
+
|---------|------------------------------------------|
|
|
80
|
+
| ptsa | PTSA TimeSeries conversion (`convert_unit`, `mne_*_to_ptsa`) |
|
|
81
|
+
| pytest | Running the test suite |
|
|
82
|
+
|
|
83
|
+
Install all dependencies:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install mne mne-bids pandas numpy
|
|
87
|
+
# Optional
|
|
88
|
+
pip install ptsa pytest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
*See a more robust tutorial in tutorials/*
|
|
94
|
+
|
|
95
|
+
### Basic usage with CMLBIDSReader
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from bidsreader import CMLBIDSReader
|
|
99
|
+
|
|
100
|
+
# Initialize a reader (defaults to /data/LTP_BIDS for CML data)
|
|
101
|
+
reader = CMLBIDSReader(subject="R1001P", task="FR1", session=0)
|
|
102
|
+
|
|
103
|
+
# Load behavioral events
|
|
104
|
+
events = reader.load_events("beh")
|
|
105
|
+
|
|
106
|
+
# Load electrode locations
|
|
107
|
+
electrodes = reader.load_electrodes()
|
|
108
|
+
|
|
109
|
+
# Load channel metadata (intracranial requires acquisition type)
|
|
110
|
+
channels = reader.load_channels("monopolar")
|
|
111
|
+
|
|
112
|
+
# Load combined channel + electrode data
|
|
113
|
+
combined = reader.load_combined_channels("bipolar")
|
|
114
|
+
|
|
115
|
+
# Load raw EEG data (returns MNE Raw object)
|
|
116
|
+
raw = reader.load_raw(acquisition="monopolar")
|
|
117
|
+
|
|
118
|
+
# Load epochs around events
|
|
119
|
+
epochs = reader.load_epochs(tmin=-0.5, tmax=1.5, acquisition="monopolar")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Using a custom BIDS root
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
reader = CMLBIDSReader(
|
|
126
|
+
root="/path/to/your/bids/dataset",
|
|
127
|
+
subject="sub01",
|
|
128
|
+
task="rest",
|
|
129
|
+
session="01",
|
|
130
|
+
device="eeg",
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Querying dataset metadata
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
reader = CMLBIDSReader(root="/data/LTP_BIDS", subject="R1001P", task="FR1")
|
|
138
|
+
|
|
139
|
+
# List all subjects in the dataset
|
|
140
|
+
subjects = reader.get_dataset_subjects()
|
|
141
|
+
|
|
142
|
+
# List all tasks in the dataset
|
|
143
|
+
tasks = reader.get_dataset_tasks()
|
|
144
|
+
|
|
145
|
+
# List sessions for this subject
|
|
146
|
+
sessions = reader.get_subject_sessions()
|
|
147
|
+
|
|
148
|
+
# List tasks for this subject
|
|
149
|
+
subject_tasks = reader.get_subject_tasks()
|
|
150
|
+
|
|
151
|
+
# Get the highest session number across all subjects
|
|
152
|
+
max_session = reader.get_dataset_max_sessions(outlier_thresh=100)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Changing reader fields after creation
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
reader = CMLBIDSReader(subject="R1001P", task="FR1", session=0)
|
|
159
|
+
|
|
160
|
+
# Switch to a different session
|
|
161
|
+
reader.set_fields(session=1)
|
|
162
|
+
|
|
163
|
+
# Switch subject and task
|
|
164
|
+
reader.set_fields(subject="R1002P", task="catFR1")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Filtering events by trial type
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from bidsreader import filter_events_df_by_trial_types, filter_by_trial_types
|
|
171
|
+
|
|
172
|
+
# Filter a DataFrame
|
|
173
|
+
events = reader.load_events("beh")
|
|
174
|
+
word_events, indices = filter_events_df_by_trial_types(events, ["WORD"])
|
|
175
|
+
|
|
176
|
+
# Filter across multiple data objects at once (with consistency checks)
|
|
177
|
+
filtered_df, filtered_raw_events, filtered_epochs, event_id, idx = filter_by_trial_types(
|
|
178
|
+
["WORD", "STIM"],
|
|
179
|
+
events_df=events,
|
|
180
|
+
epochs=epochs,
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Unit detection and conversion
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from bidsreader import detect_unit, get_scale_factor, convert_unit
|
|
188
|
+
|
|
189
|
+
# Detect the unit of an MNE object
|
|
190
|
+
unit = detect_unit(raw) # e.g., "V"
|
|
191
|
+
|
|
192
|
+
# Get conversion factor
|
|
193
|
+
factor = get_scale_factor("V", "uV") # 1_000_000.0
|
|
194
|
+
|
|
195
|
+
# Convert data to a target unit (returns a copy by default)
|
|
196
|
+
raw_uv = convert_unit(raw, "uV")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Converting MNE data to PTSA TimeSeries
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from bidsreader import mne_epochs_to_ptsa, mne_raw_to_ptsa
|
|
203
|
+
|
|
204
|
+
# Convert epochs (requires events DataFrame with 'sample' column)
|
|
205
|
+
ts = mne_epochs_to_ptsa(epochs, events)
|
|
206
|
+
|
|
207
|
+
# Convert raw data (optionally select channels and time window)
|
|
208
|
+
ts = mne_raw_to_ptsa(raw, picks=["E1", "E2"], tmin=0.0, tmax=10.0)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Architecture
|
|
212
|
+
|
|
213
|
+
### Class hierarchy
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
BaseReader # Abstract base — BIDS path construction, metadata queries, field validation
|
|
217
|
+
└── CMLBIDSReader # Concrete reader for the CML (Computational Memory Lab) dataset
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Module overview
|
|
221
|
+
|
|
222
|
+
| Module | Purpose |
|
|
223
|
+
|------------------|----------------------------------------------------------|
|
|
224
|
+
| `basereader.py` | `BaseReader` class — shared BIDS logic and metadata queries |
|
|
225
|
+
| `cmlbidsreader.py` | `CMLBIDSReader` — CML-specific loading and auto-detection |
|
|
226
|
+
| `filtering.py` | Trial-type filtering for DataFrames, MNE Raw, and Epochs |
|
|
227
|
+
| `convert.py` | MNE to PTSA TimeSeries conversion |
|
|
228
|
+
| `units.py` | Unit detection, scaling, and conversion |
|
|
229
|
+
| `helpers.py` | Utility functions (validation, BIDS prefix handling, bipolar electrode merging) |
|
|
230
|
+
| `exc.py` | Custom exception hierarchy |
|
|
231
|
+
| `_errorwrap.py` | `@public_api` decorator for consistent exception wrapping |
|
|
232
|
+
|
|
233
|
+
### Exception hierarchy
|
|
234
|
+
|
|
235
|
+
All exceptions inherit from `BIDSReaderError`, so you can catch everything with a single handler:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
BIDSReaderError
|
|
239
|
+
├── InvalidOptionError # Invalid argument value
|
|
240
|
+
├── MissingRequiredFieldError # Required reader field not set
|
|
241
|
+
├── FileNotFoundBIDSError # Expected BIDS file not found
|
|
242
|
+
├── AmbiguousMatchError # Multiple files matched when one expected
|
|
243
|
+
├── DataParseError # TSV/JSON parsing failure
|
|
244
|
+
├── DependencyError # Optional dependency issue
|
|
245
|
+
└── ExternalLibraryError # Unexpected error from MNE/pandas/etc.
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from bidsreader.exc import BIDSReaderError, FileNotFoundBIDSError
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
events = reader.load_events()
|
|
253
|
+
except FileNotFoundBIDSError:
|
|
254
|
+
print("Events file not found for this subject/session")
|
|
255
|
+
except BIDSReaderError as e:
|
|
256
|
+
print(f"Something went wrong: {e}")
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Creating a New Reader
|
|
260
|
+
|
|
261
|
+
To support a different BIDS dataset, subclass `BaseReader` and implement your dataset-specific logic. Here is a step-by-step guide.
|
|
262
|
+
|
|
263
|
+
### Step 1: Create your reader class
|
|
264
|
+
|
|
265
|
+
Create a new file (e.g., `bidsreader/myreader.py`):
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
import pandas as pd
|
|
269
|
+
import mne
|
|
270
|
+
from pathlib import Path
|
|
271
|
+
from typing import Optional, Union
|
|
272
|
+
from .basereader import BaseReader
|
|
273
|
+
from ._errorwrap import public_api
|
|
274
|
+
from .helpers import validate_option
|
|
275
|
+
from .exc import FileNotFoundBIDSError
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class MyDatasetReader(BaseReader):
|
|
279
|
+
"""Reader for the My Dataset BIDS archive."""
|
|
280
|
+
|
|
281
|
+
# Valid options for constrained fields
|
|
282
|
+
VALID_DEVICES = ("eeg", "meg")
|
|
283
|
+
|
|
284
|
+
def __init__(
|
|
285
|
+
self,
|
|
286
|
+
root: Optional[Union[str, Path]] = "/data/my_dataset",
|
|
287
|
+
subject: Optional[str] = None,
|
|
288
|
+
task: Optional[str] = None,
|
|
289
|
+
session: Optional[str | int] = None,
|
|
290
|
+
space: Optional[str] = None,
|
|
291
|
+
acquisition: Optional[str] = None,
|
|
292
|
+
device: Optional[str] = None,
|
|
293
|
+
):
|
|
294
|
+
# Validate device before passing to base
|
|
295
|
+
device = validate_option("device", device, self.VALID_DEVICES)
|
|
296
|
+
super().__init__(
|
|
297
|
+
root=root,
|
|
298
|
+
subject=subject,
|
|
299
|
+
task=task,
|
|
300
|
+
session=session,
|
|
301
|
+
space=space,
|
|
302
|
+
acquisition=acquisition,
|
|
303
|
+
device=device,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# --- Override auto-detection hooks ---
|
|
307
|
+
|
|
308
|
+
def _determine_device(self) -> Optional[str]:
|
|
309
|
+
"""Infer device type from subject ID or dataset structure.
|
|
310
|
+
|
|
311
|
+
Return None if it cannot be determined.
|
|
312
|
+
"""
|
|
313
|
+
if self.subject is None:
|
|
314
|
+
return None
|
|
315
|
+
# Example: subjects starting with "MEG" use MEG
|
|
316
|
+
if self.subject.startswith("MEG"):
|
|
317
|
+
return "meg"
|
|
318
|
+
return "eeg"
|
|
319
|
+
|
|
320
|
+
def _determine_space(self) -> Optional[str]:
|
|
321
|
+
"""Infer coordinate space from files on disk.
|
|
322
|
+
|
|
323
|
+
Return None or raise FileNotFoundBIDSError / AmbiguousMatchError
|
|
324
|
+
if it cannot be determined.
|
|
325
|
+
"""
|
|
326
|
+
# Implement dataset-specific logic here
|
|
327
|
+
return "MNI152NLin2009aSym"
|
|
328
|
+
|
|
329
|
+
# --- Add your loading methods ---
|
|
330
|
+
|
|
331
|
+
@public_api
|
|
332
|
+
def load_events(self) -> pd.DataFrame:
|
|
333
|
+
"""Load behavioral events for the current subject/session/task."""
|
|
334
|
+
self._require(("subject", "task", "session", "device"), context="load_events")
|
|
335
|
+
|
|
336
|
+
bp = self._bp(datatype="beh", suffix="beh", extension=".tsv")
|
|
337
|
+
matches = bp.match()
|
|
338
|
+
if not matches:
|
|
339
|
+
raise FileNotFoundBIDSError(f"No events file found for {bp}")
|
|
340
|
+
|
|
341
|
+
return pd.read_csv(matches[0].fpath, sep="\t")
|
|
342
|
+
|
|
343
|
+
@public_api
|
|
344
|
+
def load_raw(self) -> mne.io.BaseRaw:
|
|
345
|
+
"""Load raw continuous data."""
|
|
346
|
+
from mne_bids import read_raw_bids
|
|
347
|
+
|
|
348
|
+
self._require(("subject", "task", "session", "device"), context="load_raw")
|
|
349
|
+
bp = self._bp(datatype=self.device)
|
|
350
|
+
return read_raw_bids(bp)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Step 2: Key patterns to follow
|
|
354
|
+
|
|
355
|
+
1. **Validate constrained fields in `__init__`** using `validate_option()` before calling `super().__init__()`.
|
|
356
|
+
|
|
357
|
+
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.
|
|
358
|
+
|
|
359
|
+
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.
|
|
360
|
+
|
|
361
|
+
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.
|
|
362
|
+
|
|
363
|
+
5. **Decorate all public methods with `@public_api`** so that external exceptions (FileNotFoundError, JSONDecodeError, etc.) are automatically mapped to the `BIDSReaderError` hierarchy.
|
|
364
|
+
|
|
365
|
+
6. **Use `self._add_bids_prefix(field, value)`** when you need to manually construct BIDS-prefixed path segments (e.g., `"sub-001"`, `"ses-0"`).
|
|
366
|
+
|
|
367
|
+
### Step 3: Export your reader
|
|
368
|
+
|
|
369
|
+
Add your reader to [\_\_init\_\_.py](bidsreader/__init__.py):
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from .myreader import MyDatasetReader
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Step 4: Write tests
|
|
376
|
+
|
|
377
|
+
Follow the patterns in [tests/conftest.py](tests/conftest.py) for fixtures and [tests/test_cmlbidsreader.py](tests/test_cmlbidsreader.py) for test structure. Key patterns:
|
|
378
|
+
|
|
379
|
+
- Use `tmp_path` fixtures to create temporary BIDS directory structures
|
|
380
|
+
- Use skip decorators for integration tests that require real data on disk
|
|
381
|
+
- Test both the happy path and error cases (missing fields, invalid options, missing files)
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
import pytest
|
|
385
|
+
from bidsreader import MyDatasetReader
|
|
386
|
+
|
|
387
|
+
@pytest.fixture
|
|
388
|
+
def my_reader(tmp_path):
|
|
389
|
+
return MyDatasetReader(root=tmp_path, subject="EEG001", task="rest", session=1)
|
|
390
|
+
|
|
391
|
+
def test_device_detection(my_reader):
|
|
392
|
+
assert my_reader.device == "eeg"
|
|
393
|
+
|
|
394
|
+
def test_missing_field_raises(tmp_path):
|
|
395
|
+
reader = MyDatasetReader(root=tmp_path, subject="EEG001", task="rest")
|
|
396
|
+
reader.session = None
|
|
397
|
+
with pytest.raises(Exception):
|
|
398
|
+
reader.load_events()
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Development Setup
|
|
402
|
+
|
|
403
|
+
### Running tests
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
# Run all tests
|
|
407
|
+
python -m pytest tests/
|
|
408
|
+
|
|
409
|
+
# Run a specific test file
|
|
410
|
+
python -m pytest tests/test_basereader.py -v
|
|
411
|
+
|
|
412
|
+
# Run with output
|
|
413
|
+
python -m pytest tests/ -v -s
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Integration tests that depend on real data at `/data/LTP_BIDS/` are skipped automatically when that data is not available.
|
|
417
|
+
|
|
418
|
+
### Creating a pyproject.toml (recommended)
|
|
419
|
+
|
|
420
|
+
If you want proper `pip install -e .` support, create a `pyproject.toml`:
|
|
421
|
+
|
|
422
|
+
```toml
|
|
423
|
+
[build-system]
|
|
424
|
+
requires = ["setuptools>=64"]
|
|
425
|
+
build-backend = "setuptools.backends._legacy:_Backend"
|
|
426
|
+
|
|
427
|
+
[project]
|
|
428
|
+
name = "bidsreader"
|
|
429
|
+
version = "0.1.0"
|
|
430
|
+
description = "Data loader and file reader for the OpenBIDS format"
|
|
431
|
+
requires-python = ">=3.10"
|
|
432
|
+
dependencies = [
|
|
433
|
+
"mne",
|
|
434
|
+
"mne-bids",
|
|
435
|
+
"pandas",
|
|
436
|
+
"numpy",
|
|
437
|
+
]
|
|
438
|
+
|
|
439
|
+
[project.optional-dependencies]
|
|
440
|
+
ptsa = ["ptsa"]
|
|
441
|
+
dev = ["pytest"]
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Then install with:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
pip install -e ".[dev]"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## API Reference
|
|
451
|
+
|
|
452
|
+
### BaseReader
|
|
453
|
+
|
|
454
|
+
| Method | Description |
|
|
455
|
+
|--------|-------------|
|
|
456
|
+
| `set_fields(**kwargs)` | Set multiple reader fields at once (chainable) |
|
|
457
|
+
| `get_dataset_subjects()` | List all subjects in the dataset |
|
|
458
|
+
| `get_dataset_tasks()` | List all tasks in the dataset |
|
|
459
|
+
| `get_subject_sessions()` | List sessions for the current subject |
|
|
460
|
+
| `get_subject_tasks()` | List tasks for the current subject |
|
|
461
|
+
| `get_dataset_max_sessions(outlier_thresh=None)` | Get highest session number across all subjects |
|
|
462
|
+
|
|
463
|
+
### CMLBIDSReader
|
|
464
|
+
|
|
465
|
+
Inherits all `BaseReader` methods, plus:
|
|
466
|
+
|
|
467
|
+
| Method | Description |
|
|
468
|
+
|--------|-------------|
|
|
469
|
+
| `is_intracranial()` | Returns `True` if device is `"ieeg"` |
|
|
470
|
+
| `load_events(event_type="beh")` | Load events TSV (`"beh"` or device-type events) |
|
|
471
|
+
| `load_electrodes()` | Load electrode coordinates TSV |
|
|
472
|
+
| `load_channels(acquisition=None)` | Load channel metadata TSV (iEEG requires `"monopolar"` or `"bipolar"`) |
|
|
473
|
+
| `load_combined_channels(acquisition=None)` | Merge channel + electrode data into one DataFrame |
|
|
474
|
+
| `load_coordsystem_desc()` | Load coordinate system JSON metadata |
|
|
475
|
+
| `load_raw(acquisition=None)` | Load raw continuous data (returns `mne.io.BaseRaw`) |
|
|
476
|
+
| `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 |
|
|
477
|
+
|
|
478
|
+
### Standalone Functions
|
|
479
|
+
|
|
480
|
+
| Function | Module | Description |
|
|
481
|
+
|----------|--------|-------------|
|
|
482
|
+
| `filter_events_df_by_trial_types(events_df, trial_types)` | `filtering` | Filter events DataFrame by trial type |
|
|
483
|
+
| `filter_raw_events_by_trial_types(raw, trial_types)` | `filtering` | Filter MNE Raw annotations by trial type |
|
|
484
|
+
| `filter_epochs_by_trial_types(epochs, trial_types)` | `filtering` | Filter MNE Epochs by trial type |
|
|
485
|
+
| `filter_by_trial_types(trial_types, *, events_df, raw, epochs)` | `filtering` | Filter multiple data objects with consistency checks |
|
|
486
|
+
| `detect_unit(data, current_unit=None)` | `units` | Detect or validate EEG data unit |
|
|
487
|
+
| `get_scale_factor(from_unit, to_unit)` | `units` | Get multiplicative conversion factor between units |
|
|
488
|
+
| `convert_unit(data, target, *, current_unit=None, copy=True)` | `units` | Convert EEG data to a target unit |
|
|
489
|
+
| `mne_epochs_to_ptsa(epochs, events)` | `convert` | Convert MNE Epochs to PTSA TimeSeries |
|
|
490
|
+
| `mne_raw_to_ptsa(raw, picks=None, tmin=None, tmax=None)` | `convert` | Convert MNE Raw to PTSA TimeSeries |
|
|
491
|
+
|
|
492
|
+
## License
|
|
493
|
+
|
|
494
|
+
TBD
|