Repository: mne-tools/mne-bids

Issue:
we should probably restrict the kinds of "labels" that can be passed to entity: `*_space-<label>`
We should probably restrict the kinds of "labels" that can be passed to entity: `*_space-<label>`

Currently, any string can be passed ... this passes the validator, because the validator does not cover this case currently (but may do so in the future).

came up in https://github.com/mne-tools/mne-bids/issues/677#issuecomment-778284872

see bids-validator issue: https://github.com/bids-standard/bids-validator/issues/743
see bids spec clarification PR: https://github.com/bids-standard/bids-specification/pull/734/files

note that although I crossreference to a clarification PR in the BIDS spec, the spec always contained the sentence that the `label` for the `*_space-<label>` should be taken from a list of permissible keywords.


Discussion:
Our CI is now failing because of extended coverage in the validator https://github.com/bids-standard/bids-validator/pull/1190

![image](https://user-images.githubusercontent.com/9084751/109127673-f9fa0a80-774e-11eb-8e91-5d49d17a693c.png)


https://github.com/mne-tools/mne-bids/runs/1977304655?check_suite_focus=true




So … which `space` should be added in the concrete (failing) example?
@adam2392 Can you have a look at this?
I think the issue is that now anything with `coordframe == 'mri'` in mne-python -> "Other" and one must pass  `*CoordinateSystemDescription` and should pass `IntendedFor`.

@sappelhoff  can you help clarify what the below should be?

For iEEG mne-python -> BIDS:

`fs_tal` -> ? no idea
`mni_tal` -> `fsaverage`?
`mri_voxel` -> error, needs to convert to mm
`ras` -> ? no idea
` head` -> EEG Captrak (or error if it's ieeg)
> and one must pass `coordinatesystemdescription`

HELP, is this supposed to be a keyword argument?? It makes my eyes bleed trying to read this monstrosity 😱 Can we come up with something **considerably** more concise? :)
Yes, `"Other"` if the Coordinate System is not "known" in BIDS.

Of course, if it is a well known coordinate system, then it can be added to BIDS without a log of hassle.

However, there is a huge amount of coordinate systems supported, see: https://bids-specification.readthedocs.io/en/latest/99-appendices/08-coordinate-systems.html#standard-template-identifiers

And I suspect that for some of these, the issue just boils down to not knowing the mapping.

> fs_tal -> ? no idea

I don't know what that is either ... so probably "Other", and put some meaningful thing in `"*CoordsystemDescription"`

>  mni_tal -> fsaverage?

that'd be great if you could find out ... because `fsaverage` would be supported.

>  mri_voxel -> error, needs to convert to mm

and if we convert to mm (which should be straight forward), then what would the coord system be from the BIDS list?

>  ras -> ? no idea

`ras` is no coordinate system, it just tells you about the direction of the axes, but nothing about the origin of the coordinate system (and nothing about the units, which is a bit less important, but still).

>  head -> EEG Captrak (or error if it's ieeg)

We could make `CapTrak` permissible for iEEG as well, ... *IF* any ieeg people actually use this kind of coordinate system.

> HELP, is this supposed to be a keyword argument?? It makes my eyes bleed trying to read this monstrosity scream Can we come up with something considerably more concise? :)

it's actually `EEGCoordinateSystemDescription`, and may I introduce you to my friend `DigitizedHeadPointsCoordinateSystemDescription` <3 (not a joke :cry: )
> it's actually `EEGCoordinateSystemDescription`, and may I introduce you to my friend `DigitizedHeadPointsCoordinateSystemDescription` <3 (not a joke 😢 )

At least this is CamelCased SoItsNotAllThatBadIGuessButInTheEndItsReallyJustAMatterOfTasteRight?

But seriously, if we do decide to use these names, we should at least turn them into snake_case and not simply lowercase them :)
@hoechenberger apologies. Typing this out on GitHub mobile app heh :p 
crossref to mne discoure group: https://mne.discourse.group/t/quick-clarification-on-mne-coordinate-systems-mni-tal-vs-fs-tal/2752

Resolving patch:
diff --git a/mne_bids/config.py b/mne_bids/config.py
--- a/mne_bids/config.py
+++ b/mne_bids/config.py
@@ -144,6 +144,63 @@
                                'space': 'space', 'rec': 'recording',
                                'split': 'split', 'suffix': 'suffix'}
 
+coordsys_standard_template = [
+    'ICBM452AirSpace',
+    'ICBM452Warp5Space',
+    'IXI549Space',
+    'fsaverage',
+    'fsaverageSym',
+    'fsLR',
+    'MNIColin27',
+    'MNI152Lin',
+    'MNI152NLin2009aSym',
+    'MNI152NLin2009bSym',
+    'MNI152NLin2009cSym',
+    'MNI152NLin2009aAsym',
+    'MNI152NLin2009bAsym',
+    'MNI152NLin2009cAsym',
+    'MNI152NLin6Sym',
+    'MNI152NLin6ASym',
+    'MNI305',
+    'NIHPD',
+    'OASIS30AntsOASISAnts',
+    'OASIS30Atropos',
+    'Talairach',
+    'UNCInfant',
+]
+
+coordsys_standard_template_deprecated = [
+    'fsaverage3',
+    'fsaverage4',
+    'fsaverage5',
+    'fsaverage6',
+    'fsaveragesym',
+    'UNCInfant0V21',
+    'UNCInfant1V21',
+    'UNCInfant2V21',
+    'UNCInfant0V22',
+    'UNCInfant1V22',
+    'UNCInfant2V22',
+    'UNCInfant0V23',
+    'UNCInfant1V23',
+    'UNCInfant2V23',
+]
+
+coordsys_meg = ['CTF', 'ElektaNeuromag', '4DBti', 'KitYokogawa', 'ChietiItab']
+coordsys_eeg = ['CapTrak']
+coordsys_ieeg = ['Pixels', 'ACPC']
+coordsys_wildcard = ['Other']
+coordsys_shared = (coordsys_standard_template +
+                   coordsys_standard_template_deprecated +
+                   coordsys_wildcard)
+
+ALLOWED_SPACES = dict()
+ALLOWED_SPACES['meg'] = coordsys_shared + coordsys_meg + coordsys_eeg
+ALLOWED_SPACES['eeg'] = coordsys_shared + coordsys_meg + coordsys_eeg
+ALLOWED_SPACES['ieeg'] = coordsys_shared + coordsys_ieeg
+ALLOWED_SPACES['anat'] = None
+ALLOWED_SPACES['beh'] = None
+
 # See: https://bids-specification.readthedocs.io/en/latest/99-appendices/04-entity-table.html#encephalography-eeg-ieeg-and-meg  # noqa
 ENTITY_VALUE_TYPE = {
     'subject': 'label',
diff --git a/mne_bids/path.py b/mne_bids/path.py
--- a/mne_bids/path.py
+++ b/mne_bids/path.py
@@ -20,6 +20,7 @@
     ALLOWED_PATH_ENTITIES, ALLOWED_FILENAME_EXTENSIONS,
     ALLOWED_FILENAME_SUFFIX, ALLOWED_PATH_ENTITIES_SHORT,
     ALLOWED_DATATYPES, SUFFIX_TO_DATATYPE, ALLOWED_DATATYPE_EXTENSIONS,
+    ALLOWED_SPACES,
     reader, ENTITY_VALUE_TYPE)
 from mne_bids.utils import (_check_key_val, _check_empty_room_basename,
                             param_regex, _ensure_tuple)
@@ -176,7 +177,11 @@ class BIDSPath(object):
     recording : str | None
         The recording name for this item. Corresponds to "rec".
     space : str | None
-        The coordinate space for an anatomical file. Corresponds to "space".
+        The coordinate space for an anatomical or sensor position
+        files (e.g., ``*_electrodes.tsv``, ``*_markers.mrk``).
+        Corresponds to "space".
+        Note that valid values for ``space`` must come from a list
+        of BIDS keywords as described in the BIDS specification.
     split : int | None
         The split of the continuous recording file for ``.fif`` data.
         Corresponds to "split".
@@ -481,16 +486,19 @@ def update(self, *, check=None, **kwargs):
 
         Also performs error checks on various entities to
         adhere to the BIDS specification. Specifically:
-        - ``suffix`` should be one of: ``anat``, ``eeg``, ``ieeg``, ``meg``
+        - ``datatype`` should be one of: ``anat``, ``eeg``, ``ieeg``, ``meg``
         - ``extension`` should be one of the accepted file
         extensions in the file path: ``.con``, ``.sqd``, ``.fif``,
         ``.pdf``, ``.ds``, ``.vhdr``, ``.edf``, ``.bdf``, ``.set``,
         ``.edf``, ``.set``, ``.mef``, ``.nwb``
-        - ``suffix`` should be one acceptable file suffixes in: ``meg``,
+        - ``suffix`` should be one of the acceptable file suffixes in: ``meg``,
         ``markers``, ``eeg``, ``ieeg``, ``T1w``,
         ``participants``, ``scans``, ``electrodes``, ``channels``,
         ``coordsystem``, ``events``, ``headshape``, ``digitizer``,
         ``beh``, ``physio``, ``stim``
+        - Depending on the modality of the data (EEG, MEG, iEEG),
+        ``space`` should be a valid string according to Appendix VIII
+        in the BIDS specification.
 
         Parameters
         ----------
@@ -559,8 +567,7 @@ def update(self, *, check=None, **kwargs):
         extension = kwargs.get('extension')
         if extension is not None:
             if not extension.startswith('.'):
-                extension = f'.{extension}'
-                kwargs['extension'] = extension
+                kwargs['extension'] = f'.{extension}'
 
         # error check entities
         for key, val in kwargs.items():
@@ -569,7 +576,7 @@ def update(self, *, check=None, **kwargs):
                 _check_key_val(key, val)
 
             # set entity value, ensuring `root` is a Path
-            if key == 'root' and val is not None:
+            if val is not None and key == 'root':
                 val = Path(val)
             setattr(self, key, val)
 
@@ -659,6 +666,26 @@ def _check(self):
                                      f'allowed. Use one of these extensions '
                                      f'{ALLOWED_FILENAME_EXTENSIONS}.')
 
+            # labels from space entity must come from list (appendix VIII)
+            space = self.space
+            if space is not None:
+                datatype = getattr(self, 'datatype', None)
+                if datatype is None:
+                    raise ValueError('You must define datatype if you want to '
+                                     'use space in your BIDSPath.')
+
+                allowed_spaces_for_dtype = ALLOWED_SPACES.get(datatype, None)
+                if allowed_spaces_for_dtype is None:
+                    raise ValueError(f'space entity is not valid for datatype '
+                                     f'{self.datatype}')
+                elif space not in allowed_spaces_for_dtype:
+                    raise ValueError(f'space ({space}) is not valid for '
+                                     f'datatype ({self.datatype}).\n'
+                                     f'Should be one of '
+                                     f'{allowed_spaces_for_dtype}')
+                else:
+                    pass
+
             # error check suffix
             suffix = self.suffix
             if suffix is not None and \


Test patch:
diff --git a/mne_bids/tests/test_path.py b/mne_bids/tests/test_path.py
--- a/mne_bids/tests/test_path.py
+++ b/mne_bids/tests/test_path.py
@@ -502,11 +502,34 @@ def test_bids_path(return_bids_test_dir):
     suffix = 'meeg'
     bids_path = BIDSPath(subject=subject_id, session=session_id,
                          suffix=suffix, check=False)
+
     # also inherits error check from instantiation
     # always error check datatype
     with pytest.raises(ValueError, match='datatype .* is not valid'):
         bids_path.copy().update(datatype=error_kind)
 
+    # does not error check on space if check=False ...
+    BIDSPath(subject=subject_id, space='foo', suffix='eeg', check=False)
+
+    # ... but raises an error with check=True
+    match = r'space \(foo\) is not valid for datatype \(eeg\)'
+    with pytest.raises(ValueError, match=match):
+        BIDSPath(subject=subject_id, space='foo', suffix='eeg')
+
+    # error check on space for datatypes that do not support space
+    match = 'space entity is not valid for datatype anat'
+    with pytest.raises(ValueError, match=match):
+        BIDSPath(subject=subject_id, space='foo', datatype='anat')
+
+    # error check on space if datatype is None
+    bids_path_tmpcopy = bids_path.copy().update(suffix='meeg')
+    match = 'You must define datatype if you want to use space'
+    with pytest.raises(ValueError, match=match):
+        bids_path_tmpcopy.update(space='CapTrak', check=True)
+
+    # making a valid space update works
+    bids_path_tmpcopy.update(suffix='eeg', space="CapTrak", check=True)
+
     # suffix won't be error checks if initial check was false
     bids_path.update(suffix=suffix)