Repository: mne-tools/mne-python

Issue:
use weakest filter settings for heterogeneous channel filters
Close #3577. Title really says it all --  makes the reader more consistent with the behavior of `Raw.filter` when `picks is not None`. 

Hopefully, the user is paying attention to his warnings and/or not doing online filtering, much less heterogeneous filtering. Should we include a note on this behavior somewhere in the documentation? Do any of the other file formats support heterogeneous channel filter settings?



Discussion:
## [Current coverage](https://codecov.io/gh/mne-tools/mne-python/pull/3593?src=pr) is 87.30% (diff: 63.63%)

> Merging [#3593](https://codecov.io/gh/mne-tools/mne-python/pull/3593?src=pr) into [master](https://codecov.io/gh/mne-tools/mne-python/branch/master?src=pr) will decrease coverage by **<.01%**

``` diff
@@             master      #3593   diff @@
==========================================
  Files           341        341          
  Lines         60372      60378     +6   
  Methods           0          0          
  Messages          0          0          
  Branches       9229       9232     +3   
==========================================
+ Hits          52705      52710     +5   
- Misses         4915       4916     +1   
  Partials       2752       2752          
```

![Sunburst](https://codecov.io/gh/mne-tools/mne-python/pull/3593/graphs/sunburst.svg?src=pr&size=150)

> Powered by [Codecov](https://codecov.io?src=pr). Last update [765cff8...fbe795e](https://codecov.io/gh/mne-tools/mne-python/compare/765cff8a214f45045b45ff2500a4a91ecdaa2b32...fbe795e6eac8d96d13a769fa1fc869cd25a63d38?src=pr)

In that case LGTM.

Thanks @palday 


Resolving patch:
diff --git a/mne/channels/montage.py b/mne/channels/montage.py
--- a/mne/channels/montage.py
+++ b/mne/channels/montage.py
@@ -14,6 +14,7 @@
 import os.path as op
 
 import numpy as np
+import xml.etree.ElementTree as ElementTree
 
 from ..viz import plot_montage
 from .channels import _contains_ch_type
@@ -461,7 +462,8 @@ def transform_to_head(self):
         native_head_t = get_ras_to_neuromag_trans(nasion, lpa, rpa)
         self.nasion, self.lpa, self.rpa = apply_trans(
             native_head_t, np.array([nasion, lpa, rpa]))
-        self.elp = apply_trans(native_head_t, self.elp)
+        if self.elp is not None:
+            self.elp = apply_trans(native_head_t, self.elp)
         if self.hsp is not None:
             self.hsp = apply_trans(native_head_t, self.hsp)
         if self.dig_ch_pos is not None:
@@ -513,7 +515,8 @@ def _check_frame(d, frame_str):
 
 
 def read_dig_montage(hsp=None, hpi=None, elp=None, point_names=None,
-                     unit='auto', fif=None, transform=True, dev_head_t=False):
+                     unit='auto', fif=None, egi=None, transform=True,
+                     dev_head_t=False):
     r"""Read subject-specific digitization montage from a file.
 
     Parameters
@@ -553,6 +556,12 @@ def read_dig_montage(hsp=None, hpi=None, elp=None, point_names=None,
 
         .. versionadded:: 0.12
 
+    egi : str | None
+        EGI MFF XML coordinates file from which to read digitization locations.
+        If str (filename), all other arguments are ignored.
+
+        .. versionadded:: 0.14
+
     transform : bool
         If True (default), points will be transformed to Neuromag space
         using :meth:`DigMontage.transform_to_head`.
@@ -590,9 +599,9 @@ def read_dig_montage(hsp=None, hpi=None, elp=None, point_names=None,
         if dev_head_t or not transform:
             raise ValueError('transform must be True and dev_head_t must be '
                              'False for FIF dig montage')
-        if not all(x is None for x in (hsp, hpi, elp, point_names)):
-            raise ValueError('hsp, hpi, elp, and point_names must all be None '
-                             'if fif is not None')
+        if not all(x is None for x in (hsp, hpi, elp, point_names, egi)):
+            raise ValueError('hsp, hpi, elp, point_names, egi must all be '
+                             'None if fif is not None')
         _check_fname(fif, overwrite=True, must_exist=True)
         # Load the dig data
         f, tree = fiff_open(fif)[:2]
@@ -624,6 +633,45 @@ def read_dig_montage(hsp=None, hpi=None, elp=None, point_names=None,
         hsp = np.array(hsp) if len(hsp) else None
         elp = np.array(elp) if len(elp) else None
         coord_frame = 'head'
+    elif egi is not None:
+        if not all(x is None for x in (hsp, hpi, elp, point_names, fif)):
+            raise ValueError('hsp, hpi, elp, point_names, fif must all be '
+                             'None if egi is not None')
+        _check_fname(egi, overwrite=True, must_exist=True)
+
+        root = ElementTree.parse(egi).getroot()
+        ns = root.tag[root.tag.index('{'):root.tag.index('}') + 1]
+        sensors = root.find('%ssensorLayout/%ssensors' % (ns, ns))
+        fids = dict()
+        dig_ch_pos = dict()
+
+        fid_name_map = {'Nasion': 'nasion',
+                        'Right periauricular point': 'rpa',
+                        'Left periauricular point': 'lpa'}
+
+        for s in sensors:
+            name, number, kind = s[0].text, int(s[1].text), int(s[2].text)
+            coordinates = np.array([float(s[3].text), float(s[4].text),
+                                    float(s[5].text)])
+            # EEG Channels
+            if kind == 0:
+                dig_ch_pos['EEG %03d' % number] = coordinates
+            # Reference
+            elif kind == 1:
+                dig_ch_pos['EEG %03d' %
+                           (len(dig_ch_pos.keys()) + 1)] = coordinates
+            # Fiducials
+            elif kind == 2:
+                fid_name = fid_name_map[name]
+                fids[fid_name] = coordinates
+            # Unknown
+            else:
+                warn('Unknown sensor type %s detected. Skipping sensor...'
+                     'Proceed with caution!' % kind)
+
+        fids = [fids[key] for key in ('nasion', 'lpa', 'rpa')]
+        coord_frame = 'unknown'
+
     else:
         fids = [None] * 3
         dig_ch_pos = None
diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py
--- a/mne/datasets/utils.py
+++ b/mne/datasets/utils.py
@@ -225,7 +225,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True,
     path = _get_path(path, key, name)
     # To update the testing or misc dataset, push commits, then make a new
     # release on GitHub. Then update the "releases" variable:
-    releases = dict(testing='0.29', misc='0.3')
+    releases = dict(testing='0.30', misc='0.3')
     # And also update the "hashes['testing']" variable below.
 
     # To update any other dataset, update the data archive itself (upload
@@ -275,7 +275,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True,
         sample='1d5da3a809fded1ef5734444ab5bf857',
         somato='f3e3a8441477bb5bacae1d0c6e0964fb',
         spm='f61041e3f3f2ba0def8a2ca71592cc41',
-        testing='1f9182a51749f5fef484dd7a02ee40e3',
+        testing='a8209367a0795f8e6d31bbc913227fae',
         multimodal='26ec847ae9ab80f58f204d09e2c08367',
         visual_92_categories='46c7e590f4a48596441ce001595d5e58',
     )


Test patch:
diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py
--- a/mne/channels/tests/test_montage.py
+++ b/mne/channels/tests/test_montage.py
@@ -22,12 +22,15 @@
 from mne.io.constants import FIFF
 from mne.io.meas_info import _read_dig_points
 from mne.io.kit import read_mrk
-from mne.io import read_raw_brainvision
+from mne.io import read_raw_brainvision, read_raw_egi, Raw
 
 from mne.datasets import testing
 
 data_path = testing.data_path(download=False)
 fif_dig_montage_fname = op.join(data_path, 'montage', 'eeganes07.fif')
+egi_dig_montage_fname = op.join(data_path, 'montage', 'coordinates.xml')
+egi_raw_fname = op.join(data_path, 'montage', 'egi_dig_test.raw')
+egi_fif_fname = op.join(data_path, 'montage', 'egi_dig_raw.fif')
 locs_montage_fname = op.join(data_path, 'EEGLAB', 'test_chans.locs')
 evoked_fname = op.join(data_path, 'montage', 'level2_raw-ave.fif')
 
@@ -392,6 +395,39 @@ def test_fif_dig_montage():
     _check_roundtrip(montage, fname_temp)
 
 
+@testing.requires_testing_data
+def test_egi_dig_montage():
+    """Test EGI MFF XML dig montage support."""
+    dig_montage = read_dig_montage(egi=egi_dig_montage_fname)
+
+    # # test round-trip IO
+    temp_dir = _TempDir()
+    fname_temp = op.join(temp_dir, 'egi_test.fif')
+    _check_roundtrip(dig_montage, fname_temp)
+
+    # Test coordinate transform
+    dig_montage.transform_to_head()
+    # nasion
+    assert_almost_equal(dig_montage.nasion[0], 0)
+    assert_almost_equal(dig_montage.nasion[2], 0)
+    # lpa and rpa
+    assert_allclose(dig_montage.lpa[1:], 0, atol=1e-16)
+    assert_allclose(dig_montage.rpa[1:], 0, atol=1e-16)
+
+    # Test accuracy and embedding within raw object
+    raw_egi = read_raw_egi(egi_raw_fname)
+    raw_egi.set_montage(dig_montage)
+    test_raw_egi = Raw(egi_fif_fname)
+
+    assert_equal(len(raw_egi.ch_names), len(test_raw_egi.ch_names))
+    for ch_raw, ch_test_raw in zip(raw_egi.info['chs'],
+                                   test_raw_egi.info['chs']):
+        assert_equal(ch_raw['ch_name'], ch_test_raw['ch_name'])
+        assert_equal(ch_raw['coord_frame'], FIFF.FIFFV_COORD_HEAD)
+        assert_allclose(ch_raw['loc'], ch_test_raw['loc'], atol=1e-7)
+    assert_dig_allclose(raw_egi.info, test_raw_egi.info)
+
+
 def _check_roundtrip(montage, fname):
     """Check roundtrip writing."""
     assert_equal(montage.coord_frame, 'head')
@@ -399,8 +435,10 @@ def _check_roundtrip(montage, fname):
     montage_read = read_dig_montage(fif=fname)
     assert_equal(str(montage), str(montage_read))
     for kind in ('elp', 'hsp', 'nasion', 'lpa', 'rpa'):
-        assert_allclose(getattr(montage, kind),
-                        getattr(montage_read, kind), err_msg=kind)
+        if getattr(montage, kind) is not None:
+            assert_allclose(getattr(montage, kind),
+                            getattr(montage_read, kind), err_msg=kind)
     assert_equal(montage_read.coord_frame, 'head')
 
+
 run_tests_if_main()