From 3994270bdbb25db1e138dcfd5fe78afd166cb881 Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Tue, 28 Mar 2023 09:35:43 -0700 Subject: [PATCH 1/5] [BUG, MRG] Fix topomap extra plot generated, add util to check a range --- mne/utils/__init__.py | 2 +- mne/utils/check.py | 48 +++++++++++++++++++++++++++-------- mne/utils/tests/test_check.py | 17 ++++++++++--- mne/viz/topomap.py | 5 ++-- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/mne/utils/__init__.py b/mne/utils/__init__.py index 0d04c882783..2ceef298cae 100644 --- a/mne/utils/__init__.py +++ b/mne/utils/__init__.py @@ -11,7 +11,7 @@ _check_pandas_index_arguments, _check_event_id, _check_ch_locs, _check_compensation_grade, _check_if_nan, _is_numeric, _ensure_int, _check_preload, - _validate_type, _check_info_inv, + _validate_type, _check_range, _check_info_inv, _check_channels_spatial_filter, _check_one_ch_type, _check_rank, _check_option, _check_depth, _check_combine, _path_like, _check_src_normal, _check_stc_units, diff --git a/mne/utils/check.py b/mne/utils/check.py index cb4459e9e26..dd39526bd61 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """The check functions.""" # Authors: Alexandre Gramfort # @@ -15,7 +16,6 @@ import numpy as np from ..fixes import _median_complex, _compare_version -from .docs import deprecated from ._logging import (warn, logger, verbose, _record_warnings, _verbose_safe_false) @@ -55,7 +55,7 @@ def check_fname(fname, filetype, endings, endings_err=()): if len(endings_err) > 0 and not fname.endswith(endings_err): print_endings = ' or '.join([', '.join(endings_err[:-1]), endings_err[-1]]) - raise OSError('The filename (%s) for file type %s must end with %s' + raise IOError('The filename (%s) for file type %s must end with %s' % (fname, filetype, print_endings)) print_endings = ' or '.join([', '.join(endings[:-1]), endings[-1]]) if not fname.endswith(endings): @@ -233,13 +233,13 @@ def _check_fname( if must_exist: if need_dir: if not fname.is_dir(): - raise OSError( + raise IOError( f"Need a directory for {name} but found a file " f"at {fname}" ) else: if not fname.is_file(): - raise OSError( + raise IOError( f"Need a file for {name} but found a directory " f"at {fname}" ) @@ -468,7 +468,7 @@ def _is_numeric(n): return isinstance(n, numbers.Number) -class _IntLike: +class _IntLike(object): @classmethod def __instancecheck__(cls, other): try: @@ -483,7 +483,7 @@ def __instancecheck__(cls, other): path_like = (str, Path, os.PathLike) -class _Callable: +class _Callable(object): @classmethod def __instancecheck__(cls, other): return callable(other) @@ -551,6 +551,37 @@ def _validate_type(item, types=None, item_name=None, type_name=None, *, f"got {type(item)} instead.") +def _check_range(val, min_val, max_val, name, min_inclusive=True, + max_inclusive=True): + """Check that item is within range. + + Parameters + ---------- + val : int | float + The value to be checked. + min_val : int | float + The minimum value allowed. + max_val : int | float + The maximum value allowed. + name : str + The name of the value. + min_inclusive : bool + Whether ``val`` is allowed to be ``min_val``. + max_inclusive : bool + Whether ``val`` is allowed to be ``max_val``. + """ + below_min = val < min_val if min_inclusive else val <= min_val + above_max = val > max_val if max_inclusive else val >= max_val + if below_min or above_max: + error_str = f'The value of {name} must be between {min_val} ' + if min_inclusive: + error_str += 'inclusive ' + error_str += f'and {max_val}' + if max_inclusive: + error_str += 'inclusive ' + raise ValueError(error_str) + + def _path_like(item): """Validate that `item` is `path-like`. @@ -1088,11 +1119,6 @@ def _to_rgb(*args, name='color', alpha=False): f'{repr(args)}') from None -@deprecated('has_nibabel is deprecated and will be removed in 1.5') -def has_nibabel(): - return check_version('nibabel') # pragma: no cover - - def _import_nibabel(why='use MRI files'): try: import nibabel as nib diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 8f28ee7799a..9ae38f994b9 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -18,7 +18,8 @@ from mne.utils import (check_random_state, _check_fname, check_fname, _suggest, _check_subject, _check_info_inv, _check_option, Bunch, check_version, _path_like, _validate_type, _on_missing, - _safe_input, _check_ch_locs, _check_sphere) + _safe_input, _check_ch_locs, _check_sphere, + _check_range) data_path = testing.data_path(download=False) base_dir = data_path / "MEG" / "sample" @@ -48,7 +49,7 @@ def test_check(tmp_path): os.chmod(fname, orig_perms) os.remove(fname) assert not fname.is_file() - pytest.raises(OSError, check_fname, 'foo', 'tets-dip.x', (), ('.fif',)) + pytest.raises(IOError, check_fname, 'foo', 'tets-dip.x', (), ('.fif',)) pytest.raises(ValueError, _check_subject, None, None) pytest.raises(TypeError, _check_subject, None, 1) pytest.raises(TypeError, _check_subject, 1, None) @@ -56,7 +57,8 @@ def test_check(tmp_path): check_random_state(None).choice(1) check_random_state(0).choice(1) check_random_state(np.random.RandomState(0)).choice(1) - check_random_state(np.random.default_rng(0)).choice(1) + if check_version('numpy', '1.17'): + check_random_state(np.random.default_rng(0)).choice(1) @testing.requires_testing_data @@ -184,6 +186,15 @@ def test_validate_type(): _validate_type(False, 'int-like') +def test_check_range(): + """Test _check_range.""" + _check_range(10, 1, 100, 'value') + with pytest.raises(ValueError, match='must be between'): + _check_range(0, 1, 10, 'value') + with pytest.raises(ValueError, match='must be between'): + _check_range(1, 1, 10, 'value', False, False) + + @testing.requires_testing_data def test_suggest(): """Test suggestions.""" diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index be10dbe9502..89ba1cd2496 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -626,7 +626,7 @@ def _get_extra_points(pos, extrapolate, origin, radii): return new_pos, mask_pos, tri -class _GridData: +class _GridData(object): """Unstructured (x,y) data interpolator. This class allows optimized interpolation by computing parameters @@ -908,7 +908,6 @@ def _plot_topomap( border=_BORDER_DEFAULT, res=64, cmap=None, vmin=None, vmax=None, cnorm=None, show=True, onselect=None): from matplotlib.colors import Normalize - import matplotlib.pyplot as plt from matplotlib.widgets import RectangleSelector data = np.asarray(data) logger.debug(f'Plotting topomap for {ch_type} data shape {data.shape}') @@ -1050,7 +1049,7 @@ def _plot_topomap( verticalalignment='center', size='x-small') if not axes.figure.get_constrained_layout(): - plt.subplots_adjust(top=.95) + axes.figure.subplots_adjust(top=.95) if onselect is not None: lim = axes.dataLim From c6f4023429feb1e27c3d40ad3e470880b9eadb6d Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Tue, 28 Mar 2023 09:38:37 -0700 Subject: [PATCH 2/5] update latest --- doc/changes/latest.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 2d731c83364..dba8d159b15 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -64,6 +64,7 @@ Bugs - Fix :func:`mne.io.read_raw` for file names containing multiple dots (:gh:`11521` by `Clemens Brunner`_) - Fix bug in :func:`mne.export.export_raw` when exporting to EDF with a physical range set smaller than the data range (:gh:`11569` by `Mathieu Scheltienne`_) - Fix bug in :func:`mne.concatenate_raws` where two raws could not be merged if the order of the bad channel lists did not match (:gh:`11502` by `Moritz Gerster`_) +- Fix bug where :meth:`mne.Evoked.plot_topomap` opened an extra figure (:gh:`11607` by `Alex Rockhill`_) API changes From d7b95be8e22f83152cc28db9dd257b8b73d2f842 Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Tue, 28 Mar 2023 10:44:33 -0700 Subject: [PATCH 3/5] revert bad reversions --- mne/utils/check.py | 18 +++++++++++------- mne/utils/tests/test_check.py | 8 +++----- mne/viz/topomap.py | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mne/utils/check.py b/mne/utils/check.py index dd39526bd61..18a1c49dd1f 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """The check functions.""" # Authors: Alexandre Gramfort # @@ -16,6 +15,7 @@ import numpy as np from ..fixes import _median_complex, _compare_version +from .docs import deprecated from ._logging import (warn, logger, verbose, _record_warnings, _verbose_safe_false) @@ -55,7 +55,7 @@ def check_fname(fname, filetype, endings, endings_err=()): if len(endings_err) > 0 and not fname.endswith(endings_err): print_endings = ' or '.join([', '.join(endings_err[:-1]), endings_err[-1]]) - raise IOError('The filename (%s) for file type %s must end with %s' + raise OSError('The filename (%s) for file type %s must end with %s' % (fname, filetype, print_endings)) print_endings = ' or '.join([', '.join(endings[:-1]), endings[-1]]) if not fname.endswith(endings): @@ -233,13 +233,13 @@ def _check_fname( if must_exist: if need_dir: if not fname.is_dir(): - raise IOError( + raise OSError( f"Need a directory for {name} but found a file " f"at {fname}" ) else: if not fname.is_file(): - raise IOError( + raise OSError( f"Need a file for {name} but found a directory " f"at {fname}" ) @@ -468,7 +468,7 @@ def _is_numeric(n): return isinstance(n, numbers.Number) -class _IntLike(object): +class _IntLike: @classmethod def __instancecheck__(cls, other): try: @@ -483,7 +483,7 @@ def __instancecheck__(cls, other): path_like = (str, Path, os.PathLike) -class _Callable(object): +class _Callable: @classmethod def __instancecheck__(cls, other): return callable(other) @@ -554,7 +554,6 @@ def _validate_type(item, types=None, item_name=None, type_name=None, *, def _check_range(val, min_val, max_val, name, min_inclusive=True, max_inclusive=True): """Check that item is within range. - Parameters ---------- val : int | float @@ -1119,6 +1118,11 @@ def _to_rgb(*args, name='color', alpha=False): f'{repr(args)}') from None +@deprecated('has_nibabel is deprecated and will be removed in 1.5') +def has_nibabel(): + return check_version('nibabel') # pragma: no cover + + def _import_nibabel(why='use MRI files'): try: import nibabel as nib diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 9ae38f994b9..7a591a09988 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -18,8 +18,7 @@ from mne.utils import (check_random_state, _check_fname, check_fname, _suggest, _check_subject, _check_info_inv, _check_option, Bunch, check_version, _path_like, _validate_type, _on_missing, - _safe_input, _check_ch_locs, _check_sphere, - _check_range) + _safe_input, _check_ch_locs, _check_sphere) data_path = testing.data_path(download=False) base_dir = data_path / "MEG" / "sample" @@ -49,7 +48,7 @@ def test_check(tmp_path): os.chmod(fname, orig_perms) os.remove(fname) assert not fname.is_file() - pytest.raises(IOError, check_fname, 'foo', 'tets-dip.x', (), ('.fif',)) + pytest.raises(OSError, check_fname, 'foo', 'tets-dip.x', (), ('.fif',)) pytest.raises(ValueError, _check_subject, None, None) pytest.raises(TypeError, _check_subject, None, 1) pytest.raises(TypeError, _check_subject, 1, None) @@ -57,8 +56,7 @@ def test_check(tmp_path): check_random_state(None).choice(1) check_random_state(0).choice(1) check_random_state(np.random.RandomState(0)).choice(1) - if check_version('numpy', '1.17'): - check_random_state(np.random.default_rng(0)).choice(1) + check_random_state(np.random.default_rng(0)).choice(1) @testing.requires_testing_data diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 89ba1cd2496..0db592fe14c 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -626,7 +626,7 @@ def _get_extra_points(pos, extrapolate, origin, radii): return new_pos, mask_pos, tri -class _GridData(object): +class _GridData: """Unstructured (x,y) data interpolator. This class allows optimized interpolation by computing parameters From b76b99d9e09eed3d83acf75be78569182ada125d Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Tue, 28 Mar 2023 10:48:58 -0700 Subject: [PATCH 4/5] fix import --- mne/utils/tests/test_check.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py index 7a591a09988..44caa61ba10 100644 --- a/mne/utils/tests/test_check.py +++ b/mne/utils/tests/test_check.py @@ -18,7 +18,8 @@ from mne.utils import (check_random_state, _check_fname, check_fname, _suggest, _check_subject, _check_info_inv, _check_option, Bunch, check_version, _path_like, _validate_type, _on_missing, - _safe_input, _check_ch_locs, _check_sphere) + _safe_input, _check_ch_locs, _check_sphere, + _check_range) data_path = testing.data_path(download=False) base_dir = data_path / "MEG" / "sample" From 2a62f9ef1c062adce875605a58eb54f32cb3577a Mon Sep 17 00:00:00 2001 From: Alex Rockhill Date: Tue, 28 Mar 2023 10:55:47 -0700 Subject: [PATCH 5/5] copy-paste error --- mne/utils/check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/utils/check.py b/mne/utils/check.py index 18a1c49dd1f..780458264c9 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -554,6 +554,7 @@ def _validate_type(item, types=None, item_name=None, type_name=None, *, def _check_range(val, min_val, max_val, name, min_inclusive=True, max_inclusive=True): """Check that item is within range. + Parameters ---------- val : int | float