Repository: mne-tools/mne-bids

Issue:
setting DigitizedLandmarks/HeadPoints
At the moment both of these values are set as false by default.
Since they are both REQUIRED values I think it would be good if they could be set correctly and automatically.
For KIT data, you can have it so that when you specify something in the `electrode` or `hsp` arguments of `raw_to_bids` you can set the values correctly like so:
```python
    extra_data["DigitizedLandmarks"] = (True if (electrode is not None and emptyroom is not True) else False)
    extra_data["DigitizedHeadPoints"] = (True if (hsp is not None and emptyroom is not True) else False)
```
Where this extra_data dictionary is passed to the requisite function.
Also, see #50  about the empty room variable above.

Both .elp and .hsp files (for KIT data) should also be copied to the working folder (see sec. 8.4.5 of the bids specification)
I currently have working code for KIT data, but this is one thing that will require a bit more work to get working for other formats.


Discussion:
@teonbrooks what's your opinion?
well, this will likely lead to the resurrection of this dead PR (https://github.com/mne-tools/mne-python/pull/1001). I agree that these points should be set automatically if provided with the args. just a matter of getting these files to cooperate.

tl;dr on old pr, could get the device-head transformation within tolerance. 
I was thinking something that would be nice is if mne actually kept track of the files that were used to produce the raw file. Like for example the `raw.filenames` is a list which (at least for KIT data) is simply just a single .con file. If this contained also the path to the other files that were used to generate the raw object (so in the case of KIT data it would have the path to the .mrk, .elp and .hsp files if they are provided), then this info could be read directly from `raw.filenames`, and all the moving of files could also be handled easily as we would have the paths of all the constituent files too.

Obviously I don't know how this works for other data formats however...
oh i see. I guess the motivation there was to have a trace of the original data source. the ancillary head shape and marker points just got absorbed in the FIF file with no looking back. I doubt that the filename list would be modified to accommodate other files but I could be wrong. If you want that feature, that would need to be on the mne-python issue page.
Yeah, that's fair enough. I was thinking of raising it as an issue. It wouldn't matter so much here for mne-bids if you specify the files directly to `raw_to_bids`, however if you are passing a raw object in instead of a path then you also need to specify the elp and hsp files (and .mrk also) explicitly so all that data can be used to construct the bids data correctly.
Obviously the nicest solution would be to be able to just get out all the data from the raw object, and I guess that would have to implemented in some way as some data types only have that data stored in their raw data and not as separate files like the KIT system.
precisely. that is the convenient nature of the FIF file, it maintains all this information in one place. are you ok with closing this issue?
But this issue hasn't really been solved? It would still be good to have some way to get mne-bids to determine the correct values for the two variables.
my bad, I thought you were just trying to propose the addition of the files to raw.filename. so you're saying that `DigitizedLandmarks` and `DigitizedHeadPoints` aren't being set with KIT data?
Those values aren't being set for *any* data:
[See here](https://github.com/mne-tools/mne-bids/blob/master/mne_bids/meg_bids.py#L283-L284)
okay so PR welcome :)

Resolving patch:
diff --git a/mne_bids/dig.py b/mne_bids/dig.py
--- a/mne_bids/dig.py
+++ b/mne_bids/dig.py
@@ -227,11 +227,6 @@ def _write_coordsystem_json(*, raw, unit, hpi_coord_system,
     dig = raw.info['dig']
     if dig is None:
         dig = []
-    coords = _extract_landmarks(dig)
-    hpi = {d['ident']: d for d in dig if d['kind'] == FIFF.FIFFV_POINT_HPI}
-    if hpi:
-        for ident in hpi.keys():
-            coords['coil%d' % ident] = hpi[ident]['r'].tolist()
 
     coord_frame = set([dig[ii]['coord_frame'] for ii in range(len(dig))])
     if len(coord_frame) > 1:  # noqa E501
@@ -255,9 +250,10 @@ def _write_coordsystem_json(*, raw, unit, hpi_coord_system,
         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':
+        landmarks = dict(coords)
         hpi = {d['ident']: d for d in dig if d['kind'] == FIFF.FIFFV_POINT_HPI}
         if hpi:
             for ident in hpi.keys():
@@ -269,7 +265,10 @@ def _write_coordsystem_json(*, raw, unit, hpi_coord_system,
             'MEGCoordinateSystemDescription': sensor_coord_system_descr,
             'HeadCoilCoordinates': coords,
             'HeadCoilCoordinateSystem': hpi_coord_system,
-            'HeadCoilCoordinateUnits': unit  # XXX validate this
+            'HeadCoilCoordinateUnits': unit,  # XXX validate this
+            'AnatomicalLandmarkCoordinates': landmarks,
+            'AnatomicalLandmarkCoordinateSystem': sensor_coord_system,
+            'AnatomicalLandmarkCoordinateUnits': unit
         }
     elif datatype == 'eeg':
         fid_json = {
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -637,6 +637,21 @@ def _sidecar_json(raw, task, manufacturer, fname, datatype, overwrite=False,
     n_stimchan = len([ch for ch in raw.info['chs']
                       if ch['kind'] == FIFF.FIFFV_STIM_CH]) - n_ignored
 
+    # Set DigitizedLandmarks to True if any of LPA, RPA, NAS are found
+    # Set DigitizedHeadPoints to True if any "Extra" points are found
+    # (DigitizedHeadPoints done for Neuromag MEG files only)
+    digitized_head_points = False
+    digitized_landmark = False
+    if datatype == 'meg' and raw.info['dig'] is not None:
+        for dig_point in raw.info['dig']:
+            if dig_point['kind'] in [FIFF.FIFFV_POINT_NASION,
+                                     FIFF.FIFFV_POINT_RPA,
+                                     FIFF.FIFFV_POINT_LPA]:
+                digitized_landmark = True
+            elif dig_point['kind'] == FIFF.FIFFV_POINT_EXTRA and \
+                    raw.filenames[0].endswith('.fif'):
+                digitized_head_points = True
+
     # Define datatype-specific JSON dictionaries
     ch_info_json_common = [
         ('TaskName', task),
@@ -648,8 +663,8 @@ def _sidecar_json(raw, task, manufacturer, fname, datatype, overwrite=False,
         ('RecordingType', rec_type)]
     ch_info_json_meg = [
         ('DewarPosition', 'n/a'),
-        ('DigitizedLandmarks', False),
-        ('DigitizedHeadPoints', False),
+        ('DigitizedLandmarks', digitized_landmark),
+        ('DigitizedHeadPoints', digitized_head_points),
         ('MEGChannelCount', n_megchan),
         ('MEGREFChannelCount', n_megrefchan)]
     ch_info_json_eeg = [


Test patch:
diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py
--- a/mne_bids/tests/test_read.py
+++ b/mne_bids/tests/test_read.py
@@ -21,7 +21,7 @@
                             message="can't resolve package",
                             category=ImportWarning)
     import mne
-
+from mne.io.constants import FIFF
 from mne.utils import requires_nibabel, object_diff
 from mne.utils import assert_dig_allclose
 from mne.datasets import testing, somato
@@ -456,6 +456,30 @@ def test_handle_info_reading(tmpdir):
     with pytest.raises(ValueError, match="PowerLineFrequency .* required"):
         write_raw_bids(raw, bids_path, overwrite=True)
 
+    # check whether there are "Extra points" in raw.info['dig'] if
+    # DigitizedHeadPoints is set to True and not otherwise
+    n_dig_points = 0
+    for dig_point in raw.info['dig']:
+        if dig_point['kind'] == FIFF.FIFFV_POINT_EXTRA:
+            n_dig_points += 1
+    if sidecar_json['DigitizedHeadPoints']:
+        assert n_dig_points > 0
+    else:
+        assert n_dig_points == 0
+
+    # check whether any of NAS/LPA/RPA are present in raw.info['dig']
+    # DigitizedLandmark is set to True, and False otherwise
+    landmark_present = False
+    for dig_point in raw.info['dig']:
+        if dig_point['kind'] in [FIFF.FIFFV_POINT_LPA, FIFF.FIFFV_POINT_RPA,
+                                 FIFF.FIFFV_POINT_NASION]:
+            landmark_present = True
+            break
+    if landmark_present:
+        assert sidecar_json['DigitizedLandmarks'] is True
+    else:
+        assert sidecar_json['DigitizedLandmarks'] is False
+
     # make a copy of the sidecar in "derivatives/"
     # to check that we make sure we always get the right sidecar
     # in addition, it should not break the sidecar reading
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -603,6 +603,40 @@ def test_fif(_bids_validate, tmpdir):
     with pytest.raises(ValueError, match='Unrecognized file format'):
         write_raw_bids(raw, bids_path)
 
+    raw = _read_raw_fif(raw_fname)
+    write_raw_bids(raw, bids_path, events_data=events, event_id=event_id,
+                   overwrite=True)
+
+    # test whether extra points in raw.info['dig'] are correctly used
+    # to set DigitizedHeadShape in the json sidecar
+    meg_json = _find_matching_sidecar(
+        bids_path.copy().update(root=bids_root),
+        suffix='meg', extension='.json')
+    with open(meg_json, 'r') as fin:
+        meg_json_data = json.load(fin)
+        # unchanged sample data includes Extra points
+        assert meg_json_data['DigitizedHeadPoints'] is True
+
+    # drop extra points from raw.info['dig'] and write again
+    raw_no_extra_points = raw.copy()
+    new_dig = []
+    for dig_point in raw_no_extra_points.info['dig']:
+        if dig_point['kind'] != FIFF.FIFFV_POINT_EXTRA:
+            new_dig.append(dig_point)
+    raw_no_extra_points.info['dig'] = new_dig
+
+    write_raw_bids(raw_no_extra_points, bids_path, events_data=events,
+                   event_id=event_id, overwrite=True)
+
+    meg_json = _find_matching_sidecar(
+        bids_path.copy().update(root=bids_root),
+        suffix='meg', extension='.json')
+    with open(meg_json, 'r') as fin:
+        meg_json_data = json.load(fin)
+        # sample data does not have Extra points, so it should
+        # DigitizedHeadPoints should be false
+        assert meg_json_data['DigitizedHeadPoints'] is False
+
 
 @pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
 def test_fif_dtype(_bids_validate, tmpdir):