Repository: mne-tools/mne-python

Issue:
reading eeglab files with channels missing position
I have many eeglab files that contain at least one channel that has undefined position (for example biosemi EXG channels) - the way `mne.io.eeglab._get_info` works currently is to ignore all channel positions if at least one channel has no specified position. 
This behavior originates [here](https://github.com/mne-tools/mne-python/blob/master/mne/io/eeglab/eeglab.py#L81) - `np.unique` of x, y and z position is checked and if the size of the unique is == 1 the channel positions are ignored. In the case of no position for a given channel - `_to_loc` which is used to get channel positions returns 0 for x, y and z - in which case unique is of size 1 and all channel positions are ignored.
I think it would be better if `_to_loc` returned `np.nan` when given value was not present and channels with at least one `np.nan` in position were omitted when building Montage. I can make a PR if you agree.


Discussion:
I don't know what is the consequence of having `NaN`s in the `info`. Perhaps @agramfort or @Eric89GXL can have better opinion here

Resolving patch:
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
@@ -55,12 +55,14 @@ def _to_loc(ll):
     if isinstance(ll, (int, float)) or len(ll) > 0:
         return ll
     else:
-        return 0.
+        return np.nan
 
 
 def _get_info(eeg, montage, eog=()):
     """Get measurement info."""
+    from scipy import io
     info = _empty_info(sfreq=eeg.srate)
+    update_ch_names = True
 
     # add the ch_names and info['chs'][idx]['loc']
     path = None
@@ -68,21 +70,33 @@ def _get_info(eeg, montage, eog=()):
             eeg.chanlocs = [eeg.chanlocs]
 
     if len(eeg.chanlocs) > 0:
-        ch_names, pos = list(), list()
+        pos_fields = ['X', 'Y', 'Z']
+        if (isinstance(eeg.chanlocs, np.ndarray) and not isinstance(
+                eeg.chanlocs[0], io.matlab.mio5_params.mat_struct)):
+            has_pos = all(fld in eeg.chanlocs[0].dtype.names
+                          for fld in pos_fields)
+        else:
+            has_pos = all(hasattr(eeg.chanlocs[0], fld)
+                          for fld in pos_fields)
+        get_pos = has_pos and montage is None
+        pos_ch_names, ch_names, pos = list(), list(), list()
         kind = 'user_defined'
-        selection = np.arange(len(eeg.chanlocs))
-        locs_available = True
+        update_ch_names = False
         for chanloc in eeg.chanlocs:
             ch_names.append(chanloc.labels)
-            loc_x = _to_loc(chanloc.X)
-            loc_y = _to_loc(chanloc.Y)
-            loc_z = _to_loc(chanloc.Z)
-            locs = np.r_[-loc_y, loc_x, loc_z]
-            if np.unique(locs).size == 1:
-                locs_available = False
-            pos.append(locs)
-        if locs_available:
-            montage = Montage(np.array(pos), ch_names, kind, selection)
+            if get_pos:
+                loc_x = _to_loc(chanloc.X)
+                loc_y = _to_loc(chanloc.Y)
+                loc_z = _to_loc(chanloc.Z)
+                locs = np.r_[-loc_y, loc_x, loc_z]
+                if not np.any(np.isnan(locs)):
+                    pos_ch_names.append(chanloc.labels)
+                    pos.append(locs)
+        n_channels_with_pos = len(pos_ch_names)
+        info = create_info(ch_names, eeg.srate, ch_types='eeg')
+        if n_channels_with_pos > 0:
+            selection = np.arange(n_channels_with_pos)
+            montage = Montage(np.array(pos), pos_ch_names, kind, selection)
     elif isinstance(montage, string_types):
         path = op.dirname(montage)
     else:  # if eeg.chanlocs is empty, we still need default chan names
@@ -92,7 +106,7 @@ def _get_info(eeg, montage, eog=()):
         info = create_info(ch_names, eeg.srate, ch_types='eeg')
     else:
         _check_update_montage(info, montage, path=path,
-                              update_ch_names=True)
+                              update_ch_names=update_ch_names)
 
     info['buffer_size_sec'] = 1.  # reasonable default
     # update the info dict


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
@@ -1,4 +1,5 @@
 # Author: Mainak Jas <mainak.jas@telecom-paristech.fr>
+#         Mikolaj Magnuski <mmagnuski@swps.edu.pl>
 #
 # License: BSD (3-clause)
 
@@ -8,7 +9,7 @@
 import warnings
 from nose.tools import assert_raises, assert_equal, assert_true
 import numpy as np
-from numpy.testing import assert_array_equal
+from numpy.testing import assert_array_equal, assert_array_almost_equal
 
 from mne import write_events, read_epochs_eeglab, Epochs, find_events
 from mne.io import read_raw_eeglab
@@ -135,6 +136,72 @@ def test_io_set():
     # no warning for 'no events found'
     assert_equal(len(w), 0)
 
+    # test reading file with 3 channels - one without position information
+    # first, create chanlocs structured array
+    ch_names = ['F3', 'unknown', 'FPz']
+    x, y, z = [1., 2., np.nan], [4., 5., np.nan], [7., 8., np.nan]
+    dt = [('labels', 'S10'), ('X', 'f8'), ('Y', 'f8'), ('Z', 'f8')]
+    chanlocs = np.zeros((3,), dtype=dt)
+    for ind, vals in enumerate(zip(ch_names, x, y, z)):
+        for fld in range(4):
+            chanlocs[ind][dt[fld][0]] = vals[fld]
+
+    # save set file
+    one_chanpos_fname = op.join(temp_dir, 'test_chanpos.set')
+    io.savemat(one_chanpos_fname, {'EEG':
+               {'trials': eeg.trials, 'srate': eeg.srate,
+                'nbchan': 3, 'data': np.random.random((3, 3)),
+                'epoch': eeg.epoch, 'event': eeg.epoch,
+                'chanlocs': chanlocs, 'times': eeg.times[:3], 'pnts': 3}})
+    # load it
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_eeglab(input_fname=one_chanpos_fname, preload=True)
+    # one warning because some channels are not found in Montage
+    assert_equal(len(w), 1)
+    # position should be present for first two channels
+    for i in range(2):
+        assert_array_equal(raw.info['chs'][i]['loc'][:3],
+                           np.array([-chanlocs[i]['Y'],
+                                     chanlocs[i]['X'],
+                                     chanlocs[i]['Z']]))
+    # position of the last channel should be zero
+    assert_array_equal(raw.info['chs'][-1]['loc'][:3], np.array([0., 0., 0.]))
+
+    # test reading channel names from set and positions from montage
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_eeglab(input_fname=one_chanpos_fname, preload=True,
+                              montage=montage)
+    # one warning because some channels are not found in Montage
+    assert_equal(len(w), 1)
+
+    # when montage was passed - channel positions should be taken from there
+    correct_pos = [[-0.56705965, 0.67706631, 0.46906776], [0., 0., 0.],
+                   [0., 0.99977915, -0.02101571]]
+    for ch_ind in range(3):
+        assert_array_almost_equal(raw.info['chs'][ch_ind]['loc'][:3],
+                                  np.array(correct_pos[ch_ind]))
+
+    # test reading channel names but not positions when there is no X (only Z)
+    # field in the EEG.chanlocs structure
+    nopos_chanlocs = chanlocs[['labels', 'Z']]
+    nopos_fname = op.join(temp_dir, 'test_no_chanpos.set')
+    io.savemat(nopos_fname, {'EEG':
+               {'trials': eeg.trials, 'srate': eeg.srate, 'nbchan': 3,
+                'data': np.random.random((3, 2)), 'epoch': eeg.epoch,
+                'event': eeg.epoch, 'chanlocs': nopos_chanlocs,
+                'times': eeg.times[:2], 'pnts': 2}})
+    # load the file
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
+        raw = read_raw_eeglab(input_fname=nopos_fname, preload=True)
+    # test that channel names have been loaded but not channel positions
+    for i in range(3):
+        assert_equal(raw.info['chs'][i]['ch_name'], ch_names[i])
+        assert_array_equal(raw.info['chs'][i]['loc'][:3],
+                           np.array([0., 0., 0.]))
+
     # test if .dat file raises an error
     eeg = io.loadmat(epochs_fname, struct_as_record=False,
                      squeeze_me=True)['EEG']