From 258fbaef62f441ef05aa3e21e291429012269843 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Mar 2022 11:13:25 -0800 Subject: [PATCH 1/6] wip --- doc/whats_new.rst | 4 + examples/convert_ieeg_to_bids.py | 134 +++++++++++++++++++++++++++++++ mne_bids/config.py | 66 ++++++++++++--- mne_bids/dig.py | 126 ++++++++++++----------------- mne_bids/tests/test_dig.py | 122 ++++++++++++++++++++++++++++ mne_bids/tests/test_write.py | 43 +--------- mne_bids/write.py | 13 +++ 7 files changed, 382 insertions(+), 126 deletions(-) create mode 100644 mne_bids/tests/test_dig.py diff --git a/doc/whats_new.rst b/doc/whats_new.rst index ba9c82ef1..76217e686 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -56,6 +56,8 @@ Enhancements - All non-MNE-Python BIDS coordinate frames are now set to ``'unknown'`` on reading, by `Alex Rockhill`_ (:gh:`979`) +- :func:`mne_bids.write_raw_bids` can now write to template coordinates by `Alex Rockhill`_ (:gh:`980`) + API and behavior changes ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -65,6 +67,8 @@ API and behavior changes - Passing ``fs_subject=None`` to :func:`get_head_mri_trans` has been deprecated. Please pass the FreeSurfer subject name explicitly, by Richard Höchenberger`_ (:gh:`977`) +- Corrupted or missing fiducials in 'head' coordinates now raise an error instead of warning in :func:`mne_bids.write_raw_bids` by `Alex Rockhill`_ (:gh:`980`) + Requirements ^^^^^^^^^^^^ diff --git a/examples/convert_ieeg_to_bids.py b/examples/convert_ieeg_to_bids.py index 936220982..a8983c9b4 100644 --- a/examples/convert_ieeg_to_bids.py +++ b/examples/convert_ieeg_to_bids.py @@ -20,6 +20,9 @@ 5. Repeat the process for the ``fsaverage`` template coordinate frame. +6. Repeat the process for one of the other standard template coordinate frames + allowed by BIDS. + The iEEG data will be written by :func:`write_raw_bids` with the addition of extra metadata elements in the following files: @@ -50,6 +53,7 @@ # %% import os.path as op +import numpy as np import shutil from nilearn.plotting import plot_anat @@ -331,3 +335,133 @@ # ``montage.add_estimated_fiducials(template, os.environ['FREESURFER_HOME'])`` # where ``template`` maybe be ``cvs_avg35_inMNI152`` for instance raw.set_montage(montage) + +# %% +# Step 6: Store coordinates in another template space accepted by BIDS +# -------------------------------------------------------------------- +# As of thid writing, BIDS accepts channel coordinates in reference to the +# the following template spaces: ``ICBM452AirSpace``, ``ICBM452Warp5Space``, +# ``IXI549Space``, ``fsaverage``, ``fsaverageSym``, ``fsLR``, ``MNIColin27``, +# ``MNI152Lin``, ``MNI152NLin2009[a-c][Sym|Asym]``, ``MNI152NLin6Sym``, +# ``MNI152NLin6ASym``, ``MNI305``, ``NIHPD``, ``OASIS30AntsOASISAnts``, +# ``OASIS30Atropos``, ``Talairach`` and ``UNCInfant``. As discussed above, +# it is recommended to share the coordinates in the individual subject's +# anatomical reference frame so that researchers who use the data can +# transform the coordinates to any of these templates that they choose. If +# BIDS-formatted data is shared in one of these template coordinate frames, +# the data can still be analyzed in MNE-Python. However, MNE-Python only +# recognizes a few coordinate frames (so that coordinate frames that are +# not regularly used by the MNE community don't misleadingly appear to be +# being fully maintained when they are not) so you'll need a bit more +# know-how, which we will go over below. + +# ensure the output path doesn't contain any leftover files from previous +# tests and example runs +if op.exists(bids_root): + shutil.rmtree(bids_root) + + +# first we'll write our data as if it were MNI152NLin2009bAsym coordinates +# (you would need to transform your coordinates to this template first) +bids_path.update(datatype='ieeg', space='MNI152NLin2009bAsym') + +# load our raw data again +raw = mne.io.read_raw_fif(op.join( + misc_path, 'seeg', 'sample_seeg_ieeg.fif')) +raw.info['line_freq'] = 60 # specify power line frequency as required by BIDS + +# get the montage as stored in raw +# MNE stores coordinates in raw objects in "head" coordinates for consistency +montage = raw.get_montage() + +# define a transform to MNI152NLin2009bAsym (fake it) +# MNE-Python doesn't recognize MNI152NLin2009bAsym, so we have to use +# the unknown coordinate frame +head_template_t = np.array([[1.0, 0.1, 0.2, 10.1], + [-0.1, 1.0, 0.1, -20.3], + [0.2, -0.1, 1.0, -30.7], + [0.0, 0.0, 0.0, 1.0]]) +head_template_trans = mne.transforms.Transform( + fro='head', to='unknown', trans=head_template_t) +montage.apply_trans(head_template_trans) + +# write to BIDS, with the montage in fsaverage coordinates +write_raw_bids(raw, bids_path, anonymize=dict(daysback=40000), + montage=montage, overwrite=True) + +# %% +# Now, let's see how we would work with the data in MNE. As shown below, the +# montage has the same coordinates as when it was written, but the coordinate +# frame is unknown. + +# read back in the raw data +raw = read_raw_bids(bids_path=bids_path) + +# get the montage +montage2 = raw.get_montage() +print('Montage set to: ' + montage2.get_positions()['coord_frame']) + +# check that we can recover the coordinates +print('Recovered coordinate: {recovered}\n' + 'Saved coordinate: {saved}'.format( + recovered=montage2.dig[0]['r'], + saved=montage.dig[0]['r'])) + +# %% +# To work with this data in the template coordinate frame, all the same steps +# can be followed in :ref:`tut-working-with-seeg` and +# :ref:`tut-working-with-ecog` after the montage is transformed to surface +# RAS coordinates for the template MRI (if it isn't there already). +# +# First we'll need the ``subjects_dir`` where the recon-all for the template +# brain is stored. +# +# .. code-block:: python +# +# subjects_dir = op.join(misc_path, 'subjects') # for example +# # add some plotting keyword arguments +# brain_kwargs = dict(cortex='low_contrast', alpha=0.2, background='white') +# +# If the montage is already in surface RAS for the template MRI, we can use: +# +# .. code-block:: python +# +# # identity transform since 'unknown' is already 'mri' == surface RAS +# # for the template brain MRI +# trans = mne.transforms.Transform( +# fro='unknown', to='mri', trans=np.eye(4)) +# brain = mne.viz.Brain('sample_seeg', subjects_dir=subjects_dir, +# **brain_kwargs) +# brain.add_sensors(raw.info, trans=trans) +# +# If the montage was in voxel coordinates, we'll first have to transform +# to surface RAS: +# +# .. code-block:: python +# +# import nibabel as nib +# template_T1 = nib.load(op.join(subjects_dir, 'MNI152NLin2009bAsym', +# 'mri', 'T1.mgz')) +# trans = mne.transforms.Transform( # use vox to surface RAS transform +# fro='unknown', to='mri', trans=template_T1.header.get_vox2ras_tkr()) +# brain = mne.viz.Brain( +# 'sample_seeg', subjects_dir=subjects_dir, **brain_kwargs) +# brain.add_sensors(raw.info, trans=trans) +# +# +# Finally, if the montage was in scanner RAS coordinates, we'll have to +# transform it back to voxels first before going to surface RAS: +# +# .. code-block:: python +# +# import nibabel as nib +# template_T1 = nib.load(op.join(subjects_dir, 'MNI152NLin2009bAsym', +# 'mri', 'T1.mgz')) +# ras_vox_t = template_T1.header.get_ras2vox() +# vox_mri_t = template_T1.header.get_vox2ras_tkr() +# ras_mri_t = np.dot(ras_vox_t, vox_mri_t) # ras->vox with vox->mri +# trans = mne.transforms.Transform( +# fro='unknown', to='mri', trans=ras_mri_t) +# brain = mne.viz.Brain( +# 'sample_seeg', subjects_dir=subjects_dir, **brain_kwargs) +# brain.add_sensors(raw.info, trans=trans) diff --git a/mne_bids/config.py b/mne_bids/config.py index 37fa074f6..bd581ec02 100644 --- a/mne_bids/config.py +++ b/mne_bids/config.py @@ -163,7 +163,7 @@ # Annotations to never remove during reading or writing ANNOTATIONS_TO_KEEP = ('BAD_ACQ_SKIP',) -coordsys_standard_template = [ +BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES = [ 'ICBM452AirSpace', 'ICBM452Warp5Space', 'IXI549Space', @@ -216,7 +216,7 @@ # accepted coordinate SI units BIDS_COORDINATE_UNITS = ['m', 'cm', 'mm'] coordsys_wildcard = ['Other'] -BIDS_SHARED_COORDINATE_FRAMES = (coordsys_standard_template + +BIDS_SHARED_COORDINATE_FRAMES = (BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES + coordsys_standard_template_deprecated + coordsys_wildcard) @@ -264,11 +264,7 @@ MNE_TO_BIDS_FRAMES = { 'ctf_head': 'CTF', 'head': 'CapTrak', - 'mni_tal': 'fsaverage', - # 'fs_tal': 'fsaverage', # XXX: not used - 'unknown': 'Other', - 'ras': 'Other', - 'mri': 'Other' + 'mni_tal': 'fsaverage' } # these coordinate frames in mne-python are related to scalp/meg @@ -289,7 +285,7 @@ MNE_FRAME_TO_STR = {val: key for key, val in MNE_STR_TO_FRAME.items()} # see BIDS specification for description we copied over from each -BIDS_COORD_FRAME_DESCRIPTIONS = { +BIDS_COORD_FRAME_DESCRIPTIONS = dict(**{ 'acpc': 'The origin of the coordinate system is at the Anterior ' 'Commissure and the negative y-axis is passing through the ' 'Posterior Commissure. The positive z-axis is passing through ' @@ -318,7 +314,59 @@ 'fsaverage': 'Defined by FreeSurfer, the MRI (surface RAS) origin is ' 'at the center of a 256×256×256 mm^3 anisotropic volume ' '(may not be in the center of the head).', -} + 'icbm452airspace': 'Reference space defined by the "average of 452 ' + 'T1-weighted MRIs of normal young adult brains" ' + 'with "linear transforms of the subjects into the ' + 'atlas space using a 12-parameter affine ' + 'transformation"', + 'icbm452warp5space': 'Reference space defined by the "average of 452 ' + 'T1-weighted MRIs of normal young adult brains" ' + '"based on a 5th order polynomial transformation ' + 'into the atlas space"', + 'ixi549space': 'Reference space defined by the average of the "549 (...) ' + 'subjects from the IXI dataset" linearly transformed to ' + 'ICBM MNI 452.', + 'fsaveragesym': 'The fsaverage is a dual template providing both ' + 'volumetric and surface coordinates references. The ' + 'volumetric template corresponds to a FreeSurfer variant ' + 'of MNI305 space. The fsaverageSym atlas also defines a ' + 'symmetric surface reference system (formerly described ' + 'as fsaveragesym).', + 'fslr': 'The fsLR is a dual template providing both volumetric and ' + 'surface coordinates references. The volumetric template ' + 'corresponds to MNI152NLin6Asym. Surface templates are given ' + 'at several sampling densities: 164k (used by HCP pipelines ' + 'for 3T and 7T anatomical analysis), 59k (used by HCP pipelines ' + 'for 7T MRI bold and DWI analysis), 32k (used by HCP pipelines ' + 'for 3T MRI bold and DWI analysis), or 4k (used by HCP ' + 'pipelines for MEG analysis) fsaverage_LR surface ' + 'reconstructed from the T1w image.', + 'mnicolin27': 'Average of 27 T1 scans of a single subject.', + 'mni152lin': 'Also known as ICBM (version with linear coregistration).', + 'mni152nlin6sym': 'Also known as symmetric ICBM 6th generation ' + '(non-linear coregistration).', + 'mni152nlin6asym': 'A variation of MNI152NLin6Sym built by A. Janke that ' + 'is released as the MNI template of FSL. Volumetric ' + 'templates included with HCP-Pipelines correspond to ' + 'this template too.', + 'mni305': 'Also known as avg305.', + 'nihpd': 'Pediatric templates generated from the NIHPD sample. Available ' + 'for different age groups (4.5–18.5 y.o., 4.5–8.5 y.o., ' + '7–11 y.o., 7.5–13.5 y.o., 10–14 y.o., 13–18.5 y.o. This ' + 'template also comes in either -symmetric or -asymmetric flavor.', + 'oasis30antsoasisants': + 'See https://figshare.com/articles/ANTs_ANTsR_Brain_Templates/915436', + 'oasis30atropos': + 'See https://mindboggle.info/data.html', + 'talairach': 'Piecewise linear scaling of the brain is implemented as ' + 'described in TT88.', + 'uncinfant': 'Infant Brain Atlases from Neonates to 1- and 2-year-olds.' +}, + **{f'mni152nlin2009{letter}{sym}': 'Also known as ICBM ' + '(non-linear coregistration with 40 iterations, ' + 'released in 2009). It comes in either three different flavours ' + 'each in symmetric or asymmetric version.' + for letter in ('a', 'b', 'c') for sym in ('Sym', 'Asym')}) REFERENCES = {'mne-bids': 'Appelhoff, S., Sanderson, M., Brooks, T., Vliet, M., ' diff --git a/mne_bids/dig.py b/mne_bids/dig.py index c3cd677f7..5801fc5bd 100644 --- a/mne_bids/dig.py +++ b/mne_bids/dig.py @@ -7,6 +7,7 @@ from collections import OrderedDict from pathlib import Path import re +import warnings import mne import numpy as np @@ -16,6 +17,7 @@ from mne.io.pick import _picks_to_idx from mne_bids.config import (ALLOWED_SPACES, + BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES, BIDS_COORDINATE_UNITS, MNE_TO_BIDS_FRAMES, BIDS_TO_MNE_FRAMES, MNE_FRAME_TO_STR, BIDS_COORD_FRAME_DESCRIPTIONS) @@ -368,7 +370,7 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, overwrite=False): """Write BIDS formatted DigMontage from Raw instance. - Handles coordinatesystem.json and electrodes.tsv writing + Handles coordsystem.json and electrodes.tsv writing from DigMontage. Parameters @@ -398,23 +400,33 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, else: # prevent transformation back to "head", only should be used # in this specific circumstance - if bids_path.datatype == 'ieeg': + if montage.get_positions()['coord_frame'] != 'head': montage.remove_fiducials() - raw.set_montage(montage) + with warnings.catch_warnings(): + warnings.filterwarnings(action='ignore', category=RuntimeWarning, + module='mne') + raw.set_montage(montage) # get coordinate frame from digMontage digpoint = montage.dig[0] if any(digpoint['coord_frame'] != _digpoint['coord_frame'] for _digpoint in montage.dig): - warn("Not all digpoints have the same coordinate frame. " - "Skipping electrodes.tsv writing...") - return + raise RuntimeError("Not all digpoints have the same coordinate frame.") # get the accepted mne-python coordinate frames coord_frame_int = int(digpoint['coord_frame']) mne_coord_frame = MNE_FRAME_TO_STR.get(coord_frame_int, None) coord_frame = MNE_TO_BIDS_FRAMES.get(mne_coord_frame, None) + if coord_frame == 'CapTrak' and \ + bids_path.datatype == 'eeg' or bids_path.datatype == 'nirs': + coords = _extract_landmarks(raw.info['dig']) + landmarks = set(['RPA', 'NAS', 'LPA']) == set(list(coords.keys())) + if not landmarks: + raise RuntimeError("'head' coordinate frame must contain nasion " + "and left and right pre-auricular point " + "landmarks") + if bids_path.datatype == 'ieeg' and mne_coord_frame == 'mri': if not acpc_aligned: raise RuntimeError( @@ -425,9 +437,23 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, '`acpc_aligned=True`') coord_frame = 'ACPC' - if bids_path.datatype == 'ieeg' and bids_path.space is not None and \ - bids_path.space.lower() == 'pixels': - coord_frame = 'Pixels' + if bids_path.space is None: # no space, use MNE coord frame + if coord_frame is None: # if no MNE coord frame, skip + warn("Coordinate frame could not be inferred from the raw object " + "and the BIDSPath.space was none, skipping the writing of " + "channel positions") + return + else: # either a space and an MNE coord frame or just a space + if coord_frame is None: # just a space, use that + coord_frame = bids_path.space + else: # space and raw have coordinate frame, check match + if bids_path.space != coord_frame and not ( + coord_frame == 'fsaverage' and + bids_path.space == 'MNI305'): # fsaverage == MNI305 + raise ValueError('Coordinates in the raw object or montage ' + f'are in the {coord_frame} coordinate ' + 'frame but BIDSPath.space is ' + f'{bids_path.space}') # create electrodes/coordsystem files using a subset of entities # that are specified for these files in the specification @@ -437,77 +463,25 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, 'subject': bids_path.subject, 'session': bids_path.session, 'acquisition': bids_path.acquisition, - 'space': bids_path.space + 'space': coord_frame } - datatype = bids_path.datatype - electrodes_path = BIDSPath(**coord_file_entities, suffix='electrodes', - extension='.tsv') - optodes_path = BIDSPath(**coord_file_entities, suffix='optodes', - extension='.tsv') + channels_suffix = \ + 'optodes' if bids_path.datatype == 'nirs' else 'electrodes' + _channels_fun = _write_optodes_tsv if bids_path.datatype == 'nirs' else \ + _write_electrodes_tsv + channels_path = BIDSPath(**coord_file_entities, suffix=channels_suffix, + extension='.tsv') coordsystem_path = BIDSPath(**coord_file_entities, suffix='coordsystem', extension='.json') - if datatype == 'ieeg': - if coord_frame is not None: - # XXX: To improve when mne-python allows coord_frame='unknown' - # coordinate frame is either - coordsystem_path.update(space=coord_frame) - electrodes_path.update(space=coord_frame) - - # Now write the data to the elec coords and the coordsystem - _write_electrodes_tsv(raw, electrodes_path, - datatype, overwrite) - _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system='n/a', - sensor_coord_system=(coord_frame, - mne_coord_frame), - fname=coordsystem_path, datatype=datatype, - overwrite=overwrite) - else: - # default coordinate frame to mri if not available - warn("Coordinate frame of iEEG coords missing/unknown " - "for {}. Skipping reading " - "in of montage...".format(electrodes_path)) - elif datatype == 'eeg': - # We only write EEG electrodes.tsv and coordsystem.json - # if we have LPA, RPA, and NAS available to rescale to a known - # coordinate system frame - coords = _extract_landmarks(raw.info['dig']) - landmarks = set(['RPA', 'NAS', 'LPA']) == set(list(coords.keys())) - - # XXX: to be improved to allow rescaling if landmarks are present - # mne-python automatically converts unknown coord frame to head - if coord_frame_int == FIFF.FIFFV_COORD_HEAD and landmarks: - # Now write the data - _write_electrodes_tsv(raw, electrodes_path, datatype, - overwrite) - _write_coordsystem_json(raw=raw, unit='m', hpi_coord_system='n/a', - sensor_coord_system='CapTrak', - fname=coordsystem_path, datatype=datatype, - overwrite=overwrite) - else: - warn("Skipping EEG electrodes.tsv... " - "Setting montage not possible if anatomical " - "landmarks (NAS, LPA, RPA) are missing, " - "and coord_frame is not 'head'.") - elif datatype == 'nirs': - # We only write NIRS optodes.tsv and coordsystem.json - # if we have LPA, RPA, and NAS available to rescale to a known - # coordinate system frame - coords = _extract_landmarks(raw.info['dig']) - landmarks = set(['RPA', 'NAS', 'LPA']) == set(list(coords.keys())) - - if coord_frame_int == FIFF.FIFFV_COORD_HEAD and landmarks: - # Now write the data - _write_optodes_tsv(raw, optodes_path, overwrite) - _write_coordsystem_json(raw=raw, unit='m', hpi_coord_system='n/a', - sensor_coord_system='CapTrak', - fname=coordsystem_path, datatype=datatype, - overwrite=overwrite) - else: - warn("Skipping fNIRS optodes.tsv... " - "Setting montage not possible if anatomical " - "landmarks (NAS, LPA, RPA) are missing, " - "and coord_frame is not 'head'.") + # Now write the data to the elec coords and the coordsystem + _channels_fun(raw, channels_path, bids_path.datatype, overwrite) + _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system='n/a', + sensor_coord_system=(coord_frame, + mne_coord_frame), + fname=coordsystem_path, + datatype=bids_path.datatype, + overwrite=overwrite) def _read_dig_bids(electrodes_fpath, coordsystem_fpath, diff --git a/mne_bids/tests/test_dig.py b/mne_bids/tests/test_dig.py new file mode 100644 index 000000000..0d4a1b6ae --- /dev/null +++ b/mne_bids/tests/test_dig.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +"""Test the digitizations. +For each supported coordinate frame, implement a test. +""" +# Authors: Alex Rockhill +# +# License: BSD-3-Clause + +import os +import os.path as op +import numpy as np +import pytest + +import mne +from mne.datasets import testing +from mne_bids import BIDSPath, write_raw_bids +from mne_bids.dig import _write_dig_bids, _read_dig_bids +from mne_bids.config import (BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES, + BIDS_TO_MNE_FRAMES) + +import warnings +warnings.filterwarnings('ignore', category=DeprecationWarning) + + +base_path = op.join(op.dirname(mne.__file__), 'io') +subject_id = '01' +session_id = '01' +run = '01' +acq = '01' +run2 = '02' +task = 'testing' + +_bids_path = BIDSPath( + subject=subject_id, session=session_id, run=run, acquisition=acq, + task=task) + +data_path = testing.data_path() +raw_fname = op.join(data_path, 'MEG', 'sample', + 'sample_audvis_trunc_raw.fif') +raw = mne.io.read_raw(raw_fname) +raw.info['line_freq'] = 60 +montage = raw.get_montage() + + +def test_dig_io(tmp_path): + """Test passing different coordinate frames give proper warnings.""" + bids_root = tmp_path / 'bids1' + for datatype in ('eeg', 'ieeg'): + os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', datatype)) + + # test no coordinate frame in dig or in bids_path.space + mnt = montage.copy() + mnt.apply_trans(mne.transforms.Transform('head', 'unknown')) + for datatype in ('eeg', 'ieeg'): + bids_path = _bids_path.copy().update(root=bids_root, datatype=datatype, + space=None) + with pytest.warns(RuntimeWarning, + match='Coordinate frame could not be inferred'): + _write_dig_bids(bids_path, raw, mnt, acpc_aligned=True) + + # test coordinate frame-BIDSPath.space mismatch + mnt = montage.copy() + bids_path = _bids_path.copy().update( + root=bids_root, datatype='eeg', space='fsaverage') + with pytest.raises(ValueError, match='Coordinates in the raw object ' + 'or montage are in the CapTrak ' + 'coordinate frame but ' + 'BIDSPath.space is fsaverage'): + _write_dig_bids(bids_path, raw, mnt) + + # test MEG space conflict fif (ElektaNeuromag) != CTF + bids_path = _bids_path.copy().update( + root=bids_root, datatype='meg', space='CTF') + with pytest.raises(ValueError, match='conflicts'): + write_raw_bids(raw, bids_path) + + +def test_dig_template(tmp_path): + """Test that eeg and ieeg dig are stored properly.""" + bids_root = tmp_path / 'bids1' + for datatype in ('eeg', 'ieeg'): + os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', datatype)) + + for datatype in ('eeg', 'ieeg'): + bids_path = _bids_path.copy().update(root=bids_root, datatype=datatype) + for coord_frame in BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES: + bids_path.update(space=coord_frame) + mnt = montage.copy() + pos = mnt.get_positions() + mne_coord_frame = BIDS_TO_MNE_FRAMES.get(coord_frame, None) + if mne_coord_frame is None: + mnt.apply_trans(mne.transforms.Transform('head', 'unknown')) + else: + mnt.apply_trans(mne.transforms.Transform( + 'head', mne_coord_frame)) + _write_dig_bids(bids_path, raw, mnt, acpc_aligned=True) + electrodes_path = bids_path.copy().update( + task=None, run=None, suffix='electrodes', extension='.tsv') + coordsystem_path = bids_path.copy().update( + task=None, run=None, suffix='coordsystem', extension='.json') + if bids_path.space == 'Pixels': + with pytest.warns(RuntimeWarning, + match='not recognized by MNE'): + _read_dig_bids(electrodes_path, coordsystem_path, + datatype, raw) + elif mne_coord_frame is None: + with pytest.warns(RuntimeWarning, + match='not an MNE-Python coordinate frame'): + _read_dig_bids(electrodes_path, coordsystem_path, + datatype, raw) + else: + _read_dig_bids(electrodes_path, coordsystem_path, + datatype, raw) + mnt2 = raw.get_montage() + pos2 = mnt2.get_positions() + np.testing.assert_array_almost_equal( + np.array(list(pos['ch_pos'].values())), + np.array(list(pos2['ch_pos'].values()))) + if mne_coord_frame is None: + assert pos2['coord_frame'] == 'unknown' + else: + assert pos2['coord_frame'] == mne_coord_frame diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index 9dcf52796..8f4c4fb3f 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -1312,8 +1312,8 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate, tmp_path): coord_frame='head') raw.set_montage(eeg_montage) # electrodes are not written w/o landmarks - with pytest.warns(RuntimeWarning, match='Skipping EEG electrodes.tsv... ' - 'Setting montage not possible'): + with pytest.raises(RuntimeError, match="'head' coordinate frame must " + "contain nasion"): write_raw_bids(raw, bids_path, overwrite=True) electrodes_fpath = _find_matching_sidecar(bids_path, @@ -3612,42 +3612,3 @@ def test_anonymize_dataset_daysback(tmpdir): rng=np.random.default_rng(), show_progress_thresh=20 ) - - -def test_write_dig(tmpdir): - """Test whether the channel locations are written out properly.""" - # Check progress bar output - bids_root = tmpdir / 'bids' - data_path = Path(testing.data_path()) - raw_path = data_path / 'MEG' / 'sample' / 'sample_audvis_trunc_raw.fif' - - # test coordinates in pixels - bids_path = _bids_path.copy().update( - root=bids_root, datatype='ieeg', space='Pixels') - os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', bids_path.datatype), - exist_ok=True) - raw = _read_raw_fif(raw_path, verbose=False) - raw.pick_types(eeg=True) - raw.del_proj() - raw.set_channel_types({ch: 'ecog' for ch in raw.ch_names}) - - montage = raw.get_montage() - # fake transform to pixel coordinates - montage.apply_trans(mne.transforms.Transform('head', 'unknown')) - with pytest.warns(RuntimeWarning, - match='assuming identity'): - _write_dig_bids(bids_path, raw, montage) - electrodes_path = bids_path.copy().update( - task=None, run=None, suffix='electrodes', extension='.tsv') - coordsystem_path = bids_path.copy().update( - task=None, run=None, suffix='coordsystem', extension='.json') - with pytest.warns(RuntimeWarning, - match='not an MNE-Python coordinate frame'): - _read_dig_bids(electrodes_path, coordsystem_path, - bids_path.datatype, raw) - montage2 = raw.get_montage() - assert montage2.get_positions()['coord_frame'] == 'unknown' - assert_array_almost_equal( - np.array(list(montage.get_positions()['ch_pos'].values())), - np.array(list(montage2.get_positions()['ch_pos'].values())) - ) diff --git a/mne_bids/write.py b/mne_bids/write.py index 6041e78d2..82d6e4792 100644 --- a/mne_bids/write.py +++ b/mne_bids/write.py @@ -1605,6 +1605,19 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, # for MEG, we only write coordinate system if bids_path.datatype == 'meg' and not data_is_emptyroom: + if bids_path.space is None: + sensor_coord_system = orient + elif orient == 'n/a': + sensor_coord_system = bids_path.space + elif orient.lower() != bids_path.space.lower(): + raise ValueError(f'BIDSPath.space {bids_path.space} conflicts' + f'with filetype {ext} which has coordinate ' + f'frame {orient}') + _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system=orient, + sensor_coord_system=sensor_coord_system, + fname=coordsystem_path.fpath, + datatype=bids_path.datatype, + overwrite=overwrite) _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system=orient, sensor_coord_system=orient, fname=coordsystem_path.fpath, From 24e4dd1a315d44247cf457fda094b475be910448 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Mar 2022 12:26:26 -0800 Subject: [PATCH 2/6] allow template coordinate frames for write --- mne_bids/dig.py | 34 ++++++++++++++-------------------- mne_bids/tests/test_dig.py | 25 +++++++++++-------------- mne_bids/tests/test_read.py | 9 +++++---- mne_bids/utils.py | 4 +--- 4 files changed, 31 insertions(+), 41 deletions(-) diff --git a/mne_bids/dig.py b/mne_bids/dig.py index 5801fc5bd..2241e7442 100644 --- a/mne_bids/dig.py +++ b/mne_bids/dig.py @@ -17,7 +17,6 @@ from mne.io.pick import _picks_to_idx from mne_bids.config import (ALLOWED_SPACES, - BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES, BIDS_COORDINATE_UNITS, MNE_TO_BIDS_FRAMES, BIDS_TO_MNE_FRAMES, MNE_FRAME_TO_STR, BIDS_COORD_FRAME_DESCRIPTIONS) @@ -293,19 +292,9 @@ def _write_coordsystem_json(*, raw, unit, hpi_coord_system, .format(coord_frame)) # get the coordinate frame description - try: - sensor_coord_system, sensor_coord_system_mne = sensor_coord_system - except ValueError: - sensor_coord_system_mne = "n/a" sensor_coord_system_descr = (BIDS_COORD_FRAME_DESCRIPTIONS .get(sensor_coord_system.lower(), "n/a")) - if sensor_coord_system == 'Other': - logger.info('Using the `Other` keyword for the CoordinateSystem ' - 'field. Please specify the CoordinateSystemDescription ' - 'field manually.') - sensor_coord_system_descr = (BIDS_COORD_FRAME_DESCRIPTIONS - .get(sensor_coord_system_mne.lower(), - "n/a")) + coords = _extract_landmarks(dig) # create the coordinate json data structure based on 'datatype' if datatype == 'meg': @@ -398,14 +387,17 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, if montage is None: montage = raw.get_montage() else: - # prevent transformation back to "head", only should be used - # in this specific circumstance - if montage.get_positions()['coord_frame'] != 'head': - montage.remove_fiducials() + # transform back to native montage coordinates if given with warnings.catch_warnings(): warnings.filterwarnings(action='ignore', category=RuntimeWarning, - module='mne') + message='.*nasion not found', module='mne') raw.set_montage(montage) + montage_coord_frame = montage.get_positions()['coord_frame'] + if montage_coord_frame != 'head': + for ch in raw.info['chs']: + ch['coord_frame'] = _str_to_frame[montage_coord_frame] + for d in raw.info['dig']: + d['coord_frame'] = _str_to_frame[montage_coord_frame] # get coordinate frame from digMontage digpoint = montage.dig[0] @@ -477,8 +469,7 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, # Now write the data to the elec coords and the coordsystem _channels_fun(raw, channels_path, bids_path.datatype, overwrite) _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system='n/a', - sensor_coord_system=(coord_frame, - mne_coord_frame), + sensor_coord_system=coord_frame, fname=coordsystem_path, datatype=bids_path.datatype, overwrite=overwrite) @@ -549,7 +540,10 @@ def _read_dig_bids(electrodes_fpath, coordsystem_fpath, # XXX: Starting with mne 0.24, this will raise a RuntimeWarning # if channel types are included outside of # (EEG/sEEG/ECoG/DBS/fNIRS). Probably needs a fix in the future. - raw.set_montage(montage, on_missing='warn') + with warnings.catch_warnings(): + warnings.filterwarnings(action='ignore', category=RuntimeWarning, + message='.*nasion not found', module='mne') + raw.set_montage(montage, on_missing='warn') # put back in unknown for unknown coordinate frame if coord_frame == 'unknown': diff --git a/mne_bids/tests/test_dig.py b/mne_bids/tests/test_dig.py index 0d4a1b6ae..124774a48 100644 --- a/mne_bids/tests/test_dig.py +++ b/mne_bids/tests/test_dig.py @@ -18,10 +18,6 @@ from mne_bids.config import (BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES, BIDS_TO_MNE_FRAMES) -import warnings -warnings.filterwarnings('ignore', category=DeprecationWarning) - - base_path = op.join(op.dirname(mne.__file__), 'io') subject_id = '01' session_id = '01' @@ -38,6 +34,7 @@ raw_fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_trunc_raw.fif') raw = mne.io.read_raw(raw_fname) +raw.drop_channels(raw.info['bads']) raw.info['line_freq'] = 60 montage = raw.get_montage() @@ -81,6 +78,8 @@ def test_dig_template(tmp_path): for datatype in ('eeg', 'ieeg'): os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', datatype)) + raw_test = raw.copy().pick_types(eeg=True) + for datatype in ('eeg', 'ieeg'): bids_path = _bids_path.copy().update(root=bids_root, datatype=datatype) for coord_frame in BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES: @@ -93,25 +92,23 @@ def test_dig_template(tmp_path): else: mnt.apply_trans(mne.transforms.Transform( 'head', mne_coord_frame)) - _write_dig_bids(bids_path, raw, mnt, acpc_aligned=True) + _write_dig_bids(bids_path, raw_test, mnt, acpc_aligned=True) electrodes_path = bids_path.copy().update( task=None, run=None, suffix='electrodes', extension='.tsv') coordsystem_path = bids_path.copy().update( task=None, run=None, suffix='coordsystem', extension='.json') - if bids_path.space == 'Pixels': - with pytest.warns(RuntimeWarning, - match='not recognized by MNE'): - _read_dig_bids(electrodes_path, coordsystem_path, - datatype, raw) - elif mne_coord_frame is None: + if mne_coord_frame is None: with pytest.warns(RuntimeWarning, match='not an MNE-Python coordinate frame'): _read_dig_bids(electrodes_path, coordsystem_path, - datatype, raw) + datatype, raw_test) else: + if coord_frame == 'MNI305': # saved to fsaverage, same + electrodes_path.update(space='fsaverage') + coordsystem_path.update(space='fsaverage') _read_dig_bids(electrodes_path, coordsystem_path, - datatype, raw) - mnt2 = raw.get_montage() + datatype, raw_test) + mnt2 = raw_test.get_montage() pos2 = mnt2.get_positions() np.testing.assert_array_almost_equal( np.array(list(pos['ch_pos'].values())), diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py index 25548ca22..c60f5f024 100644 --- a/mne_bids/tests/test_read.py +++ b/mne_bids/tests/test_read.py @@ -693,7 +693,8 @@ def test_handle_eeg_coords_reading(tmp_path): montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="unknown") raw.set_montage(montage) - with pytest.warns(RuntimeWarning, match="Skipping EEG electrodes.tsv"): + with pytest.raises(RuntimeError, match="'head' coordinate frame " + "must contain"): write_raw_bids(raw, bids_path, overwrite=True) bids_path.update(root=tmp_path) @@ -709,12 +710,12 @@ def test_handle_eeg_coords_reading(tmp_path): assert electrodes_fname is None # create montage in head frame and set should result in - # warning if landmarks not set + # an error if landmarks are not set montage = mne.channels.make_dig_montage(ch_pos=ch_pos, coord_frame="head") raw.set_montage(montage) - with pytest.warns(RuntimeWarning, match='Setting montage not possible ' - 'if anatomical landmarks'): + with pytest.raises(RuntimeError, match="'head' coordinate frame " + "must contain"): write_raw_bids(raw, bids_path, overwrite=True) montage = mne.channels.make_dig_montage(ch_pos=ch_pos, diff --git a/mne_bids/utils.py b/mne_bids/utils.py index 9edf43716..ba9812727 100644 --- a/mne_bids/utils.py +++ b/mne_bids/utils.py @@ -298,9 +298,7 @@ def _extract_landmarks(dig): coords['RPA'] = landmarks[FIFF.FIFFV_POINT_RPA]['r'].tolist() coord_frame['RPA'] = landmarks[FIFF.FIFFV_POINT_RPA]['coord_frame'] - # for now, we only support "head" coordinates - for frame in coord_frame.values(): - assert frame == FIFF.FIFFV_COORD_HEAD + assert len(set(coord_frame.values())) == 1 # must all be the same return coords From d1203fcb6c58db74c25f2f21a0dd2325cb683c24 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Mar 2022 12:28:49 -0800 Subject: [PATCH 3/6] fix flake --- mne_bids/tests/test_write.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index 8f4c4fb3f..6474c2434 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -42,7 +42,6 @@ get_anat_landmarks, write, anonymize_dataset, get_entity_vals) from mne_bids.write import _get_fid_coords -from mne_bids.dig import _write_dig_bids, _read_dig_bids from mne_bids.utils import (_stamp_to_dt, _get_anonymization_daysback, get_anonymization_daysback, _write_json) from mne_bids.tsv_handler import _from_tsv, _to_tsv From 36b0be9d6362f27ff6f387b70100d8659a2f412e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Mar 2022 13:18:26 -0800 Subject: [PATCH 4/6] fix tests --- mne_bids/dig.py | 2 +- mne_bids/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mne_bids/dig.py b/mne_bids/dig.py index 2241e7442..3d5da5806 100644 --- a/mne_bids/dig.py +++ b/mne_bids/dig.py @@ -455,7 +455,7 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, 'subject': bids_path.subject, 'session': bids_path.session, 'acquisition': bids_path.acquisition, - 'space': coord_frame + 'space': None if bids_path.datatype == 'nirs' else coord_frame } channels_suffix = \ 'optodes' if bids_path.datatype == 'nirs' else 'electrodes' diff --git a/mne_bids/utils.py b/mne_bids/utils.py index ba9812727..43320e8e1 100644 --- a/mne_bids/utils.py +++ b/mne_bids/utils.py @@ -298,7 +298,7 @@ def _extract_landmarks(dig): coords['RPA'] = landmarks[FIFF.FIFFV_POINT_RPA]['r'].tolist() coord_frame['RPA'] = landmarks[FIFF.FIFFV_POINT_RPA]['coord_frame'] - assert len(set(coord_frame.values())) == 1 # must all be the same + assert len(set(coord_frame.values())) < 2 # must all be the same return coords From d173e750050f20b2c71ea8bbc13cf77708dcde33 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Mar 2022 15:20:57 -0800 Subject: [PATCH 5/6] richard review, fix codecov --- doc/whats_new.rst | 2 +- examples/convert_ieeg_to_bids.py | 17 +++++++++++++---- mne_bids/config.py | 26 +++++++++++++++----------- mne_bids/dig.py | 11 ++--------- mne_bids/tests/test_dig.py | 15 +++++++++++++-- mne_bids/tests/test_write.py | 4 ++-- mne_bids/write.py | 11 +++++++---- 7 files changed, 53 insertions(+), 33 deletions(-) diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 76217e686..c7abe77b1 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -67,7 +67,7 @@ API and behavior changes - Passing ``fs_subject=None`` to :func:`get_head_mri_trans` has been deprecated. Please pass the FreeSurfer subject name explicitly, by Richard Höchenberger`_ (:gh:`977`) -- Corrupted or missing fiducials in 'head' coordinates now raise an error instead of warning in :func:`mne_bids.write_raw_bids` by `Alex Rockhill`_ (:gh:`980`) +- Corrupted or missing fiducials in ``head`` coordinates now raise an error instead of warning in :func:`mne_bids.write_raw_bids` by `Alex Rockhill`_ (:gh:`980`) Requirements ^^^^^^^^^^^^ diff --git a/examples/convert_ieeg_to_bids.py b/examples/convert_ieeg_to_bids.py index a8983c9b4..a52ad3623 100644 --- a/examples/convert_ieeg_to_bids.py +++ b/examples/convert_ieeg_to_bids.py @@ -339,7 +339,7 @@ # %% # Step 6: Store coordinates in another template space accepted by BIDS # -------------------------------------------------------------------- -# As of thid writing, BIDS accepts channel coordinates in reference to the +# As of this writing, BIDS accepts channel coordinates in reference to the # the following template spaces: ``ICBM452AirSpace``, ``ICBM452Warp5Space``, # ``IXI549Space``, ``fsaverage``, ``fsaverageSym``, ``fsLR``, ``MNIColin27``, # ``MNI152Lin``, ``MNI152NLin2009[a-c][Sym|Asym]``, ``MNI152NLin6Sym``, @@ -429,7 +429,10 @@ # # identity transform since 'unknown' is already 'mri' == surface RAS # # for the template brain MRI # trans = mne.transforms.Transform( -# fro='unknown', to='mri', trans=np.eye(4)) +# fro='unknown', +# to='mri', +# trans=np.eye(4) +# ) # brain = mne.viz.Brain('sample_seeg', subjects_dir=subjects_dir, # **brain_kwargs) # brain.add_sensors(raw.info, trans=trans) @@ -443,7 +446,10 @@ # template_T1 = nib.load(op.join(subjects_dir, 'MNI152NLin2009bAsym', # 'mri', 'T1.mgz')) # trans = mne.transforms.Transform( # use vox to surface RAS transform -# fro='unknown', to='mri', trans=template_T1.header.get_vox2ras_tkr()) +# fro='unknown', +# to='mri', +# trans=template_T1.header.get_vox2ras_tkr() +# ) # brain = mne.viz.Brain( # 'sample_seeg', subjects_dir=subjects_dir, **brain_kwargs) # brain.add_sensors(raw.info, trans=trans) @@ -461,7 +467,10 @@ # vox_mri_t = template_T1.header.get_vox2ras_tkr() # ras_mri_t = np.dot(ras_vox_t, vox_mri_t) # ras->vox with vox->mri # trans = mne.transforms.Transform( -# fro='unknown', to='mri', trans=ras_mri_t) +# fro='unknown', +# to='mri', +# trans=ras_mri_t +# ) # brain = mne.viz.Brain( # 'sample_seeg', subjects_dir=subjects_dir, **brain_kwargs) # brain.add_sensors(raw.info, trans=trans) diff --git a/mne_bids/config.py b/mne_bids/config.py index bd581ec02..a50cfd9ea 100644 --- a/mne_bids/config.py +++ b/mne_bids/config.py @@ -109,10 +109,12 @@ ] # allowed extensions (data formats) in BIDS spec -ALLOWED_DATATYPE_EXTENSIONS = {'meg': allowed_extensions_meg, - 'eeg': allowed_extensions_eeg, - 'ieeg': allowed_extensions_ieeg, - 'nirs': allowed_extensions_nirs} +ALLOWED_DATATYPE_EXTENSIONS = { + 'meg': allowed_extensions_meg, + 'eeg': allowed_extensions_eeg, + 'ieeg': allowed_extensions_ieeg, + 'nirs': allowed_extensions_nirs +} # allow additional extensions that are not BIDS # compliant, but we will convert to the @@ -285,7 +287,7 @@ MNE_FRAME_TO_STR = {val: key for key, val in MNE_STR_TO_FRAME.items()} # see BIDS specification for description we copied over from each -BIDS_COORD_FRAME_DESCRIPTIONS = dict(**{ +BIDS_COORD_FRAME_DESCRIPTIONS = { 'acpc': 'The origin of the coordinate system is at the Anterior ' 'Commissure and the negative y-axis is passing through the ' 'Posterior Commissure. The positive z-axis is passing through ' @@ -361,12 +363,14 @@ 'talairach': 'Piecewise linear scaling of the brain is implemented as ' 'described in TT88.', 'uncinfant': 'Infant Brain Atlases from Neonates to 1- and 2-year-olds.' -}, - **{f'mni152nlin2009{letter}{sym}': 'Also known as ICBM ' - '(non-linear coregistration with 40 iterations, ' - 'released in 2009). It comes in either three different flavours ' - 'each in symmetric or asymmetric version.' - for letter in ('a', 'b', 'c') for sym in ('Sym', 'Asym')}) +} + +for letter in ('a', 'b', 'c'): + for sym in ('Sym', 'Asym'): + BIDS_COORD_FRAME_DESCRIPTIONS[f'mni152nlin2009{letter}{sym}'] = \ + 'Also known as ICBM (non-linear coregistration with 40 iterations,' + ' released in 2009). It comes in either three different flavours ' + 'each in symmetric or asymmetric version.' REFERENCES = {'mne-bids': 'Appelhoff, S., Sanderson, M., Brooks, T., Vliet, M., ' diff --git a/mne_bids/dig.py b/mne_bids/dig.py index 3d5da5806..35ff12f56 100644 --- a/mne_bids/dig.py +++ b/mne_bids/dig.py @@ -399,19 +399,12 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, for d in raw.info['dig']: d['coord_frame'] = _str_to_frame[montage_coord_frame] - # get coordinate frame from digMontage - digpoint = montage.dig[0] - if any(digpoint['coord_frame'] != _digpoint['coord_frame'] - for _digpoint in montage.dig): - raise RuntimeError("Not all digpoints have the same coordinate frame.") - # get the accepted mne-python coordinate frames - coord_frame_int = int(digpoint['coord_frame']) + coord_frame_int = int(montage.dig[0]['coord_frame']) mne_coord_frame = MNE_FRAME_TO_STR.get(coord_frame_int, None) coord_frame = MNE_TO_BIDS_FRAMES.get(mne_coord_frame, None) - if coord_frame == 'CapTrak' and \ - bids_path.datatype == 'eeg' or bids_path.datatype == 'nirs': + if coord_frame == 'CapTrak' and bids_path.datatype in ('eeg', 'nirs'): coords = _extract_landmarks(raw.info['dig']) landmarks = set(['RPA', 'NAS', 'LPA']) == set(list(coords.keys())) if not landmarks: diff --git a/mne_bids/tests/test_dig.py b/mne_bids/tests/test_dig.py index 124774a48..b47d75e7c 100644 --- a/mne_bids/tests/test_dig.py +++ b/mne_bids/tests/test_dig.py @@ -13,7 +13,7 @@ import mne from mne.datasets import testing -from mne_bids import BIDSPath, write_raw_bids +from mne_bids import BIDSPath, write_raw_bids, read_raw_bids from mne_bids.dig import _write_dig_bids, _read_dig_bids from mne_bids.config import (BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES, BIDS_TO_MNE_FRAMES) @@ -72,11 +72,12 @@ def test_dig_io(tmp_path): write_raw_bids(raw, bids_path) +@pytest.mark.filterwarnings('ignore:The unit for chann*.:RuntimeWarning:mne') def test_dig_template(tmp_path): """Test that eeg and ieeg dig are stored properly.""" bids_root = tmp_path / 'bids1' for datatype in ('eeg', 'ieeg'): - os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', datatype)) + (bids_root / 'sub-01' / 'ses-01' / datatype).mkdir(parents=True) raw_test = raw.copy().pick_types(eeg=True) @@ -117,3 +118,13 @@ def test_dig_template(tmp_path): assert pos2['coord_frame'] == 'unknown' else: assert pos2['coord_frame'] == mne_coord_frame + + # test MEG + for coord_frame in BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES: + bids_path = _bids_path.copy().update(root=bids_root, datatype='meg', + space=coord_frame) + write_raw_bids(raw, bids_path) + raw_test = read_raw_bids(bids_path) + for ch, ch2 in zip(raw.info['chs'], raw_test.info['chs']): + np.testing.assert_array_equal(ch['loc'], ch2['loc']) + assert ch['coord_frame'] == ch2['coord_frame'] diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index 6474c2434..291d3cd8f 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -1637,8 +1637,8 @@ def test_snirf(_bids_validate, tmp_path): raw = _read_raw_snirf(raw_fname, optode_frame="mri") raw.info['dig'].pop(1) - with pytest.warns(RuntimeWarning, - match='Setting montage not possible'): + with pytest.raises(RuntimeError, + match="'head' coordinate frame must contain nasion"): write_raw_bids(raw, bids_path, overwrite=True) diff --git a/mne_bids/write.py b/mne_bids/write.py index 82d6e4792..a55bf984f 100644 --- a/mne_bids/write.py +++ b/mne_bids/write.py @@ -55,7 +55,8 @@ IGNORED_CHANNELS, ALLOWED_DATATYPE_EXTENSIONS, BIDS_VERSION, REFERENCES, _map_options, reader, ALLOWED_INPUT_EXTENSIONS, CONVERT_FORMATS, - ANONYMIZED_JSON_KEY_WHITELIST) + ANONYMIZED_JSON_KEY_WHITELIST, + BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES) _FIFF_SPLIT_SIZE = '2GB' # MNE-Python default; can be altered during debugging @@ -1609,8 +1610,10 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, sensor_coord_system = orient elif orient == 'n/a': sensor_coord_system = bids_path.space - elif orient.lower() != bids_path.space.lower(): - raise ValueError(f'BIDSPath.space {bids_path.space} conflicts' + elif bids_path.space in BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES: + sensor_coord_system = bids_path.space + elif orient != bids_path.space: + raise ValueError(f'BIDSPath.space {bids_path.space} conflicts ' f'with filetype {ext} which has coordinate ' f'frame {orient}') _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system=orient, @@ -1619,7 +1622,7 @@ def write_raw_bids(raw, bids_path, events_data=None, event_id=None, datatype=bids_path.datatype, overwrite=overwrite) _write_coordsystem_json(raw=raw, unit=unit, hpi_coord_system=orient, - sensor_coord_system=orient, + sensor_coord_system=sensor_coord_system, fname=coordsystem_path.fpath, datatype=bids_path.datatype, overwrite=overwrite) From c29942d52f21cc47a0c9bb496543d2bcf782c68f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Mar 2022 16:00:25 -0800 Subject: [PATCH 6/6] fix raw file modified issue --- mne_bids/tests/test_dig.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mne_bids/tests/test_dig.py b/mne_bids/tests/test_dig.py index b47d75e7c..37d6ea6a2 100644 --- a/mne_bids/tests/test_dig.py +++ b/mne_bids/tests/test_dig.py @@ -42,6 +42,7 @@ def test_dig_io(tmp_path): """Test passing different coordinate frames give proper warnings.""" bids_root = tmp_path / 'bids1' + raw_test = raw.copy() for datatype in ('eeg', 'ieeg'): os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', datatype)) @@ -53,7 +54,7 @@ def test_dig_io(tmp_path): space=None) with pytest.warns(RuntimeWarning, match='Coordinate frame could not be inferred'): - _write_dig_bids(bids_path, raw, mnt, acpc_aligned=True) + _write_dig_bids(bids_path, raw_test, mnt, acpc_aligned=True) # test coordinate frame-BIDSPath.space mismatch mnt = montage.copy() @@ -63,13 +64,13 @@ def test_dig_io(tmp_path): 'or montage are in the CapTrak ' 'coordinate frame but ' 'BIDSPath.space is fsaverage'): - _write_dig_bids(bids_path, raw, mnt) + _write_dig_bids(bids_path, raw_test, mnt) # test MEG space conflict fif (ElektaNeuromag) != CTF bids_path = _bids_path.copy().update( root=bids_root, datatype='meg', space='CTF') with pytest.raises(ValueError, match='conflicts'): - write_raw_bids(raw, bids_path) + write_raw_bids(raw_test, bids_path) @pytest.mark.filterwarnings('ignore:The unit for chann*.:RuntimeWarning:mne') @@ -120,11 +121,12 @@ def test_dig_template(tmp_path): assert pos2['coord_frame'] == mne_coord_frame # test MEG + raw_test = raw.copy() for coord_frame in BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES: bids_path = _bids_path.copy().update(root=bids_root, datatype='meg', space=coord_frame) - write_raw_bids(raw, bids_path) - raw_test = read_raw_bids(bids_path) - for ch, ch2 in zip(raw.info['chs'], raw_test.info['chs']): + write_raw_bids(raw_test, bids_path) + raw_test2 = read_raw_bids(bids_path) + for ch, ch2 in zip(raw.info['chs'], raw_test2.info['chs']): np.testing.assert_array_equal(ch['loc'], ch2['loc']) assert ch['coord_frame'] == ch2['coord_frame']