Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] Add tiny BIDS test dataset, fix doctests, and run it in CI #831

Merged
merged 27 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
60ed51e
fix doctests in path.py
sappelhoff Jul 6, 2021
6f26c14
test: use pytest --doctest-modules
sappelhoff Jul 6, 2021
a2c60c1
Merge branch 'main' into fix/doctests
sappelhoff Jul 6, 2021
0601d21
fix some more examples
sappelhoff Jul 7, 2021
0b56bc3
Merge branch 'main' into fix/doctests
sappelhoff Jul 13, 2021
57d5565
make use of _write_json instead of json.dump
sappelhoff Jul 13, 2021
d3af6dd
fix fine-calib and crosstalk doctest
sappelhoff Jul 13, 2021
9bc1d1e
DATA: add tiny_bids test dataset ~800kb
sappelhoff Jul 13, 2021
396d9bf
fix all doctests using new test ds
sappelhoff Jul 13, 2021
96fda84
add code for generating tiny_bids
sappelhoff Jul 13, 2021
b5555f1
fix example, sphinx warning
sappelhoff Jul 13, 2021
0067d11
use tiny_bids in bidspath example, fix #829
sappelhoff Jul 13, 2021
ea8fe32
write TSV files with newline character
sappelhoff Jul 13, 2021
c605b8f
add missing newlines at end of files
sappelhoff Jul 13, 2021
508047c
do not run pytest on examples
sappelhoff Jul 13, 2021
c0ae22f
add whatsnew for TSV line end
sappelhoff Jul 13, 2021
045c297
properly ignore examples in pytest
sappelhoff Jul 13, 2021
5a235e0
fix head_to_mri upstream API change
sappelhoff Jul 13, 2021
fbe80e6
merge master
sappelhoff Jul 14, 2021
ef17468
xfail doctest on windows due to / vs \
sappelhoff Jul 14, 2021
d1d9dbc
BIDSPath __str__ always .as_posix()
sappelhoff Jul 14, 2021
f86b5e5
fix op.join -> Path
sappelhoff Jul 14, 2021
b6558f4
root in __repr__ --> posix path or None
sappelhoff Jul 14, 2021
dd4588d
circumvent bids_path.root __str__
sappelhoff Jul 14, 2021
459c723
1) fix pep, 2) fix type hints, 3) fix example ds
sappelhoff Jul 14, 2021
6ab7a5e
fix type hint
sappelhoff Jul 14, 2021
d09a1c9
I should run make pep before pushing
sappelhoff Jul 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,14 @@ jobs:
run: |
export BIDS_VALIDATOR_VERSION=`bids-validator --version`
echo Using bids-validator $BIDS_VALIDATOR_VERSION
python -m pytest . --cov=mne_bids mne_bids/tests/ mne_bids/commands/tests/ --cov-report=xml --cov-config=setup.cfg --verbose --ignore mne-python
python -m pytest . \
--doctest-modules \
--cov=mne_bids mne_bids/tests/ mne_bids/commands/tests/ \
--cov-report=xml \
--cov-config=setup.cfg \
--verbose \
--ignore mne-python \
--ignore examples
shell: bash
- name: Upload coverage stats to codecov
if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' && matrix.bids-validator == 'main' }}
Expand Down
11 changes: 9 additions & 2 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@ Authors
* `Alex Rockhill`_
* `Richard Höchenberger`_
* `Adam Li`_
* `Eduard Ort`_
* `Richard Köhler`_ (new contributor)
* `Jean-Rémi King`_ (new contributor)
* `Sin Kim`_ (new contributor)
* `Alexandre Gramfort`_
* `Mainak Jas`_
* `Stefan Appelhoff`_

Detailed list of changes
~~~~~~~~~~~~~~~~~~~~~~~~

Enhancements
^^^^^^^^^^^^

- The fields "DigitizedLandmarks" and "DigitizedHeadPoints" in the json sidecar of Neuromag data are now set to True/False depending on whether any landmarks (NAS, RPA, LPA) or extra points are found in raw.info['dig'], by `Eduard Ort`_ (:gh:`772`)
- Updated the "Read BIDS datasets" example to use data from `OpenNeuro <https://openneuro.org>`_, by `Alex Rockhill`_ (:gh:`753`)
- :func:`mne_bids.get_head_mri_trans` is now more lenient when looking for the fiducial points (LPA, RPA, and nasion) in the MRI JSON sidecar file, and accepts a larger variety of landmark names (upper- and lowercase letters; ``'nasion'`` instead of only ``'NAS'``), by `Richard Höchenberger`_ (:gh:`769`)
Expand All @@ -56,8 +62,9 @@ API and behavior changes
- The ``raw_to_bids`` command has lost its ``--allow_maxshield`` parameter. If writing a FIFF file, we will now always assume that writing data before applying a Maxwell filter is fine, by `Richard Höchenberger`_ (:gh:`787`)
- :meth:`mne_bids.BIDSPath.find_empty_room` now first looks for an ``AssociatedEmptyRoom`` field in the MEG JSON sidecar file to retrieve the empty-room recording; only if this information is missing, it will proceed to try and find the best-matching empty-room recording based on measurement date (i.e., fall back to the previous behavior), by `Richard Höchenberger`_ (:gh:`795`)
- If :func:`mne_bids.read_raw_bids` encounters raw data with the ``STI 014`` stimulus channel and this channel is not explicitly listed in ``*_channels.tsv``, it is now automatically removed upon reading, by `Richard Höchenberger`_ (:gh:`823`)
- :func:`mne_bids.get_anat_landmarks` was added to clarify and simplify the process of generating landmarks that now need to be passed to :func:`mne_bids.write_anat`; this depreciates the arguments ``raw``, ``trans`` and ``t1w`` of :func:`mne_bids.write_anat`, by `Alex Rockhill`_ and `Alexandre Gramfort`_ (:gh:`827`)
- :func:`write_raw_bids` now accepts preloaded raws as input with some caveats if the new parameter ``allow_preload`` is explicitly set to ``True``. This enables some preliminary support for uncommon file formats, generated data, processed derivatives etc., by `Sin Kim`_ (:gh:`819`)
- :func:`mne_bids.get_anat_landmarks` was added to clarify and simplify the process of generating landmarks that now need to be passed to :func:`mne_bids.write_anat`; this deprecates the arguments ``raw``, ``trans`` and ``t1w`` of :func:`mne_bids.write_anat`, by `Alex Rockhill`_ and `Alexandre Gramfort`_ (:gh:`827`)
- :func:`write_raw_bids` now accepts preloaded raws as input with some caveats if the new parameter ``allow_preload`` is explicitly set to ``True``. This enables some preliminary support for items such as uncommon file formats, generated data, and processed derivatives, by `Sin Kim`_ (:gh:`819`)
- MNE-BIDS now writes all TSV data files with a newline character at the end of the file, complying with UNIX/POSIX standards, by `Stefan Appelhoff`_ (:gh:`831`)

Requirements
^^^^^^^^^^^^
Expand Down
39 changes: 29 additions & 10 deletions examples/bidspath.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
# %%
# Obviously, to start exploring BIDSPath, we first need to import it.

from pathlib import Path

import mne_bids
from mne_bids import BIDSPath

# %%
Expand All @@ -27,13 +30,15 @@
# consider where to store your data upon BIDS conversion. Again, the intended
# target folder will be the BIDS root of your data.
#
# Let's just pick an arbitrary BIDS root, for the purpose of this
# demonstration.
# For the purpose of this demonstration, let's pick the ``tiny_bids`` example
# dataset that ships with the MNE-BIDS test suite.

bids_root = './my_bids_root'
# We are using a pathlib.Path object for convenience, but you could just use
# a string to specify ``bids_root`` here.
bids_root = Path(mne_bids.__file__).parent / 'tests' / 'data' / 'tiny_bids'

# %%
# This refers to a folder named `my_bids_root` in the current working
# This refers to a folder named ``my_bids_root`` in the current working
# directory. Finally, let is create a ``BIDSPath``, and tell it about our
# BIDS root. We can then also query the ``BIDSPath`` for its root.

Expand All @@ -45,7 +50,7 @@
# identifiers**. We can either create a new ``BIDSPath``, or update our
# existing one. The value can be retrieved via the ``.subject`` attribute.

subject = '123'
subject = '01'

# Option 1: Create an entirely new BIDSPath.
bids_path_new = BIDSPath(subject=subject, root=bids_root)
Expand All @@ -66,7 +71,7 @@
# information on our experimental session, and try to retrieve it again via
# ``.session``.

session = 'test'
session = 'eeg'
bids_path.update(session=session)
print(bids_path.session)

Expand All @@ -78,7 +83,7 @@
# using `mne_bids.write_raw_bids`. For the sake of this example, however, we
# are going to specify the data type explicitly.

datatype = 'meg'
datatype = 'eeg'
bids_path.update(datatype=datatype)
print(bids_path.datatype)

Expand Down Expand Up @@ -110,7 +115,7 @@
# %%
# The two entities you can see here are the ``subject`` entity (``sub``) and
# the ``session`` entity (``ses``). Each entity name also has a value; for
# ``sub``, this is ``123``, and for ``ses``, it is ``test`` in our example.
# ``sub``, this is ``01``, and for ``ses``, it is ``eeg`` in our example.
# Entity names (or "keys") and values are separated via hyphens.
# BIDS knows a much larger number of entities, and MNE-BIDS allows you to make
# use of them. To get a list of all supported entities, use:
Expand All @@ -129,11 +134,25 @@

# %%
# As you can see, the ``basename`` has been updated. In fact, the entire
# **path** has been updated, and the ``ses-test`` folder has been dropped from
# **path** has been updated, and the ``ses-eeg`` folder has been dropped from
# the path:

print(bids_path.fpath)

# %%
# Oups! The cell above produced a ``RuntimeWarning`` that our data file could
# not be found. That's because we changed the ``run`` and ``session`` entities
# above, and the ``tiny_bids`` dataset does not contain corresponding data.
#
# That shows us that ``BIDSPath`` is doing a lot of guess-work and checking
# in the background, but note that this may change in the future.
#
# For now, let's revert to the last working iteration of our ``bids_path``
# instance.

bids_path.update(run=None, session='eeg')
print(bids_path.fpath)

# %%
# Awesome! We're almost done! Two important things are still missing, though:
# the so-called **suffix** and the filename **extension**. Sometimes these
Expand All @@ -151,7 +170,7 @@
# ``.tsv``.
# Let's put our new knowledge to use!

bids_path.update(suffix='meg', extension='fif')
bids_path.update(suffix='eeg', extension='.vhdr')
print(bids_path.fpath)
bids_path

Expand Down
14 changes: 12 additions & 2 deletions mne_bids/inspect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""Inspect and annotate BIDS raw data."""
# Authors: Richard Höchenberger <[email protected]>
# Stefan Appelhoff <[email protected]>
#
# License: BSD (3-clause)

from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -78,8 +84,12 @@ def inspect_dataset(bids_path, find_flat=True, l_freq=None, h_freq=None,
Disable flat channel & segment detection, and apply a filter with a
passband of 1–30 Hz.

>>> inspect_dataset(bids_path=bids_path, find_flat=False,
l_freq=1, h_freq=30)
>>> from mne_bids import BIDSPath
>>> root = Path('./mne_bids/tests/data/tiny_bids').absolute()
>>> bids_path = BIDSPath(subject='01', task='rest', session='eeg',
... suffix='eeg', extension='.vhdr', root=root)
>>> inspect_dataset(bids_path=bids_path, find_flat=False, # doctest: +SKIP
... l_freq=1, h_freq=30)
"""
allowed_extensions = set(ALLOWED_DATATYPE_EXTENSIONS['meg'] +
ALLOWED_DATATYPE_EXTENSIONS['eeg'] +
Expand Down
92 changes: 54 additions & 38 deletions mne_bids/path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""BIDS compatible path functionality."""
# Authors: Adam Li <[email protected]>
# Stefan Appelhoff <[email protected]>
#
# License: BSD (3-clause)
import glob
Expand All @@ -13,7 +14,7 @@
from pathlib import Path
from datetime import datetime
import json
from typing import Optional, Union
from typing import Optional

import numpy as np
from mne.utils import warn, logger, _validate_type
Expand Down Expand Up @@ -224,37 +225,50 @@ class BIDSPath(object):

Examples
--------
Generate a BIDSPath object and inspect it

>>> bids_path = BIDSPath(subject='test', session='two', task='mytask',
suffix='ieeg', extension='.edf')
... suffix='ieeg', extension='.edf')
>>> print(bids_path.basename)
sub-test_ses-two_task-mytask_ieeg.edf
>>> bids_path
BIDSPath(root: None,
BIDSPath(
root: None
datatype: ieeg
basename: sub-test_ses-two_task-mytask_ieeg.edf)
>>> # copy and update multiple entities at once

Copy and update multiple entities at once

>>> new_bids_path = bids_path.copy().update(subject='test2',
session='one')
... session='one')
>>> print(new_bids_path.basename)
sub-test2_ses-one_task-mytask_ieeg.edf
>>> # printing the BIDSPath will show relative path when
>>> # root is not set

Printing a BIDSPath will show a relative path when `root` is not set

>>> print(new_bids_path)
sub-test2/ses-one/ieeg/sub-test2_ses-one_task-mytask_ieeg.edf
>>> new_bids_path.update(suffix='channels', extension='.tsv')
>>> # setting suffix without an identifiable datatype will
>>> # result in a wildcard at the datatype directory level

Setting `suffix` without an identifiable datatype will make
BIDSPath try to guess the datatype

>>> new_bids_path = new_bids_path.update(suffix='channels',
... extension='.tsv')
>>> print(new_bids_path)
sub-test2/ses-one/*/sub-test2_ses-one_task-mytask_channels.tsv
>>> # set a root for the BIDS dataset
>>> new_bids_path.update(root='/bids_dataset')
>>> print(new_bids_path.root)
sub-test2/ses-one/ieeg/sub-test2_ses-one_task-mytask_channels.tsv

You can set a new root for the BIDS dataset. Let's see what the
different properties look like for our object:

>>> new_bids_path = new_bids_path.update(root='/bids_dataset')
>>> print(new_bids_path.root.as_posix())
/bids_dataset
>>> print(new_bids_path.basename)
sub-test2_ses-one_task-mytask_ieeg.edf
sub-test2_ses-one_task-mytask_channels.tsv
>>> print(new_bids_path)
/bids_dataset/sub-test2/ses-one/ieeg/sub-test2_ses-one_task-mytask_ieeg.edf
>>> print(new_bids_path.directory)
/bids_dataset/sub-test2/ses-one/ieeg/
/bids_dataset/sub-test2/ses-one/ieeg/sub-test2_ses-one_task-mytask_channels.tsv
>>> print(new_bids_path.directory.as_posix())
/bids_dataset/sub-test2/ses-one/ieeg

Notes
-----
Expand Down Expand Up @@ -440,7 +454,7 @@ def suffix(self, value):
self.update(suffix=value)

@property
def root(self) -> Optional[Union[str, Path]]:
def root(self) -> Optional[Path]:
"""The root directory of the BIDS dataset."""
return self._root

Expand Down Expand Up @@ -477,12 +491,14 @@ def extension(self, value):

def __str__(self):
"""Return the string representation of the path."""
return str(self.fpath)
return str(self.fpath.as_posix())

def __repr__(self):
"""Representation in the style of `pathlib.Path`."""
root = self.root.as_posix() if self.root is not None else None

return f'{self.__class__.__name__}(\n' \
f'root: {self.root}\n' \
f'root: {root}\n' \
f'datatype: {self.datatype}\n' \
f'basename: {self.basename})'

Expand Down Expand Up @@ -651,13 +667,13 @@ def update(self, *, check=None, **kwargs):
:func:`mne_bids.BIDSPath`:

>>> bids_path = BIDSPath(subject='test', session='two',
task='mytask', suffix='channels',
extension='.tsv')
... task='mytask', suffix='channels',
... extension='.tsv')
>>> print(bids_path.basename)
sub-test_ses-two_task-mytask_channels.tsv
>>> # Then, one can update this `BIDSPath` object in place
>>> bids_path.update(acquisition='test', suffix='ieeg',
extension='.vhdr', task=None)
>>> bids_path = bids_path.update(acquisition='test', suffix='ieeg',
... extension='.vhdr', task=None)
>>> print(bids_path.basename)
sub-test_ses-two_acq-test_ieeg.vhdr
"""
Expand Down Expand Up @@ -1147,16 +1163,16 @@ def get_entities_from_fname(fname, on_error='raise'):
--------
>>> fname = 'sub-01_ses-exp_run-02_meg.fif'
>>> get_entities_from_fname(fname)
{'subject': '01',
'session': 'exp',
'task': None,
'acquisition': None,
'run': '02',
'processing': None,
'space': None,
'recording': None,
'split': None,
'suffix': 'meg'}
{'subject': '01', \
'session': 'exp', \
'task': None, \
'acquisition': None, \
'run': '02', \
'processing': None, \
'space': None, \
'recording': None, \
'split': None, \
'suffix': 'meg'}
"""
if on_error not in ('warn', 'raise', 'ignore'):
raise ValueError(f'Acceptable values for on_error are: warn, raise, '
Expand Down Expand Up @@ -1398,12 +1414,12 @@ def get_entity_vals(root, entity_key, *, ignore_subjects='emptyroom',

Examples
--------
>>> root = os.path.expanduser('~/mne_data/eeg_matchingpennies')
>>> root = Path('./mne_bids/tests/data/tiny_bids').absolute()
>>> entity_key = 'subject'
>>> get_entity_vals(root, entity_key)
['05', '06', '07', '08', '09', '10', '11']
['01']
>>> get_entity_vals(root, entity_key, with_key=True)
['sub-05', 'sub-06', 'sub-07', 'sub-08', 'sub-09', 'sub-10', 'sub-11']
['sub-01']

Notes
-----
Expand Down
Loading