Repository: mne-tools/mne-python

Issue:
`mne.cov.compute_whitener` returns wrong output if bads are passed explicitly.
#### Describe the bug

Description in `picks` for `mne.cov.compute_whitener`: 

> [...] Note that channels in info['bads'] will be included if their names or indices are explicitly provided.

However, the output differs from the one obtained if the same channels are passed; but with none marked as bads.
The bug is somewhere in `_smart_eigh`.

#### Steps to reproduce

```
#%% Load raw
import mne
from pathlib import Path

data_folder = Path(mne.datasets.sample.data_path())
raw_file = data_folder / 'MEG' / 'sample' / 'sample_audvis_filt-0-40_raw.fif'
raw = mne.io.read_raw_fif(raw_file, preload=True)
raw.pick_types(eeg=True)

#%% Covariance without mark bad channels
raw.info['bads'] = list()

picks_without_bads = mne.pick_types(raw.info, eeg=True, exclude=[])

noise_cov_without_bads = mne.compute_raw_covariance(
    raw, picks=picks_without_bads)

whitener_without_bads = mne.cov.compute_whitener(
    noise_cov_without_bads, raw.info, picks=raw.info.ch_names)

#%% Covariance with marked bad channels including the marked bads
raw.info['bads'] = ['EEG 002', 'EEG 012', 'EEG 015']

picks_with_bads = mne.pick_types(raw.info, eeg=True, exclude=[])

noise_cov_with_bads = mne.compute_raw_covariance(
    raw, picks=picks_with_bads)

whitener_with_bads = mne.cov.compute_whitener(
    noise_cov_with_bads, raw.info, picks=raw.info.ch_names)

#%% Test

assert (picks_without_bads == picks_with_bads).all()  # OK
assert (noise_cov_without_bads['data'] == noise_cov_with_bads['data']).all()  # OK
assert noise_cov_without_bads['names'] == noise_cov_with_bads['names']  # OK

#%% Whitener

assert whitener_without_bads[1] == whitener_with_bads[1]  # OK
assert (whitener_without_bads[0] == whitener_with_bads[0]).all()  # FAIL



Discussion:

Resolving patch:
diff --git a/mne/cov.py b/mne/cov.py
--- a/mne/cov.py
+++ b/mne/cov.py
@@ -1519,7 +1519,7 @@ def _smart_eigh(C, info, rank, scalings=None, projs=None,
     eigvec = np.zeros((n_chan, n_chan), dtype)
     mask = np.zeros(n_chan, bool)
     for ch_type, picks in _picks_by_type(info, meg_combined=True,
-                                         ref_meg=False, exclude='bads'):
+                                         ref_meg=False, exclude=[]):
         if len(picks) == 0:
             continue
         this_C = C[np.ix_(picks, picks)]
diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py
--- a/mne/preprocessing/ica.py
+++ b/mne/preprocessing/ica.py
@@ -59,7 +59,7 @@
 from ..filter import filter_data
 from .bads import _find_outliers
 from .ctps_ import ctps
-from ..io.pick import pick_channels_regexp
+from ..io.pick import pick_channels_regexp, _picks_by_type
 
 __all__ = ('ICA', 'ica_find_ecg_events', 'ica_find_eog_events',
            'get_score_funcs', 'read_ica', 'read_ica_eeglab')
@@ -660,31 +660,17 @@ def _compute_pre_whitener(self, data):
             # Scale (z-score) the data by channel type
             info = self.info
             pre_whitener = np.empty([len(data), 1])
-            for ch_type in _DATA_CH_TYPES_SPLIT + ('eog', "ref_meg"):
-                if _contains_ch_type(info, ch_type):
-                    if ch_type == 'seeg':
-                        this_picks = pick_types(info, meg=False, seeg=True)
-                    elif ch_type == 'dbs':
-                        this_picks = pick_types(info, meg=False, dbs=True)
-                    elif ch_type == 'ecog':
-                        this_picks = pick_types(info, meg=False, ecog=True)
-                    elif ch_type == 'eeg':
-                        this_picks = pick_types(info, meg=False, eeg=True)
-                    elif ch_type in ('mag', 'grad'):
-                        this_picks = pick_types(info, meg=ch_type)
-                    elif ch_type == 'eog':
-                        this_picks = pick_types(info, meg=False, eog=True)
-                    elif ch_type in ('hbo', 'hbr'):
-                        this_picks = pick_types(info, meg=False, fnirs=ch_type)
-                    elif ch_type == 'ref_meg':
-                        this_picks = pick_types(info, meg=False, ref_meg=True)
-                    else:
-                        raise RuntimeError('Should not be reached.'
-                                           'Unsupported channel {}'
-                                           .format(ch_type))
-                    pre_whitener[this_picks] = np.std(data[this_picks])
+            for _, picks_ in _picks_by_type(info, ref_meg=False, exclude=[]):
+                pre_whitener[picks_] = np.std(data[picks_])
+            if _contains_ch_type(info, "ref_meg"):
+                picks_ = pick_types(info, ref_meg=True, exclude=[])
+                pre_whitener[picks_] = np.std(data[picks_])
+            if _contains_ch_type(info, "eog"):
+                picks_ = pick_types(info, eog=True, exclude=[])
+                pre_whitener[picks_] = np.std(data[picks_])
         else:
-            pre_whitener, _ = compute_whitener(self.noise_cov, self.info)
+            pre_whitener, _ = compute_whitener(
+                self.noise_cov, self.info, picks=self.info.ch_names)
             assert data.shape[0] == pre_whitener.shape[1]
         self.pre_whitener_ = pre_whitener
 
@@ -841,16 +827,7 @@ def _transform_raw(self, raw, start, stop, reject_by_annotation=False):
         if not hasattr(self, 'mixing_matrix_'):
             raise RuntimeError('No fit available. Please fit ICA.')
         start, stop = _check_start_stop(raw, start, stop)
-
-        picks = pick_types(raw.info, include=self.ch_names, exclude='bads',
-                           meg=False, ref_meg=False)
-        if len(picks) != len(self.ch_names):
-            raise RuntimeError('Raw doesn\'t match fitted data: %i channels '
-                               'fitted but %i channels supplied. \nPlease '
-                               'provide Raw compatible with '
-                               'ica.ch_names' % (len(self.ch_names),
-                                                 len(picks)))
-
+        picks = self._get_picks(raw)
         reject = 'omit' if reject_by_annotation else None
         data = raw.get_data(picks, start, stop, reject)
         return self._transform(data)
@@ -859,44 +836,41 @@ def _transform_epochs(self, epochs, concatenate):
         """Aux method."""
         if not hasattr(self, 'mixing_matrix_'):
             raise RuntimeError('No fit available. Please fit ICA.')
-
-        picks = pick_types(epochs.info, include=self.ch_names, exclude='bads',
-                           meg=False, ref_meg=False)
-        # special case where epochs come picked but fit was 'unpicked'.
-        if len(picks) != len(self.ch_names):
-            raise RuntimeError('Epochs don\'t match fitted data: %i channels '
-                               'fitted but %i channels supplied. \nPlease '
-                               'provide Epochs compatible with '
-                               'ica.ch_names' % (len(self.ch_names),
-                                                 len(picks)))
-
+        picks = self._get_picks(epochs)
         data = np.hstack(epochs.get_data()[:, picks])
         sources = self._transform(data)
-
         if not concatenate:
             # Put the data back in 3D
             sources = np.array(np.split(sources, len(epochs.events), 1))
-
         return sources
 
     def _transform_evoked(self, evoked):
         """Aux method."""
         if not hasattr(self, 'mixing_matrix_'):
             raise RuntimeError('No fit available. Please fit ICA.')
+        picks = self._get_picks(evoked)
+        return self._transform(evoked.data[picks])
 
-        picks = pick_types(evoked.info, include=self.ch_names, exclude='bads',
-                           meg=False, ref_meg=False)
-
+    def _get_picks(self, inst):
+        """Pick logic for _transform method."""
+        picks = _picks_to_idx(
+            inst.info, self.ch_names, exclude=[], allow_empty=True)
         if len(picks) != len(self.ch_names):
-            raise RuntimeError('Evoked doesn\'t match fitted data: %i channels'
-                               ' fitted but %i channels supplied. \nPlease '
-                               'provide Evoked compatible with '
-                               'ica.ch_names' % (len(self.ch_names),
-                                                 len(picks)))
-
-        sources = self._transform(evoked.data[picks])
-
-        return sources
+            if isinstance(inst, BaseRaw):
+                kind, do = 'Raw', "doesn't"
+            elif isinstance(inst, BaseEpochs):
+                kind, do = 'Epochs', "don't"
+            elif isinstance(inst, Evoked):
+                kind, do = 'Evoked', "doesn't"
+            else:
+                raise ValueError('Data input must be of Raw, Epochs or Evoked '
+                                 'type')
+            raise RuntimeError("%s %s match fitted data: %i channels "
+                               "fitted but %i channels supplied. \nPlease "
+                               "provide %s compatible with ica.ch_names"
+                               % (kind, do, len(self.ch_names), len(picks),
+                                  kind))
+        return picks
 
     def get_components(self):
         """Get ICA topomap for components as numpy arrays.
diff --git a/mne/rank.py b/mne/rank.py
--- a/mne/rank.py
+++ b/mne/rank.py
@@ -198,7 +198,7 @@ def _estimate_rank_meeg_cov(data, info, scalings, tol='auto',
         If return_singular is True, the singular values that were
         thresholded to determine the rank are also returned.
     """
-    picks_list = _picks_by_type(info)
+    picks_list = _picks_by_type(info, exclude=[])
     scalings = _handle_default('scalings_cov_rank', scalings)
     _apply_scaling_cov(data, picks_list, scalings)
     if data.shape[1] < data.shape[0]:
@@ -322,8 +322,11 @@ def compute_rank(inst, rank=None, scalings=None, info=None, tol='auto',
         inst_type = 'covariance'
         if info is None:
             raise ValueError('info cannot be None if inst is a Covariance.')
+        # Reset bads as it's already taken into account in inst['names']
+        info = info.copy()
+        info['bads'] = []
         inst = pick_channels_cov(
-            inst, set(inst['names']) & set(info['ch_names']))
+            inst, set(inst['names']) & set(info['ch_names']), exclude=[])
         if info['ch_names'] != inst['names']:
             info = pick_info(info, [info['ch_names'].index(name)
                                     for name in inst['names']])


Test patch:
diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py
--- a/mne/preprocessing/tests/test_ica.py
+++ b/mne/preprocessing/tests/test_ica.py
@@ -664,7 +664,8 @@ def test_ica_additional(method, tmpdir, short_raw_epochs):
     ica_different_channels = ICA(n_components=2, max_iter=1)
     with pytest.warns(Warning, match='converge'):
         ica_different_channels.fit(raw, picks=[2, 3, 4, 5])
-    pytest.raises(ValueError, corrmap, [ica_different_channels, ica], (0, 0))
+    with pytest.raises(ValueError, match='Not all ICA instances have the'):
+        corrmap([ica_different_channels, ica], (0, 0))
 
     # test warnings on bad filenames
     ica_badname = tmpdir.join('test-bad-name.fif.gz')
@@ -705,7 +706,9 @@ def test_ica_additional(method, tmpdir, short_raw_epochs):
     assert_equal(ica_sorted.labels_, dict(blink=[3], think=[2]))
 
     # epochs extraction from raw fit
-    pytest.raises(RuntimeError, ica.get_sources, epochs)
+    with pytest.warns(RuntimeWarning, match='could not be picked'), \
+         pytest.raises(RuntimeError, match="match fitted data"):
+        ica.get_sources(epochs)
 
     # test filtering
     ica_raw = ica.get_sources(raw)
@@ -774,8 +777,8 @@ def f(x, y):
     # check univariate stats
     scores = ica.score_sources(raw, start=0, stop=50, score_func=stats.skew)
     # check exception handling
-    pytest.raises(ValueError, ica.score_sources, raw,
-                  target=np.arange(1))
+    with pytest.raises(ValueError, match='Sources and target do not have'):
+        ica.score_sources(raw, target=np.arange(1))
 
     params = []
     params += [(None, -1, slice(2), [0, 1])]  # variance, kurtosis params
@@ -821,10 +824,10 @@ def f(x, y):
                                     stop=epochs.times[-1])
 
     assert_equal(len(scores), ica.n_components_)
-    pytest.raises(ValueError, ica.find_bads_ecg, epochs.average(),
-                  method='ctps', threshold='auto')
-    pytest.raises(ValueError, ica.find_bads_ecg, raw,
-                  method='crazy-coupling')
+    with pytest.raises(ValueError, match='only Raw and Epochs input'):
+        ica.find_bads_ecg(epochs.average(), method='ctps', threshold='auto')
+    with pytest.raises(ValueError, match='not supported.'):
+        ica.find_bads_ecg(raw, method='crazy-coupling')
 
     with pytest.warns(RuntimeWarning, match='longer'):
         idx, scores = ica.find_bads_eog(raw)
@@ -862,8 +865,8 @@ def f(x, y):
     scores = ica.score_sources(epochs, score_func=stats.skew)
 
     # check exception handling
-    pytest.raises(ValueError, ica.score_sources, epochs,
-                  target=np.arange(1))
+    with pytest.raises(ValueError, match='Sources and target do not have'):
+        ica.score_sources(epochs, target=np.arange(1))
 
     # ecg functionality
     ecg_scores = ica.score_sources(raw, target='MEG 1531',
@@ -932,6 +935,14 @@ def f(x, y):
         with pytest.warns(RuntimeWarning, match='longer'):
             ica.find_bads_ecg(raw, threshold='auto')
 
+    # test passing picks including the marked bad channels
+    raw_ = raw.copy()
+    raw_.pick_types(eeg=True)
+    raw_.info['bads'] = [raw_.ch_names[0]]
+    picks = pick_types(raw_.info, eeg=True, exclude=[])
+    ica = ICA(n_components=0.99, max_iter='auto')
+    ica.fit(raw_, picks=picks, reject_by_annotation=True)
+
 
 @requires_sklearn
 @pytest.mark.slowtest
@@ -1149,9 +1160,11 @@ def test_bad_channels(method, allow_ref_meg):
                 picks_bad2 = pick_types(inst.info, meg=True,
                                         **{str(ch): True})
 
-            pytest.raises(ValueError, ica.fit, inst, picks=picks_bad1)
-            pytest.raises(ValueError, ica.fit, inst, picks=picks_bad2)
-        pytest.raises(ValueError, ica.fit, inst, picks=[])
+            with pytest.raises(ValueError, match='Invalid channel type'):
+                ica.fit(inst, picks=picks_bad1)
+                ica.fit(inst, picks=picks_bad2)
+        with pytest.raises(ValueError, match='No appropriate channels found'):
+            ica.fit(inst, picks=[])
 
 
 @requires_sklearn
diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py
--- a/mne/tests/test_cov.py
+++ b/mne/tests/test_cov.py
@@ -56,6 +56,7 @@ def test_compute_whitener(proj, pca):
         raw.del_proj()
     with pytest.warns(RuntimeWarning, match='Too few samples'):
         cov = compute_raw_covariance(raw)
+    assert cov['names'] == raw.ch_names
     W, _, C = compute_whitener(cov, raw.info, pca=pca, return_colorer=True,
                                verbose='error')
     n_channels = len(raw.ch_names)
@@ -75,6 +76,24 @@ def test_compute_whitener(proj, pca):
         assert pca is False
         assert_allclose(round_trip, np.eye(n_channels), atol=0.05)
 
+    raw.info['bads'] = [raw.ch_names[0]]
+    picks = pick_types(raw.info, meg=True, eeg=True, exclude=[])
+    with pytest.warns(RuntimeWarning, match='Too few samples'):
+        cov2 = compute_raw_covariance(raw, picks=picks)
+        cov3 = compute_raw_covariance(raw, picks=None)
+    assert_allclose(cov2['data'][1:, 1:], cov3['data'])
+    W2, _, C2 = compute_whitener(cov2, raw.info, pca=pca, return_colorer=True,
+                                 picks=picks, verbose='error')
+    W3, _, C3 = compute_whitener(cov3, raw.info, pca=pca, return_colorer=True,
+                                 picks=None, verbose='error')
+    assert_array_equal(W, W2)
+    assert_array_equal(C, C2)
+    n_channels = len(raw.ch_names) - len(raw.info['bads'])
+    n_reduced = len(raw.ch_names) - len(raw.info['bads'])
+    rank = n_channels - len(raw.info['projs'])
+    n_reduced = rank if pca is True else n_channels
+    assert W3.shape == C3.shape[::-1] == (n_reduced, n_channels)
+
 
 def test_cov_mismatch():
     """Test estimation with MEG<->Head mismatch."""
diff --git a/mne/tests/test_rank.py b/mne/tests/test_rank.py
--- a/mne/tests/test_rank.py
+++ b/mne/tests/test_rank.py
@@ -261,3 +261,42 @@ def test_maxfilter_get_rank(n_proj, fname, rank_orig, meg, tol_kind, tol):
     rank_new = _compute_rank_int(raw, 'info')
     assert rank_new == rank
     assert_array_equal(raw[:][0], data_orig)
+
+
+def test_explicit_bads_pick():
+    """Test when bads channels are explicitly passed + default picks=None."""
+    raw = read_raw_fif(raw_fname, preload=True)
+    raw.pick_types(eeg=True, meg=True, ref_meg=True)
+
+    # Covariance
+    # Default picks=None
+    raw.info['bads'] = list()
+    noise_cov_1 = compute_raw_covariance(raw, picks=None)
+    rank = compute_rank(noise_cov_1, info=raw.info)
+    assert rank == dict(meg=303, eeg=60)
+    assert raw.info['bads'] == []
+
+    raw.info['bads'] = ['EEG 002', 'EEG 012', 'EEG 015', 'MEG 0122']
+    noise_cov = compute_raw_covariance(raw, picks=None)
+    rank = compute_rank(noise_cov, info=raw.info)
+    assert rank == dict(meg=302, eeg=57)
+    assert raw.info['bads'] == ['EEG 002', 'EEG 012', 'EEG 015', 'MEG 0122']
+
+    # Explicit picks
+    picks = pick_types(raw.info, meg=True, eeg=True, exclude=[])
+    noise_cov_2 = compute_raw_covariance(raw, picks=picks)
+    rank = compute_rank(noise_cov_2, info=raw.info)
+    assert rank == dict(meg=303, eeg=60)
+    assert raw.info['bads'] == ['EEG 002', 'EEG 012', 'EEG 015', 'MEG 0122']
+
+    assert_array_equal(noise_cov_1['data'], noise_cov_2['data'])
+    assert noise_cov_1['names'] == noise_cov_2['names']
+
+    # Raw
+    raw.info['bads'] = list()
+    rank = compute_rank(raw)
+    assert rank == dict(meg=303, eeg=60)
+
+    raw.info['bads'] = ['EEG 002', 'EEG 012', 'EEG 015', 'MEG 0122']
+    rank = compute_rank(raw)
+    assert rank == dict(meg=302, eeg=57)
diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py
--- a/mne/viz/tests/test_ica.py
+++ b/mne/viz/tests/test_ica.py
@@ -264,13 +264,18 @@ def test_plot_ica_sources(raw_orig, browse_backend):
     raw.set_annotations(orig_annot)
 
     # test error handling
-    raw.info['bads'] = ['MEG 0113']
-    with pytest.raises(RuntimeError, match="Raw doesn't match fitted data"):
-        ica.plot_sources(inst=raw)
-    epochs.info['bads'] = ['MEG 0113']
-    with pytest.raises(RuntimeError, match="Epochs don't match fitted data"):
-        ica.plot_sources(inst=epochs)
-    epochs.info['bads'] = []
+    raw_ = raw.copy().load_data()
+    raw_.drop_channels('MEG 0113')
+    with pytest.raises(RuntimeError, match="Raw doesn't match fitted data"), \
+         pytest.warns(RuntimeWarning, match='could not be picked'):
+        ica.plot_sources(inst=raw_)
+    epochs_ = epochs.copy().load_data()
+    epochs_.drop_channels('MEG 0113')
+    with pytest.raises(RuntimeError, match="Epochs don't match fitted data"), \
+         pytest.warns(RuntimeWarning, match='could not be picked'):
+        ica.plot_sources(inst=epochs_)
+    del raw_
+    del epochs_
 
     # test w/ epochs and evokeds
     ica.plot_sources(epochs)