diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e83c8db3dae..474cdf326aa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -114,7 +114,7 @@ stages: - bash: | set -e python -m pip install --progress-bar off --upgrade pip - python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git" "git+https://github.com/python-quantities/python-quantities" pyvista scikit-learn python-picard qtpy nibabel sphinx-gallery "PySide6!=6.8.0,!=6.8.0.1,!=6.8.1.1,!=6.9.1" pandas neo pymatreader antio defusedxml curryreader + python -m pip install --progress-bar off "mne-qt-browser[opengl] @ git+https://github.com/mne-tools/mne-qt-browser.git" pyvista scikit-learn python-picard qtpy nibabel sphinx-gallery "PySide6!=6.8.0,!=6.8.0.1,!=6.8.1.1,!=6.9.1" pandas neo pymatreader antio defusedxml curryreader python -m pip uninstall -yq mne python -m pip install --progress-bar off --upgrade -e . --group=test displayName: 'Install dependencies with pip' @@ -173,7 +173,7 @@ stages: python -m pip install --progress-bar off --upgrade pip python -m pip install --progress-bar off --upgrade --pre --only-binary=\"numpy,scipy,matplotlib,vtk\" numpy scipy matplotlib vtk python -c "import vtk" - python -m pip install --progress-bar off --upgrade -ve .[full] --group=test_extra "git+https://github.com/python-quantities/python-quantities" + python -m pip install --progress-bar off --upgrade -ve .[full] --group=test_extra displayName: 'Install dependencies with pip' - bash: | set -e diff --git a/doc/changes/dev/13595.bugfix.rst b/doc/changes/dev/13595.bugfix.rst new file mode 100644 index 00000000000..1eda0af925c --- /dev/null +++ b/doc/changes/dev/13595.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.viz.plot_evoked_white` did not accept a single "meg" rank value like those returned from :func:`mne.compute_rank`, by `Eric Larson`_. diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py index d5eec6e7f91..43cfc8fc11b 100644 --- a/mne/minimum_norm/tests/test_inverse.py +++ b/mne/minimum_norm/tests/test_inverse.py @@ -23,6 +23,7 @@ EvokedArray, SourceEstimate, combine_evoked, + compute_rank, compute_raw_covariance, convert_forward_solution, make_ad_hoc_cov, @@ -992,21 +993,34 @@ def test_make_inverse_operator_diag(evoked, noise_cov, tmp_path, azure_windows): def test_inverse_operator_noise_cov_rank(evoked, noise_cov): """Test MNE inverse operator with a specified noise cov rank.""" - fwd_op = read_forward_solution_meg(fname_fwd, surf_ori=True) - inv = make_inverse_operator(evoked.info, fwd_op, noise_cov, rank=dict(meg=64)) + fwd_op_meg = read_forward_solution_meg(fname_fwd, surf_ori=True) + inv = make_inverse_operator(evoked.info, fwd_op_meg, noise_cov, rank=dict(meg=64)) assert compute_rank_inverse(inv) == 64 - inv = make_inverse_operator(evoked.info, fwd_op, noise_cov, rank=dict(meg=64)) + inv = make_inverse_operator(evoked.info, fwd_op_meg, noise_cov, rank=dict(meg=64)) assert compute_rank_inverse(inv) == 64 bad_cov = noise_cov.copy() bad_cov["data"][0, 0] *= 1e12 with pytest.warns(RuntimeWarning, match="orders of magnitude"): - make_inverse_operator(evoked.info, fwd_op, bad_cov, rank=dict(meg=64)) + make_inverse_operator(evoked.info, fwd_op_meg, bad_cov, rank=dict(meg=64)) - fwd_op = read_forward_solution_eeg(fname_fwd, surf_ori=True) - inv = make_inverse_operator(evoked.info, fwd_op, noise_cov, rank=dict(eeg=20)) + fwd_op_eeg = read_forward_solution_eeg(fname_fwd, surf_ori=True) + inv = make_inverse_operator(evoked.info, fwd_op_eeg, noise_cov, rank=dict(eeg=20)) assert compute_rank_inverse(inv) == 20 + # with and without rank passed explicitly + inv_info = make_inverse_operator(evoked.info, fwd_op_meg, noise_cov, rank="info") + info_rank = 302 + assert compute_rank_inverse(inv_info) == info_rank + rank = compute_rank(noise_cov, info=evoked.copy().pick("meg").info, rank="info") + assert "meg" in rank + assert sum(rank.values()) == info_rank + inv_rank = make_inverse_operator(evoked.info, fwd_op_meg, noise_cov, rank=rank) + assert compute_rank_inverse(inv_rank) == info_rank + evoked_info = apply_inverse(evoked, inv_info, lambda2, "MNE") + evoked_rank = apply_inverse(evoked, inv_rank, lambda2, "MNE") + assert_allclose(evoked_rank.data, evoked_info.data) + def test_inverse_operator_volume(evoked, tmp_path): """Test MNE inverse computation on volume source space.""" diff --git a/mne/tests/test_cov.py b/mne/tests/test_cov.py index ad05f5d8b1f..682b1b472e3 100644 --- a/mne/tests/test_cov.py +++ b/mne/tests/test_cov.py @@ -94,6 +94,14 @@ def test_compute_whitener(proj, pca): assert pca is False assert_allclose(round_trip, np.eye(n_channels), atol=0.05) + # with and without rank + W_info, _ = compute_whitener(cov, raw.info, pca=pca, rank="info", verbose="error") + assert_allclose(W_info, W) + rank = compute_rank(raw, rank="info", proj=proj) + assert W.shape == (n_reduced, n_channels) + W_rank, _ = compute_whitener(cov, raw.info, pca=pca, rank=rank, verbose="error") + assert_allclose(W_rank, W) + 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"): diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index c12c1f0945e..a62d2379f03 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1587,13 +1587,9 @@ def plot_evoked_white( evoked.del_proj(idx) evoked.pick_types(ref_meg=False, exclude="bads", **_PICK_TYPES_DATA_DICT) - n_ch_used, rank_list, picks_list, has_sss = _triage_rank_sss( + n_ch_used, rank_list, picks_list, meg_combined = _triage_rank_sss( evoked.info, noise_cov, rank, scalings=None ) - if has_sss: - logger.info( - "SSS has been applied to data. Showing mag and grad whitening jointly." - ) # get one whitened evoked per cov evokeds_white = [ @@ -1663,8 +1659,8 @@ def whitened_gfp(x, rank=None): # hacks to get it to plot all channels in the same axes, namely setting # the channel unit (most important) and coil type (for consistency) of # all MEG channels to be the same. - meg_idx = sss_title = None - if has_sss: + meg_idx = combined_title = None + if meg_combined: titles_["meg"] = "MEG (combined)" meg_idx = [ pi for pi, (ch_type, _) in enumerate(picks_list) if ch_type == "meg" @@ -1675,7 +1671,7 @@ def whitened_gfp(x, rank=None): use = evokeds_white[0].info["chs"][picks[0]][key] for pick in picks: evokeds_white[0].info["chs"][pick][key] = use - sss_title = f"{titles_['meg']} ({len(picks)} channel{_pl(picks)})" + combined_title = f"{titles_['meg']} ({len(picks)} channel{_pl(picks)})" evokeds_white[0].plot( unit=False, axes=axes_evoked, @@ -1684,8 +1680,8 @@ def whitened_gfp(x, rank=None): time_unit=time_unit, spatial_colors=spatial_colors, ) - if has_sss: - axes_evoked[meg_idx].set(title=sss_title) + if meg_combined: + axes_evoked[meg_idx].set(title=combined_title) # Now plot the GFP for all covs if indicated. for evoked_white, noise_cov, rank_, color in iter_gfp: diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 964acae2b31..43df7886265 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -18,6 +18,7 @@ Epochs, compute_covariance, compute_proj_evoked, + compute_rank, make_fixed_length_events, read_cov, read_events, @@ -357,6 +358,21 @@ def test_plot_evoked_image(): evoked.plot_image(clim=[-4, 4]) +def test_plot_white_rank(): + """Test plot_white with a combined-MEG rank arg.""" + cov = read_cov(cov_fname) + cov["method"] = "empirical" + cov["projs"] = [] # avoid warnings + evoked = _get_epochs().average() + evoked.set_eeg_reference("average") # Avoid warnings + rank = compute_rank(evoked, "info") + assert "grad" not in rank + assert "mag" not in rank + assert "meg" in rank + evoked.plot_white(cov) + evoked.plot_white(cov, rank=rank) + + def test_plot_white(): """Test plot_white.""" cov = read_cov(cov_fname) @@ -373,9 +389,9 @@ def test_plot_white(): evoked.plot_white(cov, rank={"grad": 8}, time_unit="s", axes=fig.axes[:4]) with pytest.raises(ValueError, match=r"must have shape \(4,\), got \(2,"): evoked.plot_white(cov, axes=fig.axes[:2]) - with pytest.raises(ValueError, match="When not using SSS"): + with pytest.raises(ValueError, match="exceeds the number"): evoked.plot_white(cov, rank={"meg": 306}) - evoked.plot_white([cov, cov], time_unit="s") + evoked.plot_white([cov, cov], rank={"meg": 9}, time_unit="s") plt.close("all") fig = plot_evoked_white(evoked, [cov, cov]) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 9c71714040a..010f7eaf1b3 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2046,13 +2046,13 @@ def _setup_plot_projector(info, noise_cov, proj=True, use_noise_cov=True, nave=1 def _check_sss(info): """Check SSS history in info.""" ch_used = [ch for ch in _DATA_CH_TYPES_SPLIT if _contains_ch_type(info, ch)] - has_meg = "mag" in ch_used and "grad" in ch_used - has_sss = ( - has_meg + has_mag_and_grad = "mag" in ch_used and "grad" in ch_used + needs_meg_combined = ( + has_mag_and_grad and len(info["proc_history"]) > 0 and info["proc_history"][0].get("max_info") is not None ) - return ch_used, has_meg, has_sss + return ch_used, has_mag_and_grad, needs_meg_combined def _triage_rank_sss(info, covs, rank=None, scalings=None): @@ -2062,22 +2062,28 @@ def _triage_rank_sss(info, covs, rank=None, scalings=None): # Only look at good channels picks = _pick_data_channels(info, with_ref_meg=False, exclude="bads") info = pick_info(info, picks) - ch_used, has_meg, has_sss = _check_sss(info) - if has_sss: + ch_used, has_mag_and_grad, needs_meg_combined = _check_sss(info) + if needs_meg_combined: if "mag" in rank or "grad" in rank: raise ValueError( 'When using SSS, pass "meg" to set the rank ' '(separate rank values for "mag" or "grad" are ' "meaningless)." ) + meg_combined = True elif "meg" in rank: - raise ValueError( - "When not using SSS, pass separate rank values " - 'for "mag" and "grad" (do not use "meg").' - ) + if needs_meg_combined: + start = "SSS has been applied to data" + else: + start = "Got a single MEG rank value" + logger.info("%s. Showing mag and grad whitening jointly.", start) + meg_combined = True + else: + meg_combined = False + del needs_meg_combined - picks_list = _picks_by_type(info, meg_combined=has_sss) - if has_sss: + picks_list = _picks_by_type(info, meg_combined=meg_combined) + if meg_combined: # reduce ch_used to combined mag grad ch_used = list(zip(*picks_list))[0] # order pick list by ch_used (required for compat with plot_evoked) @@ -2088,7 +2094,7 @@ def _triage_rank_sss(info, covs, rank=None, scalings=None): picks_list2 = [k for k in picks_list] # add meg picks if needed. - if has_meg: + if has_mag_and_grad: # append ("meg", picks_meg) picks_list2 += _picks_by_type(info, meg_combined=True) @@ -2121,7 +2127,7 @@ def _triage_rank_sss(info, covs, rank=None, scalings=None): this_rank[ch_type] = rank[ch_type] rank_list.append(this_rank) - return n_ch_used, rank_list, picks_list, has_sss + return n_ch_used, rank_list, picks_list, meg_combined def _check_cov(noise_cov, info): diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh index ffe75afca44..f9a60080f4b 100755 --- a/tools/github_actions_dependencies.sh +++ b/tools/github_actions_dependencies.sh @@ -38,8 +38,6 @@ else EXTRAS="" fi echo "" -# until quantities releases... -STD_ARGS="$STD_ARGS git+https://github.com/python-quantities/python-quantities" echo "::group::Installing test dependencies using pip" set -x