Repository: mne-tools/mne-python

Issue:
find_bad_channels_maxwell errors on CTF data
find_bad_channels_maxwell raises an exception for ctf data if the data is longer then one duration.

to replicate:

```
import os.path as op
from mne.io import read_raw_ctf
from mne.datasets import testing
from mne.preprocessing import find_bad_channels_maxwell

ctf_fname_continuous = op.join(testing.data_path(download=True),
                               'CTF', 'testdata_ctf.ds')

raw = read_raw_ctf(ctf_fname_continuous)
raw.apply_gradient_compensation(0)
bads, flats = find_bad_channels_maxwell(raw_cp)
```

which raises
```
/opt/anaconda3/envs/chop_mne-dev/lib/python3.7/site-packages/mne/preprocessing/maxwell.py in find_bad_channels_maxwell(***failed resolving arguments***)
   2021             # p2p
   2022             range_ = np.ptp(delta, axis=-1)
-> 2023             range_ *= params['coil_scale'][these_picks, 0]
   2024             mean, std = np.mean(range_), np.std(range_)
   2025             # z score

IndexError: index 298 is out of bounds for axis 0 with size 298
```

I suspect that ` _prep_maxwell_filter` and/or `_get_coil_scale` aren't dealing with ctfs axial gradiometers (that get labeled as magnetometers) correctly. So it is probably also an issue with KIT and Artemis sensors.


Discussion:
Indeed it is probably some indexing problem. I can look soon

Resolving patch:
diff --git a/mne/preprocessing/maxwell.py b/mne/preprocessing/maxwell.py
--- a/mne/preprocessing/maxwell.py
+++ b/mne/preprocessing/maxwell.py
@@ -31,7 +31,7 @@
 from ..io import _loc_to_coil_trans, _coil_trans_to_loc, BaseRaw, RawArray
 from ..io.pick import pick_types, pick_info
 from ..utils import (verbose, logger, _clean_names, warn, _time_mask, _pl,
-                     _check_option, _ensure_int, _validate_type)
+                     _check_option, _ensure_int, _validate_type, use_log_level)
 from ..fixes import _get_args, _safe_svd, einsum, bincount
 from ..channels.channels import _get_T1T2_mag_inds
 
@@ -404,7 +404,7 @@ def _run_maxwell_filter(
         st_when, ctc, coil_scale, this_pos_quat, meg_picks, good_mask,
         grad_picks, head_pos, info, _get_this_decomp_trans, S_recon,
         update_kwargs,
-        reconstruct='in', count_msg=True, copy=True):
+        reconstruct='in', copy=True):
     # Eventually find_bad_channels_maxwell could be sped up by moving this
     # outside the loop (e.g., in the prep function) but regularization depends
     # on which channels are being used, so easier just to include it here.
@@ -464,9 +464,8 @@ def _run_maxwell_filter(
 
     # Loop through buffer windows of data
     n_sig = int(np.floor(np.log10(max(len(starts), 0)))) + 1
-    if count_msg:
-        logger.info(
-            '    Processing %s data chunk%s' % (len(starts), _pl(starts)))
+    logger.info(
+        '    Processing %s data chunk%s' % (len(starts), _pl(starts)))
     for ii, (start, stop) in enumerate(zip(starts, stops)):
         tsss_valid = (stop - start) >= st_duration
         rel_times = raw_sss.times[start:stop]
@@ -1964,6 +1963,8 @@ def find_bad_channels_maxwell(
     del origin, int_order, ext_order, calibration, cross_talk, coord_frame
     del regularize, ignore_ref, bad_condition, head_pos, mag_scale
     good_meg_picks = params['meg_picks'][params['good_mask']]
+    assert len(params['meg_picks']) == len(params['coil_scale'])
+    assert len(params['good_mask']) == len(params['meg_picks'])
     noisy_chs = Counter()
     flat_chs = Counter()
     flat_limits = dict(grad=0.01e-13, mag=0.01e-15)
@@ -1975,7 +1976,6 @@ def find_bad_channels_maxwell(
     flat_step = max(20, int(30 * raw.info['sfreq'] / 1000.))
     all_flats = set()
     for si, (start, stop) in enumerate(zip(starts, stops)):
-        prefix = '%03d:' % (si,)
         n_iter = 0
         orig_data = raw.get_data(None, start, stop, verbose=False)
         chunk_raw = RawArray(
@@ -2000,27 +2000,29 @@ def find_bad_channels_maxwell(
         chunk_noisy = list()
         params['st_duration'] = int(round(
             chunk_raw.times[-1] * raw.info['sfreq']))
+        t = chunk_raw.times[[0, -1]] + start / raw.info['sfreq']
+        logger.info('        Interval %3d: %8.3f - %8.3f'
+                    % ((si + 1,) + tuple(t[[0, -1]])))
         for n_iter in range(1, 101):  # iteratively exclude the worst ones
             assert set(raw.info['bads']) & set(chunk_noisy) == set()
-            params['good_mask'][:] = np.array([
+            params['good_mask'][:] = [
                 chunk_raw.ch_names[pick] not in
                 raw.info['bads'] + chunk_noisy + chunk_flats
-                for pick in params['meg_picks']], int)
+                for pick in params['meg_picks']]
             chunk_raw._data[:] = orig_data
             delta = chunk_raw.get_data(these_picks)
-            _run_maxwell_filter(
-                chunk_raw, reconstruct='orig', count_msg=False, copy=False,
-                **params)
+            with use_log_level(False):
+                _run_maxwell_filter(
+                    chunk_raw, reconstruct='orig', copy=False, **params)
 
             if n_iter == 1 and len(chunk_flats):
-                logger.info('    %s Flat (%2d): %s'
-                            % (prefix, len(chunk_flats),
-                               ' '.join(chunk_flats)))
-                prefix = '    '
+                logger.info('            Flat (%2d): %s'
+                            % (len(chunk_flats), ' '.join(chunk_flats)))
             delta -= chunk_raw.get_data(these_picks)
             # p2p
             range_ = np.ptp(delta, axis=-1)
-            range_ *= params['coil_scale'][these_picks, 0]
+            cs_picks = np.searchsorted(params['meg_picks'], these_picks)
+            range_ *= params['coil_scale'][cs_picks, 0]
             mean, std = np.mean(range_), np.std(range_)
             # z score
             z = (range_ - mean) / std
@@ -2029,8 +2031,8 @@ def find_bad_channels_maxwell(
             if max_ < limit:
                 break
             name = raw.ch_names[these_picks[idx]]
-            logger.debug('    %s Bad:       %s %0.1f' % (prefix, name, max_))
-            prefix = '    '
+            logger.debug('            Bad:       %s %0.1f'
+                         % (name, max_))
             these_picks.pop(idx)
             chunk_noisy.append(name)
         noisy_chs.update(chunk_noisy)


Test patch:
diff --git a/mne/preprocessing/tests/test_maxwell.py b/mne/preprocessing/tests/test_maxwell.py
--- a/mne/preprocessing/tests/test_maxwell.py
+++ b/mne/preprocessing/tests/test_maxwell.py
@@ -18,7 +18,8 @@
 from mne.forward import _prep_meg_channels
 from mne.datasets import testing
 from mne.forward import use_coil_def
-from mne.io import read_raw_fif, read_info, read_raw_bti, read_raw_kit, BaseRaw
+from mne.io import (read_raw_fif, read_info, read_raw_bti, read_raw_kit,
+                    BaseRaw, read_raw_ctf)
 from mne.preprocessing.maxwell import (
     maxwell_filter, _get_n_moments, _sss_basis_basic, _sh_complex_to_real,
     _sh_real_to_complex, _sh_negate, _bases_complex_to_real, _trans_sss_basis,
@@ -89,6 +90,7 @@
 
 io_dir = op.join(op.dirname(__file__), '..', '..', 'io')
 fname_ctf_raw = op.join(io_dir, 'tests', 'data', 'test_ctf_comp_raw.fif')
+ctf_fname_continuous = op.join(data_path, 'CTF', 'testdata_ctf.ds')
 
 # In some of the tests, use identical coil defs to what is used in
 # MaxFilter
@@ -1059,17 +1061,42 @@ def test_mf_skips():
 
 
 @testing.requires_testing_data
-@pytest.mark.parametrize('bads, annot', [
-    ([], False),
-    (['MEG 0111'], True),  # just do 0111 to test picking, add annot to test it
+@pytest.mark.parametrize('fname, bads, annot, add_ch, ignore_ref, want_bads', [
+    # Neuromag data tested against MF
+    (sample_fname, [], False, False, False, ['MEG 2443']),
+    # add 0111 to test picking, add annot to test it, and prepend chs for idx
+    (sample_fname, ['MEG 0111'], True, True, False, ['MEG 2443']),
+    # CTF data seems to be sensitive to linalg lib (?) because some channels
+    # are very close to the limit, so we just check that one shows up
+    (ctf_fname_continuous, [], False, False, False, {'BR1-4304'}),
+    (ctf_fname_continuous, [], False, False, True, ['MLC24-4304']),  # faked
 ])
-def test_find_bad_channels_maxwell(bads, annot):
+def test_find_bad_channels_maxwell(fname, bads, annot, add_ch, ignore_ref,
+                                   want_bads):
     """Test automatic bad channel detection."""
-    raw = mne.io.read_raw_fif(sample_fname, allow_maxshield='yes')
-    raw.fix_mag_coil_types().load_data().pick_types(exclude=())
+    if fname.endswith('.ds'):
+        raw = read_raw_ctf(fname).load_data()
+        flat_idx = 33
+    else:
+        raw = read_raw_fif(fname)
+        raw.fix_mag_coil_types().load_data().pick_types(meg=True, exclude=())
+        flat_idx = 1
     raw.filter(None, 40)
     raw.info['bads'] = bads
-    raw._data[1] = 0  # MaxFilter didn't have this but doesn't affect results
+    raw._data[flat_idx] = 0  # MaxFilter didn't have this but doesn't affect it
+    want_flats = [raw.ch_names[flat_idx]]
+    raw.apply_gradient_compensation(0)
+    if add_ch:
+        raw_eeg = read_raw_fif(fname)
+        raw_eeg.pick_types(meg=False, eeg=True, exclude=()).load_data()
+        raw_eeg.info['lowpass'] = 40.
+        raw = raw_eeg.add_channels([raw])  # prepend the EEG channels
+        assert 0 in pick_types(raw.info, meg=False, eeg=True)
+    if ignore_ref:
+        # Fake a bad one, otherwise we don't find any
+        assert 42 in pick_types(raw.info, ref_meg=False)
+        assert raw.ch_names[42:43] == want_bads
+        raw._data[42] += np.random.RandomState(0).randn(len(raw.times))
     # maxfilter -autobad on -v -f test_raw.fif -force -cal off -ctc off -regularize off -list -o test_raw.fif -f ~/mne_data/MNE-testing-data/MEG/sample/sample_audvis_trunc_raw.fif  # noqa: E501
     if annot:
         # do a problematic one (gh-7741): exactly one "step" unit
@@ -1077,11 +1104,19 @@ def test_find_bad_channels_maxwell(bads, annot):
         dt = 1. / raw.info['sfreq']
         assert step == 1502
         raw.annotations.append(step * dt + raw._first_time, dt, 'BAD')
-    got_bads, got_flats = find_bad_channels_maxwell(
-        raw, origin=(0., 0., 0.04), regularize=None,
-        bad_condition='ignore', skip_by_annotation='BAD', verbose='debug')
-    assert got_bads == ['MEG 2443']  # from MaxFilter
-    assert got_flats == [raw.ch_names[1]]
+    with catch_logging() as log:
+        got_bads, got_flats = find_bad_channels_maxwell(
+            raw, origin=(0., 0., 0.04), regularize=None,
+            bad_condition='ignore', skip_by_annotation='BAD', verbose=True,
+            ignore_ref=ignore_ref)
+    if isinstance(want_bads, list):
+        assert got_bads == want_bads  # from MaxFilter
+    else:
+        assert want_bads.intersection(set(got_bads))
+    assert got_flats == want_flats
+    log = log.getvalue()
+    assert 'Interval   1:    0.00' in log
+    assert 'Interval   2:    5.00' in log
 
 
 run_tests_if_main()