Repository: mne-tools/mne-bids

Issue:
EEGLAB: allow to copy .fdt files and update the pointer in corresponding .set
EEGLAB files are MATLAB files ... albeit with a `.set` extension instead of the usual `.mat`. The EEGLAB files can contain the EEG data in a matlab struct field called `data`. However, this `data` field can alternatively contain a link to an accompanying (separate) `.fdt` (=FloatDaTa) file. 

Currently MNE-BIDS only allows the standalone `.set` files and thorws an error when an `.set`+`.fdt` combination is used. This should be fixed, see:

https://github.com/mne-tools/mne-bids/blob/7403fc56b33bfe9b61e2cd7ee214ab9ad24312b0/mne_bids/utils.py#L497-L505


Discussion:
+1 sounds like an easy fix ...

Resolving patch:
diff --git a/mne_bids/mne_bids.py b/mne_bids/mne_bids.py
--- a/mne_bids/mne_bids.py
+++ b/mne_bids/mne_bids.py
@@ -47,7 +47,7 @@
 
 eeg_manufacturers = {'.vhdr': 'BrainProducts', '.eeg': 'BrainProducts',
                      '.edf': 'Mixed', '.bdf': 'Biosemi', '.set': 'Mixed',
-                     '.cnt': 'Neuroscan'}
+                     '.fdt': 'Mixed', '.cnt': 'Neuroscan'}
 
 # Merge the manufacturer dictionaries in a python2 / python3 compatible way
 MANUFACTURERS = dict()
@@ -100,7 +100,7 @@ def _channels_tsv(raw, fname, verbose):
     # get the manufacturer from the file in the Raw object
     manufacturer = None
     if hasattr(raw, 'filenames'):
-        # NOTE: Hack for EEGLAB bug in MNE-Python 0.16; fixed in MNE-Python
+        # XXX: Hack for EEGLAB bug in MNE-Python 0.16; fixed in MNE-Python
         # 0.17, ... remove the hack after upgrading dependencies in MNE-BIDS
         if raw.filenames[0] is None:  # hack
             ext = '.set'  # hack
diff --git a/mne_bids/utils.py b/mne_bids/utils.py
--- a/mne_bids/utils.py
+++ b/mne_bids/utils.py
@@ -483,42 +483,32 @@ def copyfile_eeglab(src, dest):
                          ' but got {}, {}'.format(ext_src, ext_dest))
 
     # Extract matlab struct "EEG" from EEGLAB file
-    # if the data field is a string, it points to a .fdt file in src dir
-    mat = loadmat(src, matlab_compatible=True)
+    mat = loadmat(src, squeeze_me=False, chars_as_strings=False,
+                  mat_dtype=False, struct_as_record=True)
     if 'EEG' not in mat:
         raise ValueError('Could not find "EEG" field in {}'.format(src))
     eeg = mat['EEG']
+
+    # If the data field is a string, it points to a .fdt file in src dir
     data = eeg[0][0]['data']
     if all([item in data[0, -4:] for item in '.fdt']):
-        raise ValueError('Found associated .fdt file containing the binary '
-                         'EEG data: {}.\nMNE-BIDS does currently not support '
-                         ' .fdt files. Please re-load your .set file using '
-                         ' EEGLAB and save the data as a single .set file '
-                         'without accompanying .fdt file.'.format(eeg['data']))
-
-        # FIXME: We should move the .fdt file together with the .set file and
-        # give meaningful names to both. Then the .set file (which is a matlab
-        # .mat file) needs to be read. The EEG matlab structure of the .set
-        # file contains a field "data", which is a string pointing to the .fdt
-        # file. This string needs to be updated to the new BIDS name that the
-        # .set file received while copying.
-        #
-        # Unfortunately, there are issues with performing a round-trip of
-        # reading the .set, modifying it, and saving it again.
         head, tail = op.split(src)
-        fdt_path = op.join(head, eeg['data'])
+        fdt_pointer = ''.join(data.tolist()[0])
+        fdt_path = op.join(head, fdt_pointer)
         fdt_name, fdt_ext = _parse_ext(fdt_path)
         if fdt_ext != '.fdt':
             raise IOError('Expected extension {} for linked data but found'
                           ' {}'.format('.fdt', fdt_ext))
+
+        # Copy the fdt file and give it a new name
         sh.copyfile(fdt_path, fname_dest + '.fdt')
 
-        # Write a new .set file with an updated pointer
+        # Now adjust the pointer in the set file
         head, tail = op.split(fname_dest + '.fdt')
-        eeg['data'] = tail
-        savemat(dest, eeg, appendmat=False)
+        mat['EEG'][0][0]['data'] = tail
+        savemat(dest, mat, appendmat=False)
 
-    # If no .fdt file, simply copy the .set file
+    # If no .fdt file, simply copy the .set file, no modifications necessary
     else:
         sh.copyfile(src, dest)
 


Test patch:
diff --git a/mne_bids/tests/test_mne_bids.py b/mne_bids/tests/test_mne_bids.py
--- a/mne_bids/tests/test_mne_bids.py
+++ b/mne_bids/tests/test_mne_bids.py
@@ -190,6 +190,7 @@ def test_bdf():
 
 def test_set():
     """Test raw_to_bids conversion for EEGLAB data."""
+    # standalone .set file
     output_path = _TempDir()
     data_path = op.join(testing.data_path(), 'EEGLAB')
     raw_fname = op.join(data_path, 'test_raw_onefile.set')
@@ -201,6 +202,18 @@ def test_set():
     cmd = ['bids-validator', '--bep006', output_path]
     run_subprocess(cmd, shell=shell)
 
+    # .set with associated .fdt
+    output_path = _TempDir()
+    data_path = op.join(testing.data_path(), 'EEGLAB')
+    raw_fname = op.join(data_path, 'test_raw.set')
+
+    raw_to_bids(subject_id=subject_id, session_id=session_id, run=run,
+                task=task, raw_file=raw_fname, output_path=output_path,
+                overwrite=True, kind='eeg')
+
+    cmd = ['bids-validator', '--bep006', output_path]
+    run_subprocess(cmd, shell=shell)
+
 
 def test_cnt():
     """Test raw_to_bids conversion for Neuroscan data."""
diff --git a/mne_bids/tests/test_utils.py b/mne_bids/tests/test_utils.py
--- a/mne_bids/tests/test_utils.py
+++ b/mne_bids/tests/test_utils.py
@@ -152,9 +152,10 @@ def test_copyfile_eeglab():
         savemat(fake_set, {'arr': [1, 2, 3]}, appendmat=False)
         copyfile_eeglab(fake_set, new_name)
 
-    # .fdt not implemented error
-    with pytest.raises(ValueError):
-        copyfile_eeglab(raw_fname, new_name)
+    # Test copying and reading a combined set+fdt
+    copyfile_eeglab(raw_fname, new_name)
+    raw = mne.io.read_raw_eeglab(new_name)
+    assert isinstance(raw, mne.io.BaseRaw)
 
 
 def test_infer_eeg_placement_scheme():