Repository: mne-tools/mne-python

Issue:
read_raw_eeglab does not read nasion, lpa and rpa values from .set file
#### Describe the bug

`read_raw_eeglab` does not read properly the nasion, lpa and rpa coordinates of a `.set` file.

#### Steps to reproduce

This can be reproduced on the sample data `eeglab_data.set` file that comes with EEGLAB.

```
>>> import mne
>>> import os
>>> from mne_bids import write_raw_bids, BIDSPath
>>> import eeglabio
>>> file = "/Users/cmadjar/Documents/MATLAB/eeglab2022.0/sample_data/eeglab_data.set"
>>> raw = mne.io.read_raw_eeglab(input_fname=file, preload=False, verbose=True)
Reading /Users/cmadjar/Documents/MATLAB/eeglab2022.0/sample_data/eeglab_data.fdt
>>> 
>>> raw.get_montage()
<DigMontage | 0 extras (headshape), 0 HPIs, 0 fiducials, 32 channels>

>>> raw.get_montage().get_positions()
.... 'coord_frame': 'head', 'nasion': None, 'lpa': None, 'rpa': None, 'hsp': None, 'hpi': None
```

![Screen Shot 2022-03-30 at 1 11 21 PM](https://user-images.githubusercontent.com/1402456/160892869-430f3b8c-77d3-4f84-9c97-751ce3796024.png)

However, when looking in the Matlab structure, the coordinates for nasion, lpa and rpa are indeed present in the file. So for some reason, this gets lost when loading the `.set` file with `read_raw_eeglab`


#### Additional information

Reproduced on several versions of mne and different OS.

Latest information:
```
>>> mne.sys_info()
Platform:         macOS-11.3-x86_64-i386-64bit
Python:           3.9.12 (main, Mar 26 2022, 15:52:10)  [Clang 13.0.0 (clang-1300.0.29.30)]
Executable:       /Users/cmadjar/Development/python_venv/python_intel_3.9.4/eeg2bids_py3.9.12/bin/python
CPU:              i386: 8 cores
Memory:           Unavailable (requires "psutil" package)
mne:              1.0.0
numpy:            1.21.5 {blas=openblas, lapack=openblas}
scipy:            1.8.0
matplotlib:       3.5.1 {backend=MacOSX}

sklearn:          1.0.2
numba:            0.55.1
nibabel:          Not found
nilearn:          Not found
dipy:             Not found
cupy:             Not found
pandas:           1.4.1
pyvista:          Not found
pyvistaqt:        Not found
ipyvtklink:       Not found
vtk:              Not found
PyQt5:            Not found
ipympl:           Not found
pooch:            v1.6.0

mne_bids:         0.10
mne_nirs:         Not found
mne_features:     0.2
mne_qt_browser:   Not found
mne_connectivity: Not found
```


Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴🏽‍♂️

can you share a file to replicate?

thanks

> Message ID: ***@***.***>
>

Sure. Here is the EEGLAB sample data. 
[eeglab_data.zip](https://github.com/mne-tools/mne-python/files/8389773/eeglab_data.zip)

Note: when we add back the landmarks in the MNE raw object and we save the file as a .set file, the urchanlocs is not being saved either and therefore, we lose the landmark locations when saving the file too.
@cmadjar can you point me where to read the fiducial locations in the EEG structure:

loading the file in matlab I see
```
>> EEG

EEG =

             setname: 'Continuous EEG Data'
            filename: 'eeglab_data.set'
            filepath: '/Users/alex/Downloads/eeglab_data/'
             subject: ''
               group: ''
           condition: ''
             session: []
            comments: [6x769 char]
              nbchan: 32
              trials: 1
                pnts: 30504
               srate: 128
                xmin: 0
                xmax: 238.3047
               times: [1x30504 double]
                data: [32x30504 single]
              icaact: []
             icawinv: []
           icasphere: []
          icaweights: []
         icachansind: []
            chanlocs: [1x32 struct]
          urchanlocs: []
            chaninfo: [1x1 struct]
                 ref: 'common'
               event: [1x154 struct]
             urevent: [1x154 struct]
    eventdescription: {[2x29 char]  [2x63 char]  [2x36 char]  ''}
               epoch: []
    epochdescription: {}
              reject: [1x1 struct]
               stats: [1x1 struct]
            specdata: []
          specicaact: []
          splinefile: []
       icasplinefile: ''
              dipfit: []
             history: '
EEG = pop_loadset( 'filename', 'eeglab_data.set', 'filepath', '/data/common/matlab/eeglab/sample_data/');
EEG = eeg_checkset( EEG );
EEG = pop_loadset('filename','eeglab_data.set','filepath','...'
               saved: 'yes'
                 etc: [1x1 struct]
             datfile: 'eeglab_data.fdt'
                 run: []

>> length(EEG.chanlocs)

ans =

    32
```
@alexandrebarachant Apologies, looks like the eeglab_data.set do not have data in the `urchanlocs` structure... In the sample data that I have, the nasion, lpa and rpa information are stored in the `urchanlocs` structure. See below for the content.

![Screen Shot 2022-04-01 at 3 44 18 PM](https://user-images.githubusercontent.com/1402456/161331270-f10150ff-936f-4a20-9805-9ce6d9bf97aa.png)

I don't think I can share that dataset unfortunately. I will ask the owner if it is possible to share it with you, just in case.

it's @agramfort not @alexandrebarachant :)

I need such a file to debug
@agramfort I can share the file with you. What is the best way to send it to you? Probably best not to share it openly on GitHub just in case.

Thanks!
you can send me a download link to this email ***@***.***

> Message ID: ***@***.***>
>

@agramfort I cannot see the email unfortunately
see contact section in http://alexandre.gramfort.net

> Message ID: ***@***.***>
>

diagnostic : we read only chanlocs and not urchanlocs.

@cmadjar has a file where urchanlocs are chanlocs concatenated with 3 fiducials (it's therefore partially the same information).

what shall we do? Give priorirty to urchanlocs if present? I don't know where to find the information. 

cc @cbrnr @mmagnuski @arnodelorme
The Fiducials are in EEG.chaninfo.nodatchans
You may use eeg_checkchanlocs(EEG) to add them to the list of other channels.
thx @arnodelorme 

may I ask 2 side questions:
- what is the difference between chanlocs and urchanlocs? what does "ur" refer to?
- I do have things in EEG.chaninfo.nodatchans but the labels are E130, E131, E132. Can we assume a specific order in this list? It's always 3 values and it's Nasion, LPA and RPA?

do not hesitate to point me to the EEGLAB doc. 🙏 
Hello @agramfort, 
1) I am not sure what "ur" stands for, but here is the info about the channels from [here](https://github.com/sccn/nemar_tool_proof_of_concept/blob/master/eeg_checkset.m): 
%   EEG.chanlocs     - structure array containing names and locations of the channels on the scalp
%   EEG.urchanlocs   - original (ur) dataset chanlocs structure containing all channels originally collected with these data (before channel rejection)

2) I am not sure you can make that generalization on the order. I think it will be the same for all MFF files (at least of this version of NetStation), but I am not sure for other systems. Could you use the description from EEG.chaninfo.nodatchans.description to confirm? 

Thanks!  


Resolving patch:
diff --git a/mne/datasets/config.py b/mne/datasets/config.py
--- a/mne/datasets/config.py
+++ b/mne/datasets/config.py
@@ -87,7 +87,7 @@
 # respective repos, and make a new release of the dataset on GitHub. Then
 # update the checksum in the MNE_DATASETS dict below, and change version
 # here:                  ↓↓↓↓↓         ↓↓↓
-RELEASES = dict(testing='0.132', misc='0.23')
+RELEASES = dict(testing='0.133', misc='0.23')
 TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}'
 MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}'
 
@@ -111,7 +111,7 @@
 # Testing and misc are at the top as they're updated most often
 MNE_DATASETS['testing'] = dict(
     archive_name=f'{TESTING_VERSIONED}.tar.gz',  # 'mne-testing-data',
-    hash='md5:2ff8bcd18053af3ee0587dce9d6ab516',
+    hash='md5:32173080843e20558f664df1531ae4c6',
     url=('https://codeload.github.com/mne-tools/mne-testing-data/'
          f'tar.gz/{RELEASES["testing"]}'),
     folder_name='MNE-testing-data',
diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py
--- a/mne/io/eeglab/eeglab.py
+++ b/mne/io/eeglab/eeglab.py
@@ -144,10 +144,25 @@ def _get_montage_information(eeg, get_pos):
              '\n'.join([f'{key}: {sorted(unknown_types[key])}'
                         for key in sorted(unknown_types)]))
 
+    lpa, rpa, nasion = None, None, None
+    if (hasattr(eeg, "chaninfo") and
+            "nodatchans" in eeg.chaninfo and
+            len(eeg.chaninfo['nodatchans'])):
+        for item in list(zip(*eeg.chaninfo['nodatchans'].values())):
+            d = dict(zip(eeg.chaninfo['nodatchans'].keys(), item))
+            if d["type"] != 'FID':
+                continue
+            elif d['description'] == 'Nasion':
+                nasion = np.array([d["X"], d["Y"], d["Z"]])
+            elif d['description'] == 'Right periauricular point':
+                rpa = np.array([d["X"], d["Y"], d["Z"]])
+            elif d['description'] == 'Left periauricular point':
+                lpa = np.array([d["X"], d["Y"], d["Z"]])
+
     if pos_ch_names:
         montage = make_dig_montage(
             ch_pos=dict(zip(ch_names, np.array(pos))),
-            coord_frame='head')
+            coord_frame='head', lpa=lpa, rpa=rpa, nasion=nasion)
     else:
         montage = None
 


Test patch:
diff --git a/mne/io/eeglab/tests/test_eeglab.py b/mne/io/eeglab/tests/test_eeglab.py
--- a/mne/io/eeglab/tests/test_eeglab.py
+++ b/mne/io/eeglab/tests/test_eeglab.py
@@ -33,6 +33,7 @@
 raw_mat_fnames = [raw_fname_mat, raw_fname_onefile_mat]
 epochs_mat_fnames = [epochs_fname_mat, epochs_fname_onefile_mat]
 raw_fname_chanloc = op.join(base_dir, 'test_raw_chanloc.set')
+raw_fname_chanloc_fids = op.join(base_dir, 'test_raw_chanloc_fids.set')
 raw_fname_2021 = op.join(base_dir, 'test_raw_2021.set')
 raw_fname_h5 = op.join(base_dir, 'test_raw_h5.set')
 raw_fname_onefile_h5 = op.join(base_dir, 'test_raw_onefile_h5.set')
@@ -454,3 +455,17 @@ def test_get_montage_info_with_ch_type():
     with pytest.warns(RuntimeWarning, match='Unknown types found'):
         ch_names, ch_types, montage = \
             _get_montage_information(mat['EEG'], False)
+
+
+@testing.requires_testing_data
+def test_fidsposition_information():
+    """Test reading file with 3 fiducial locations."""
+    raw = read_raw_eeglab(raw_fname_chanloc_fids)
+    montage = raw.get_montage()
+    pos = montage.get_positions()
+    assert pos['nasion'] is not None
+    assert pos['lpa'] is not None
+    assert pos['rpa'] is not None
+    assert len(pos['nasion']) == 3
+    assert len(pos['lpa']) == 3
+    assert len(pos['rpa']) == 3