Skip to content

Commit 4886ff2

Browse files
committed
wip
1 parent 7c402b4 commit 4886ff2

File tree

4 files changed

+163
-13
lines changed

4 files changed

+163
-13
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies = [
2424
"numpy",
2525
"protobuf>=3.0.0",
2626
"pybids==0.17.0",
27+
"pydantic",
2728
"pydicom",
2829
"python-dateutil",
2930
"scikit-learn",
@@ -56,7 +57,7 @@ line-length = 120
5657
preview = true
5758

5859
[tool.ruff.lint]
59-
ignore = ["E202", "E203", "E221", "E241", "E251", "E272"]
60+
ignore = ["E202", "E203", "E221", "E241", "E251", "E271", "E272"]
6061
select = ["E", "EXE", "F", "I", "N", "RUF", "UP", "W"]
6162

6263
# The strict type checking configuration is used to type check only the modern (typed) modules. An

python/lib/imaging_lib/bids/dataset.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,28 @@ def data_types(self) -> list['BIDSDataType']:
230230
if not file.is_dir():
231231
continue
232232

233+
# TODO: Specialized code because only MEG has a specialized data type for now. However,
234+
# this function should be reworked once every modality has specialized data types.
235+
if file.name == 'meg' and self.meg is not None:
236+
data_types.append(self.meg)
237+
continue
238+
233239
data_types.append(BIDSDataType(self, file.name))
234240

235241
return data_types
236242

243+
@cached_property
244+
def meg(self) -> 'BIDSMEGDataType | None':
245+
"""
246+
The MEG data type directory found in this session directory, if there is one.
247+
"""
248+
249+
meg_data_type_path = self.path / 'meg'
250+
if not meg_data_type_path.exists():
251+
return None
252+
253+
return BIDSMEGDataType(self, 'meg')
254+
237255
@cached_property
238256
def tsv_scans(self) -> dict[str, BidsTsvScan] | None:
239257
"""
@@ -358,3 +376,39 @@ def get_bvec_path(self) -> Path | None:
358376
return None
359377

360378
return bvec_path
379+
380+
381+
class BIDSMEGDataType(BIDSDataType):
382+
@cached_property
383+
def acquisitions(self) -> list['BIDSMEGAcquisition']:
384+
"""
385+
The MEG acquisitions found in the MEG data type.
386+
"""
387+
388+
acquisitions: list[BIDSMEGAcquisition] = []
389+
for acquisition_name in find_dir_meg_acquisition_names(self.path):
390+
acquisitions.append(BIDSMEGAcquisition(self, acquisition_name))
391+
392+
return acquisitions
393+
394+
395+
class BIDSMEGAcquisition:
396+
data_type: BIDSMEGDataType
397+
name: str
398+
sidecar_path: Path
399+
400+
def __init__(self, data_type: BIDSMEGDataType, name: str):
401+
self.data_type = data_type
402+
self.name = name
403+
self.sidecar_path = (self.data_type.path / name).with_suffix('.json')
404+
405+
406+
def find_dir_meg_acquisition_names(dir_path: Path) -> Iterator[str]:
407+
"""
408+
Iterate over the Path objects of the NIfTI files found in a directory.
409+
"""
410+
411+
for item_path in dir_path.iterdir():
412+
name_match = re.search(r'(.+_meg)\.json', item_path.name)
413+
if name_match is not None:
414+
yield name_match.group(1)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Any, Literal
2+
3+
from pydantic import BaseModel, ConfigDict, Field
4+
5+
NA = Literal['n/a']
6+
RecordingType = Literal['continuous', 'epoched', 'discontinuous']
7+
Manufacturer = Literal['CTF', 'Neuromag/Elekta/MEGIN', 'BTi/4D', 'KIT/Yokogawa', 'ITAB', 'KRISS', 'Other']
8+
9+
10+
class BIDSMEGSidecar(BaseModel):
11+
"""
12+
Model for the BIDS MEG sidecar JSON file.
13+
14+
Documentation: https://bids-specification.readthedocs.io/en/stable/modality-specific-files/magnetoencephalography.html#sidecar-json-_megjson
15+
"""
16+
17+
# REQUIRED fields
18+
sampling_frequency : float = Field(..., gt=0)
19+
power_line_frequency : float | NA = Field(...)
20+
dewar_position : str = Field(...)
21+
software_filters : dict[str, dict[str, Any]] | NA = Field(...)
22+
digitized_landmarks : bool = Field(...)
23+
digitized_head_points : bool = Field(...)
24+
25+
# RECOMMENDED fields
26+
meg_channel_count : int | None = Field(None, ge=0)
27+
meg_ref_channel_count : int | None = Field(None, ge=0)
28+
eeg_channel_count : int | None = Field(None, ge=0)
29+
ecog_channel_count : int | None = Field(None, ge=0)
30+
seeg_channel_count : int | None = Field(None, ge=0)
31+
eog_channel_count : int | None = Field(None, ge=0)
32+
ecg_channel_count : int | None = Field(None, ge=0)
33+
emg_channel_count : int | None = Field(None, ge=0)
34+
misc_channel_count : int | None = Field(None, ge=0)
35+
trigger_channel_count : int | None = Field(None, ge=0)
36+
37+
# RECOMMENDED recording fields
38+
recording_duration : float | None = Field(None, ge=0)
39+
recording_type : RecordingType | None = Field(None)
40+
epoch_length : float | None = Field(None, ge=0)
41+
continuous_head_localization : bool | None = Field(None)
42+
head_coil_frequency : list[float] | float | None = Field(None)
43+
max_movement : float | None = Field(None, ge=0)
44+
subject_artefact_description : str | NA | None = Field(None)
45+
associated_empty_room : list[str] | str | None = Field(None)
46+
hardware_filters : dict[str, dict[str, Any]] | NA | None = Field(None)
47+
48+
# OPTIONAL electrical stimulation fields
49+
electrical_stimulation : bool | None = Field(None)
50+
electrical_stimulation_parameters : str | None = Field(None)
51+
52+
# RECOMMENDED hardware information fields
53+
manufacturer : Manufacturer | None = Field(None)
54+
manufacturers_model_name : str | None = Field(None)
55+
software_versions : str | None = Field(None)
56+
device_serial_number : str | None = Field(None)
57+
58+
# REQUIRED and RECOMMENDED task information fields
59+
task_name : str = Field(...)
60+
task_description : str | None = Field(None)
61+
instructions : str | None = Field(None)
62+
cog_atlas_id : str | None = Field(None)
63+
cog_po_id : str | None = Field(None)
64+
65+
# RECOMMENDED institution information fields
66+
institution_name : str | None = Field(None)
67+
institution_address : str | None = Field(None)
68+
institutional_department_name : str | None = Field(None)
69+
70+
# OPTIONAL EEG-specific fields (if recorded with MEG)
71+
eeg_placement_scheme : str | None = Field(None)
72+
cap_manufacturer : str | None = Field(None)
73+
cap_manufacturers_model_name : str | None = Field(None)
74+
eeg_reference : str | None = Field(None)
75+
76+
model_config = ConfigDict(
77+
str_strip_whitespace=True,
78+
extra='forbid',
79+
validate_assignment=True,
80+
populate_by_name=True,
81+
)

python/lib/import_bids_dataset/main.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import re
33
import shutil
44
from pathlib import Path
5-
from typing import Any
5+
from typing import Any, cast
66

77
from lib.config import get_data_dir_path_config, get_default_bids_visit_label_config
88
from lib.database import Database
@@ -11,7 +11,7 @@
1111
from lib.db.queries.session import try_get_session_with_cand_id_visit_label
1212
from lib.eeg import Eeg
1313
from lib.env import Env
14-
from lib.imaging_lib.bids.dataset import BIDSDataset, BIDSDataType, BIDSSession
14+
from lib.imaging_lib.bids.dataset import BIDSDataset, BIDSDataType, BIDSMEGDataType, BIDSSession
1515
from lib.imaging_lib.bids.dataset_description import BidsDatasetDescriptionError
1616
from lib.imaging_lib.bids.tsv_participants import (
1717
BidsTsvParticipant,
@@ -37,10 +37,6 @@
3737
from lib.logging import log, log_error, log_error_exit, log_warning
3838
from lib.util.iter import count
3939

40-
BIDS_EEG_DATA_TYPES = ['eeg', 'ieeg']
41-
42-
BIDS_MRI_DATA_TYPES = ['anat', 'dwi', 'fmap', 'func']
43-
4440

4541
def import_bids_dataset(env: Env, args: Args, legacy_db: Database):
4642
"""
@@ -177,12 +173,15 @@ def import_bids_data_type_files(
177173
Read the provided BIDS data type directory and import it into LORIS.
178174
"""
179175

180-
if data_type.name in BIDS_MRI_DATA_TYPES:
181-
import_bids_mri_data_type_files(env, import_env, args, session, data_type)
182-
elif data_type.name in BIDS_EEG_DATA_TYPES:
183-
import_bids_eeg_data_type_files(env, import_env, args, session, data_type, events_metadata, legacy_db)
184-
else:
185-
log_warning(env, f"Unknown data type '{data_type.name}'. Skipping.")
176+
match data_type.name:
177+
case 'anat' | 'dwi' | 'fmap' | 'func':
178+
import_bids_mri_data_type_files(env, import_env, args, session, data_type)
179+
case 'eeg' | 'ieeg':
180+
import_bids_eeg_data_type_files(env, import_env, args, session, data_type, events_metadata, legacy_db)
181+
case 'meg':
182+
import_bids_meg_data_type_files(env, import_env, args, session, cast(BIDSMEGDataType, data_type))
183+
case _:
184+
log_warning(env, f"Unknown data type '{data_type.name}'. Skipping.")
186185

187186

188187
def import_bids_mri_data_type_files(
@@ -249,6 +248,21 @@ def import_bids_eeg_data_type_files(
249248
)
250249

251250

251+
def import_bids_meg_data_type_files(
252+
env: Env,
253+
import_env: BIDSImportEnv,
254+
args: Args,
255+
session: DbSession,
256+
data_type: BIDSMEGDataType,
257+
):
258+
"""
259+
Read the BIDS MEG data type directory and import its files into LORIS.
260+
"""
261+
262+
for acquisition in data_type.acquisitions:
263+
log(env, f"Found MEG acquisition '{acquisition.name}'.")
264+
265+
252266
def copy_bids_tsv_participants(tsv_participants: dict[str, BidsTsvParticipant], loris_participants_tsv_path: Path):
253267
"""
254268
Copy some participants.tsv rows into the LORIS participants.tsv file, creating it if necessary.

0 commit comments

Comments
 (0)