Repository: mne-tools/mne-python

Issue:
removed info = _simplify_info(info) from make_lcmv
it removes the the maxfilter processing_history from info which leads to incorrect rank detection with MaxFiltered FIFF MEG data.

Thanks for contributing a pull request! Please make sure you have read the
[contribution guidelines](https://mne.tools/dev/install/contributing.html)
before submitting.

Please be aware that we are a loose team of volunteers so patience is
necessary. Assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.

Again, thanks for contributing!

#### Reference issue
Example: Fixes #1234.


#### What does this implement/fix?
Explain your changes.


#### Additional information
Any additional information you think is important.



Discussion:
Hello! 👋 Thanks for opening your first pull request here! ❤️ We will try to get back to you soon. 🚴🏽‍♂️

@britta-wstnr do you mind having a look?
cross-ref to forum post: https://mne.discourse.group/t/problem-with-correct-rank-detection-in-make-lcmv-due-to-simplify-info/6138
The `_simplify_info` is often there for speed or storage reasons. If we removed it for these, then I think we can do the removal *after* rank computation and things should be okay. IIRC the rank should end up being stored in the returned filters so we shouldn't need the MF information anymore...
Nice catch! Yeah, just moving the `_simplify_info()` down until after the rank computations sounds very reasonable to me.
@larsoner @wmvanvliet 
Yes, the rank is stored in the computed filter. _simplify_info_ is good to speed up and space saving in other routines; however,  when I test with full and simplified info I hardly any difference in time (< 1s) in filter computation. I quickly tried to dig down into the code and realized the no major step is iterated over all the entries of info. So removal or droping down of _simplify_info()_ after rank computation sould equally work.  

> I quickly tried to dig down into the code and realized the no major step is iterated over all the entries of info.

It probably looks this way, but you might be surprised. For example, when writing to disk, all entries will be iterated over. It also happens any time there is a deepcopy operation, which happens more often than you might think.

Those simplify-info calls tended to be added only when necessary, so I'd really rather not remove them unless absolutely necessary. It's very likely (almost guaranteed) to cause a performance regression of some sort.
Hi @neurosignal, thanks for raising this!
It weirdly looks like our maxfiltered data test now __breaks__:
```RuntimeWarning: Something went wrong in the data-driven estimation of the data rank as it exceeds the theoretical rank from the info (102 > 71). Consider setting rank to "auto" or setting it explicitly as an integer.```
Can you have a look at that? In the LCMV tests, we do test all rank options on maxfiltered data, as far as I can see: 
https://github.com/mne-tools/mne-python/blob/main/mne/beamformer/tests/test_lcmv.py#L789 (this is also the test that fails)
Hi @britta-wstnr 
Sorry I missed your msg. 
BTW, just after I noticed the problem, I tested it by exclusively giving the rank from maxfilter history as _rank=raw.info['proc_history'][0]['max_info']['sss_info']['nfree']_, but the rank argument in _make_lcmv_ nowadays does not accept an integer, which earlier used to do so. However, the recent versions accept it as a dictionary, and one has to give it as  _rank=dict(meg=raw.info['proc_history'][0]['max_info']['sss_info']['nfree'])_, and this way it works correctly. However, most of the users do not use it like this; they rather use the default one, i.e., _info_ which is supposed to pull the rank correctly. And I think that was the idea behind introducing the _info_ argument sometimes after v0.18. However, the simplified information lost the history and did not serve the intended purpose.
 
I'll recheck the test you have pointed here and let you know very soon.  Thanks.
Chiming in from https://mne.discourse.group/t/unexpectedly-high-rank-reported-in-make-lcmv/6714 (thanks @hoechenberger!). FWIW the test @britta-wstnr linked above currently runs fine for me without error.

One option would be to make `_simplify_info` less simple, adding some basic part of the SSS processing history. This might be overkill if `make_lcmv` is the only use case for the additional information. Another would be to do as @neurosignal implies: test for the presence of the SSS processing history, and if it exists, run `compute_rank` with something like rank={'meg': info['proc_history'][-1]['max_info']['sss_info']['nfree'] - len(data_cov['projs'])}.  For example: 
```
if 'proc_history' in info:
     rank = {'meg': info['proc_history'][-1]['max_info']['sss_info']['nfree'] - len(data_cov['projs'])}
```
as a hack to my production copy of `_lcmv.py`, right before `_simplify_info`, results in the expected rank. However I do still get a warning about a non positive semidefinite data covariance matrix.  My data doesn't have EEG so this is relatively simple.

> https://github.com/openmeeg/openmeeg/issues/577

Sure with a default of remove_proc_histoty=True that we set to False in beamforming code would work

> Another would be to do as @neurosignal implies: test for the presence of the SSS processing history

This should already happen if the history is there and you do rank='info'
> > [openmeeg/openmeeg#577](https://github.com/openmeeg/openmeeg/issues/577)
> 
> Sure with a default of remove_proc_histoty=True that we set to False in beamforming code would work

I like this better than the other solution.

> > Another would be to do as @neurosignal implies: test for the presence of the SSS processing history
> 
> This should already happen if the history is there and you do rank='info'

It would, but in `make_lcmv`  `_simplify_info` is called before `compute_rank`.
> https://github.com/openmeeg/openmeeg/issues/577

Heh, copy-paste error by me here :) I meant to say that your solution of modifying `_simplify_info` would work...

@neurosignal do you want to try modifying `_simplify_info(..., *, remove_proc_history=True)` but using `remove_proc_history=False` in `make_lcmv`?

Resolving patch:
diff --git a/mne/beamformer/_lcmv.py b/mne/beamformer/_lcmv.py
--- a/mne/beamformer/_lcmv.py
+++ b/mne/beamformer/_lcmv.py
@@ -163,7 +163,7 @@ def make_lcmv(
     .. footbibliography::
     """
     # check number of sensor types present in the data and ensure a noise cov
-    info = _simplify_info(info)
+    info = _simplify_info(info, keep=("proc_history",))
     noise_cov, _, allow_mismatch = _check_one_ch_type(
         "lcmv", info, forward, data_cov, noise_cov
     )
diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py
--- a/mne/io/meas_info.py
+++ b/mne/io/meas_info.py
@@ -1468,19 +1468,16 @@ def save(self, fname):
         write_info(fname, self)
 
 
-def _simplify_info(info):
+def _simplify_info(info, *, keep=()):
     """Return a simplified info structure to speed up picking."""
     chs = [
         {key: ch[key] for key in ("ch_name", "kind", "unit", "coil_type", "loc", "cal")}
         for ch in info["chs"]
     ]
-    sub_info = Info(
-        chs=chs,
-        bads=info["bads"],
-        comps=info["comps"],
-        projs=info["projs"],
-        custom_ref_applied=info["custom_ref_applied"],
-    )
+    keys = ("bads", "comps", "projs", "custom_ref_applied") + keep
+    sub_info = Info((key, info[key]) for key in keys if key in info)
+    with sub_info._unlock():
+        sub_info["chs"] = chs
     sub_info._update_redundant()
     return sub_info
 
diff --git a/mne/utils/config.py b/mne/utils/config.py
--- a/mne/utils/config.py
+++ b/mne/utils/config.py
@@ -202,9 +202,10 @@ def set_memmap_min_size(memmap_min_size):
 
 # These allow for partial matches, e.g. 'MNE_STIM_CHANNEL_1' is okay key
 _known_config_wildcards = (
-    "MNE_STIM_CHANNEL",
-    "MNE_DATASETS_FNIRS",
-    "MNE_NIRS",
+    "MNE_STIM_CHANNEL",  # can have multiple stim channels
+    "MNE_DATASETS_FNIRS",  # mne-nirs
+    "MNE_NIRS",  # mne-nirs
+    "MNE_KIT2FIFF",  # mne-kit-gui
 )
 
 


Test patch:
diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py
--- a/mne/beamformer/tests/test_lcmv.py
+++ b/mne/beamformer/tests/test_lcmv.py
@@ -1,3 +1,4 @@
+from contextlib import nullcontext
 from copy import deepcopy
 from inspect import signature
 
@@ -1072,9 +1073,9 @@ def test_depth_does_not_matter(bias_params_free, weight_norm, pick_ori):
         assert_allclose(d1, d2, atol=atol)
 
 
-@testing.requires_testing_data
-def test_lcmv_maxfiltered():
-    """Test LCMV on maxfiltered data."""
+@pytest.fixture(scope="session")
+def mf_data():
+    """Produce Maxwell filtered data for beamforming."""
     raw = mne.io.read_raw_fif(fname_raw).fix_mag_coil_types()
     raw_sss = mne.preprocessing.maxwell_filter(raw)
     events = mne.find_events(raw_sss)
@@ -1084,10 +1085,26 @@ def test_lcmv_maxfiltered():
     epochs = mne.Epochs(raw_sss, events)
     data_cov = mne.compute_covariance(epochs, tmin=0)
     fwd = mne.read_forward_solution(fname_fwd)
+    return epochs, data_cov, fwd
+
+
+@testing.requires_testing_data
+@pytest.mark.parametrize("use_rank", ("info", "computed", "full", None))
+def test_lcmv_maxfiltered(mf_data, use_rank):
+    """Test LCMV on maxfiltered data."""
+    epochs, data_cov, fwd = mf_data
     rank = compute_rank(data_cov, info=epochs.info)
     assert rank == {"mag": 71}
-    for use_rank in ("info", rank, "full", None):
-        make_lcmv(epochs.info, fwd, data_cov, rank=use_rank)
+    ctx = nullcontext()
+    if use_rank == "computed":
+        use_rank = rank
+    elif use_rank is None:
+        ctx = pytest.warns(RuntimeWarning, match="rank as it exceeds")
+    with catch_logging() as log, ctx:
+        make_lcmv(epochs.info, fwd, data_cov, rank=use_rank, verbose=True)
+    log = log.getvalue()
+    n = 102 if use_rank == "full" else 71
+    assert f"Making LCMV beamformer with rank {{'mag': {n}}}" in log
 
 
 # To reduce test time, only test combinations that should matter rather than