Repository: mne-tools/mne-python

Issue:
BUG- can no longer compute projs from CTF data
PR #5083 added an assert to make sure that ref channels are retained when picking data channels. this had the side effect of breaking EOG and ECG proj computations.

reference channels are explicitly not included when computing proj operators (https://github.com/mne-tools/mne-python/blob/master/mne/preprocessing/ssp.py#L186-L187). However there is no mechanism to tell pick_drop_channels to not care about the compensation channels.

here is the traceback
> File "<string>", line 2, in compute_proj_eog
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/utils.py", line 729, in verbose
>     return function(*args, **kwargs)
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/preprocessing/ssp.py", line 406, in compute_proj_eog
>     iir_params=iir_params, return_drop_log=return_drop_log)
>   File "<string>", line 2, in _compute_exg_proj
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/utils.py", line 729, in verbose
>     return function(*args, **kwargs)
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/preprocessing/ssp.py", line 202, in _compute_exg_proj
>     evoked = epochs.average()
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/epochs.py", line 824, in average
>     return self._compute_mean_or_stderr(picks, 'ave')
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/epochs.py", line 893, in _compute_mean_or_stderr
>     kind, self._name)
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/epochs.py", line 923, in _evoked_from_epoch_data
>     evoked.pick_channels(ch_names)
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/channels/channels.py", line 716, in pick_channels
>     pick_channels(self.info['ch_names'], ch_names))
>   File "/mnt/isilon/meg_lab/bloyl/Programs/mne-python-devel/mne/channels/channels.py", line 815, in _pick_drop_channels
>     assert len(comp_picks) == len(comp_names)
> AssertionError




Discussion:
> However there is no mechanism to tell pick_drop_channels to not care about the compensation channels.

You can pass `check_comps=False` to the private `_pick_drop_channels`:

https://github.com/mne-tools/mne-python/blob/master/mne/channels/channels.py#L796

Can you see if the function can be modified to use this? In particular doing `Epochs(... picks)` might not be the way to go, but rather create the object, then use the private drop function with `check_comps=False` to do the picking.
I see two options:

1. Since `_compute_proj` currently rejects the `ref_meg` channels at computation time:
https://github.com/mne-tools/mne-python/blob/master/mne/proj.py#L72-L76
we could keep the ref_meg channels in the epochs object and just let `_compute_proj` take care of removing them?

2. In `_compute_exg_proj` we could test if the supplied raw is an instance of RawCTF and only keep the ref channels for those instances.




There is a third option (that I hinted at), don't do picking with the `Epochs` constructor at all but immediately afterward with a call to the private `epochs._pick_drop_channels(picks)`. This should effectively work out to be a worse version of your (1) if `_compute_proj` is smart enough to leave the reference channels out during its computation. If it isn't, though (if it includes the ref MEG sensors), then my proposal might be easiest, or we can make the `_compute_proj` smarter (up to you).

In general we should not do special cases for CTF data, it will lead to breakages if `comps` is ever used by another system (future compat) or for another purpose (e.g., I have thought about how you could store any arbitrary linear spatial transform in there).

Resolving patch:
diff --git a/mne/preprocessing/ssp.py b/mne/preprocessing/ssp.py
--- a/mne/preprocessing/ssp.py
+++ b/mne/preprocessing/ssp.py
@@ -183,8 +183,11 @@ def _compute_exg_proj(mode, raw, raw_event, tmin, tmax,
             _safe_del_key(flat, 'eog')
 
     # exclude bad channels from projection
-    picks = pick_types(my_info, meg=True, eeg=True, eog=True, ref_meg=False,
+    # keep reference channels if compensation channels are present
+    ref_meg = len(my_info['comps']) > 0
+    picks = pick_types(my_info, meg=True, eeg=True, eog=True, ref_meg=ref_meg,
                        exclude='bads')
+
     raw.filter(l_freq, h_freq, picks=picks, filter_length=filter_length,
                n_jobs=n_jobs, method=filter_method, iir_params=iir_params,
                l_trans_bandwidth=0.5, h_trans_bandwidth=0.5,


Test patch:
diff --git a/mne/preprocessing/tests/test_ssp.py b/mne/preprocessing/tests/test_ssp.py
--- a/mne/preprocessing/tests/test_ssp.py
+++ b/mne/preprocessing/tests/test_ssp.py
@@ -5,10 +5,12 @@
 from numpy.testing import assert_array_almost_equal
 import numpy as np
 
-from mne.io import read_raw_fif
+from mne.io import read_raw_fif, read_raw_ctf
 from mne.io.proj import make_projector, activate_proj
 from mne.preprocessing.ssp import compute_proj_ecg, compute_proj_eog
 from mne.utils import run_tests_if_main
+from mne.datasets import testing
+from mne import pick_types
 
 warnings.simplefilter('always')  # enable b/c these tests throw warnings
 
@@ -17,6 +19,9 @@
 dur_use = 5.0
 eog_times = np.array([0.5, 2.3, 3.6, 14.5])
 
+ctf_fname = op.join(testing.data_path(download=False), 'CTF',
+                    'testdata_ctf.ds')
+
 
 def test_compute_proj_ecg():
     """Test computation of ECG SSP projectors."""
@@ -122,4 +127,55 @@ def test_compute_proj_parallel():
                                    bads=['MEG 2443'])
     assert_array_almost_equal(projs, projs_2, 10)
 
+
+def _check_projs_for_expected_channels(projs, n_mags, n_grads, n_eegs):
+    for p in projs:
+        if 'planar' in p['desc']:
+            assert len(p['data']['col_names']) == n_grads
+        elif 'axial' in p['desc']:
+            assert len(p['data']['col_names']) == n_mags
+        elif 'eeg' in p['desc']:
+            assert len(p['data']['col_names']) == n_eegs
+
+
+@testing.requires_testing_data
+def test_compute_proj_ctf():
+    """Test to show that projector code completes on CTF data."""
+    raw = read_raw_ctf(ctf_fname)
+    raw.load_data()
+
+    # expected channels per projector type
+    n_mags = len(pick_types(raw.info, meg='mag', ref_meg=False,
+                            exclude='bads'))
+    n_grads = len(pick_types(raw.info, meg='grad', ref_meg=False,
+                             exclude='bads'))
+    n_eegs = len(pick_types(raw.info, meg=False, eeg=True, ref_meg=False,
+                            exclude='bads'))
+
+    # Test with and without gradient compensation
+    for c in [0, 1]:
+        raw.apply_gradient_compensation(c)
+        for average in [False, True]:
+            n_projs_init = len(raw.info['projs'])
+            projs, events = compute_proj_eog(raw, n_mag=2, n_grad=2, n_eeg=2,
+                                             average=average,
+                                             ch_name='EEG059',
+                                             avg_ref=True, no_proj=False,
+                                             l_freq=None, h_freq=None,
+                                             reject=None, tmax=dur_use,
+                                             filter_length=6000)
+            _check_projs_for_expected_channels(projs, n_mags, n_grads, n_eegs)
+            assert len(projs) == (5 + n_projs_init)
+
+            projs, events = compute_proj_ecg(raw, n_mag=1, n_grad=1, n_eeg=2,
+                                             average=average,
+                                             ch_name='EEG059',
+                                             avg_ref=True, no_proj=False,
+                                             l_freq=None, h_freq=None,
+                                             reject=None, tmax=dur_use,
+                                             filter_length=6000)
+            _check_projs_for_expected_channels(projs, n_mags, n_grads, n_eegs)
+            assert len(projs) == (4 + n_projs_init)
+
+
 run_tests_if_main()