Repository: mne-tools/mne-python

Issue:
[BUG] LCMV beamformer does not inform about channel discrepancies
According to :ref:`ex-source-loc-methods`, the DICS beamformer at least only accepts a single channel type at a time. Since the covariance can be computed for both meg and eeg, that can be passed to `apply_lcmv` but then the filter channel names only include the meg channels so I assume the eeg channels are tossed. It seems that there should at least be a warning but probably more appropriately a `NotImplementedError` when eeg channels are passed that they will not have corresponding filters returned. cc @britta-wstnr


Discussion:
Can you clarify/tell me if I understand correctly that it is the latter case: 
did `make_lcmv` just drop EEG channels, or did `apply_lcmv` drop them because your `filter` was done on MEG only?

If it is the latter case, then this behavior is expected and logical, but maybe we could still inform the user, by adding something like `Warning: dropping any channels that are not present in the filter`.
The first case where you pass in a covariance matrix with eeg also and you get out filters with only meg and no warning.

I would say warning about specific data channels not present in the filter even for apply makes sense to me.
Are you sure the info, noise covariance, data covariance, and forward were all meg+eeg? If any of them are incomplete it will restrict it
Ah you're right, the covariances actually were restricted before the beamformer, sorry!
Okay I would make this restriction a logger.info message rather than a warning
It was actually the forward model my bad! I think we could still log in beamformer that there was a channel mismatch.
I agree with @larsoner that this should be just an info. It's not a bug, but expected behavior - even though it might surprise the user :-) 
@britta-wstnr @larsoner I was wondering whether I could take care of this issue (found it through a link to easy ones from the contribution guide). 

I briefly looked at the code, and if I understood correctly, the function `mne.utils.check._check_info_inv` is taking care of the consistency between channels in the info, forward model, and the covariance matrices (both data and noise). Currently, the function `mne.utils.check._compare_ch_names` is called three times to remove channels that are not in `fwd['info']['ch_names']`, `data_cov.ch_names`, and `noise_cov.ch_names`. In between, MEG reference channels are also removed through a list comprehension. Should I log all removals separately including the number of channels removed, or just mention once that some channels were removed?
Hi @ctrltz! 
As far as I can see, this is indeed not implemented yet, so it would be great if you could take it on.
I think we should log this once, not each time channels get dropped. 

Resolving patch:
diff --git a/mne/utils/check.py b/mne/utils/check.py
--- a/mne/utils/check.py
+++ b/mne/utils/check.py
@@ -652,7 +652,8 @@ def _check_if_nan(data, msg=" to be plotted"):
         raise ValueError("Some of the values {} are NaN.".format(msg))
 
 
-def _check_info_inv(info, forward, data_cov=None, noise_cov=None):
+@verbose
+def _check_info_inv(info, forward, data_cov=None, noise_cov=None, verbose=None):
     """Return good channels common to forward model and covariance matrices."""
     from .._fiff.pick import pick_types
 
@@ -696,6 +697,19 @@ def _check_info_inv(info, forward, data_cov=None, noise_cov=None):
     if noise_cov is not None:
         ch_names = _compare_ch_names(ch_names, noise_cov.ch_names, noise_cov["bads"])
 
+    # inform about excluding any channels apart from bads and reference
+    all_bads = info["bads"] + ref_chs
+    if data_cov is not None:
+        all_bads += data_cov["bads"]
+    if noise_cov is not None:
+        all_bads += noise_cov["bads"]
+    dropped_nonbads = set(info["ch_names"]) - set(ch_names) - set(all_bads)
+    if dropped_nonbads:
+        logger.info(
+            f"Excluding {len(dropped_nonbads)} channel(s) missing from the "
+            "provided forward operator and/or covariance matrices"
+        )
+
     picks = [info["ch_names"].index(k) for k in ch_names if k in info["ch_names"]]
     return picks
 
@@ -750,7 +764,13 @@ def _check_one_ch_type(method, info, forward, data_cov=None, noise_cov=None):
         info_pick = info
     else:
         _validate_type(noise_cov, [None, Covariance], "noise_cov")
-        picks = _check_info_inv(info, forward, data_cov=data_cov, noise_cov=noise_cov)
+        picks = _check_info_inv(
+            info,
+            forward,
+            data_cov=data_cov,
+            noise_cov=noise_cov,
+            verbose=_verbose_safe_false(),
+        )
         info_pick = pick_info(info, picks)
     ch_types = [_contains_ch_type(info_pick, tt) for tt in ("mag", "grad", "eeg")]
     if sum(ch_types) > 1:


Test patch:
diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py
--- a/mne/utils/tests/test_check.py
+++ b/mne/utils/tests/test_check.py
@@ -30,6 +30,7 @@
     _safe_input,
     _suggest,
     _validate_type,
+    catch_logging,
     check_fname,
     check_random_state,
     check_version,
@@ -141,12 +142,12 @@ def test_check_info_inv():
     assert [1, 2] not in picks
     # covariance matrix
     data_cov_bads = data_cov.copy()
-    data_cov_bads["bads"] = data_cov_bads.ch_names[0]
+    data_cov_bads["bads"] = [data_cov_bads.ch_names[0]]
     picks = _check_info_inv(epochs.info, forward, data_cov=data_cov_bads)
     assert 0 not in picks
     # noise covariance matrix
     noise_cov_bads = noise_cov.copy()
-    noise_cov_bads["bads"] = noise_cov_bads.ch_names[1]
+    noise_cov_bads["bads"] = [noise_cov_bads.ch_names[1]]
     picks = _check_info_inv(epochs.info, forward, noise_cov=noise_cov_bads)
     assert 1 not in picks
 
@@ -164,10 +165,16 @@ def test_check_info_inv():
     noise_cov = pick_channels_cov(
         noise_cov, include=[noise_cov.ch_names[ii] for ii in range(7, 12)]
     )
-    picks = _check_info_inv(
-        epochs.info, forward, noise_cov=noise_cov, data_cov=data_cov
-    )
-    assert list(range(7, 10)) == picks
+    with catch_logging() as log:
+        picks = _check_info_inv(
+            epochs.info, forward, noise_cov=noise_cov, data_cov=data_cov, verbose=True
+        )
+        assert list(range(7, 10)) == picks
+
+    # make sure to inform the user that 7 channels were dropped
+    # (there are 10 channels in epochs but only 3 were picked)
+    log = log.getvalue()
+    assert "Excluding 7 channel(s) missing" in log
 
 
 def test_check_option():