diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 89843fb5ab6..07987b54e59 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -66,3 +66,7 @@ Bugs API changes ~~~~~~~~~~~ - In :func:`mne.time_frequency.dpss_windows`, interpolating is deprecated (nowadays SciPy's computations are fast enough for large ``N`` without interpolation). This affects parameters ``interp_from`` and ``interp_kind``. Two new parameters of the underlying SciPy :func:`~scipy.signal.windows.dpss` function are also exposed: ``sym`` (for choosing symmetric vs. periodic windows) and ``norm`` (window normalization method). (:gh:`11293` by `Daniel McCloy`_) +- In :meth:`mne.decoding.CSP.plot_patterns`, :meth:`mne.decoding.CSP.plot_filters`, :meth:`mne.preprocessing.ICA.plot_components`, and :func:`mne.viz.plot_ica_components`, the parameters ``vmin`` and ``vmax`` are deprecated in favor of ``vlim``, for consistency with other topomap-plotting functions and methods (:gh:`11371` by `Daniel McCloy`_) +- In :meth:`mne.decoding.CSP.plot_patterns` and :meth:`mne.decoding.CSP.plot_filters` the ``title`` parameter is deprecated and will be removed in version 1.4 (:gh:`11371` by `Daniel McCloy`_) +- The APIs of :meth:`mne.preprocessing.ICA.plot_components` and :func:`mne.viz.plot_ica_components` gained new parameters ``show_names``, ``extrapolate``, ``border``, ``size``, ``cnorm``, ``cbar_fmt``, ``axes``, ``nrows``, ``ncols``, for consistency with other topomap-plotting functions and methods (:gh:`11371` by `Daniel McCloy`_) +- The APIs of :meth:`mne.decoding.CSP.plot_patterns` and :meth:`mne.decoding.CSP.plot_filters` gained new parameters ``extrapolate``, ``border``, ``cnorm``, ``axes``, ``nrows``, ``ncols``, for consistency with other topomap-plotting functions and methods (:gh:`11371` by `Daniel McCloy`_) diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index 4807d8325b6..7d5c56898bf 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -14,9 +14,16 @@ from .base import BaseEstimator from .mixin import TransformerMixin from ..cov import _regularized_covariance -from ..defaults import _INTERPOLATION_DEFAULT +from ..defaults import (_BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, + _INTERPOLATION_DEFAULT) from ..fixes import pinv -from ..utils import fill_doc, _check_option, _validate_type, copy_doc +from ..utils import fill_doc, _check_option, _validate_type, copy_doc, warn +from ..viz.utils import _warn_deprecated_vmin_vmax + +# TODO ↓↓↓↓↓ remove after 1.3 release +_TITLE_WARNING_MSG = ( + 'The "title" parameter is deprecated and will be removed in version 1.4. ' + 'Use "fig.suptitle()" instead.') @fill_doc @@ -237,14 +244,16 @@ def fit_transform(self, X, y, **fit_params): # noqa: D102 return super().fit_transform(X, y=y, **fit_params) @fill_doc - def plot_patterns(self, info, components=None, ch_type=None, - vmin=None, vmax=None, cmap='RdBu_r', sensors=True, - colorbar=True, scalings=None, units='a.u.', res=64, - size=1, cbar_fmt='%3.1f', name_format='CSP%01d', - show=True, show_names=False, title=None, mask=None, - mask_params=None, outlines='head', contours=6, - image_interp=_INTERPOLATION_DEFAULT, average=None, - sphere=None): + def plot_patterns( + self, info, components=None, *, average=None, ch_type=None, + scalings=None, sensors=True, show_names=False, mask=None, + mask_params=None, contours=6, outlines='head', sphere=None, + image_interp=_INTERPOLATION_DEFAULT, + extrapolate=_EXTRAPOLATE_DEFAULT, border=_BORDER_DEFAULT, res=64, + size=1, cmap='RdBu_r', vlim=(None, None), vmin=None, vmax=None, + cnorm=None, colorbar=True, cbar_fmt='%3.1f', units=None, + axes=None, name_format='CSP%01d', title=None, nrows=1, + ncols='auto', show=True): """Plot topographic patterns of components. The patterns explain how the measured data was generated from the @@ -255,80 +264,56 @@ def plot_patterns(self, info, components=None, ch_type=None, %(info_not_none)s Used for fitting. If not available, consider using :func:`mne.create_info`. components : float | array of float | None - The patterns to plot. If None, n_components will be shown. - ch_type : 'mag' | 'grad' | 'planar1' | 'planar2' | 'eeg' | None - The channel type to plot. For 'grad', the gradiometers are - collected in pairs and the RMS for each pair is plotted. - If None, then first available channel type from order given - above is used. Defaults to None. - vmin : float | callable - The value specifying the lower bound of the color range. - If None, and vmax is None, -vmax is used. Else np.min(data). - If callable, the output equals vmin(data). - vmax : float | callable - The value specifying the upper bound of the color range. - If None, the maximum absolute value is used. If vmin is None, - but vmax is not, default np.min(data). - If callable, the output equals vmax(data). - cmap : matplotlib colormap | (colormap, bool) | 'interactive' | None - Colormap to use. If tuple, the first value indicates the colormap - to use and the second value is a boolean defining interactivity. In - interactive mode the colors are adjustable by clicking and dragging - the colorbar with left and right mouse button. Left mouse button - moves the scale up and down and right mouse button adjusts the - range. Hitting space bar resets the range. Up and down arrows can - be used to change the colormap. If None, 'Reds' is used for all - positive data, otherwise defaults to 'RdBu_r'. If 'interactive', - translates to (None, True). Defaults to 'RdBu_r'. - - .. warning:: Interactive mode works smoothly only for a small - amount of topomaps. - sensors : bool | str - Add markers for sensor locations to the plot. Accepts matplotlib - plot format string (e.g., 'r+' for red plusses). If True, - a circle will be used (via .add_artist). Defaults to True. - colorbar : bool - Plot a colorbar. + The patterns to plot. If ``None``, all components will be shown. + %(average_plot_evoked_topomap)s + %(ch_type_topomap)s scalings : dict | float | None The scalings of the channel types to be applied for plotting. If None, defaults to ``dict(eeg=1e6, grad=1e13, mag=1e15)``. - units : dict | str | None - The unit of the channel type used for colorbar label. If - scale is None the unit is automatically determined. - res : int - The resolution of the topomap image (n pixels along each side). - size : float - Side length per topomap in inches. - cbar_fmt : str - String format for colorbar values. - name_format : str - String format for topomap values. Defaults to "CSP%%01d". - show : bool - Show figure if True. - show_names : bool | callable - If True, show channel names on top of the map. If a callable is - passed, channel names will be formatted using the callable; e.g., - to delete the prefix 'MEG ' from all channel names, pass the - function lambda x: x.replace('MEG ', ''). If ``mask`` is not None, - only significant sensors will be shown. - title : str | None - Title. If None (default), no title is displayed. + %(sensors_topomap)s + %(show_names_topomap)s %(mask_patterns_topomap)s %(mask_params_topomap)s + %(contours_topomap)s %(outlines_topomap)s - contours : int | array of float - The number of contour lines to draw. If 0, no contours will be - drawn. When an integer, matplotlib ticker locator is used to find - suitable values for the contour thresholds (may sometimes be - inaccurate, use array for accuracy). If an array, the values - represent the levels for the contours. Defaults to 6. - %(image_interp_topomap)s - average : float | None - The time window around a given time to be used for averaging - (seconds). For example, 0.01 would translate into window that - starts 5 ms before and ends 5 ms after a given time point. - Defaults to None, which means no averaging. %(sphere_topomap_auto)s + %(image_interp_topomap)s + %(extrapolate_topomap)s + + .. versionadded:: 1.3 + %(border_topomap)s + + .. versionadded:: 1.3 + %(res_topomap)s + %(size_topomap)s + %(cmap_topomap)s + %(vlim_plot_topomap)s + + .. versionadded:: 1.3 + %(vmin_vmax_topomap)s + + .. deprecated:: v1.4 + The ``vmin`` and ``vmax`` parameters will be removed in version + 1.4. Please use the ``vlim`` parameter instead. + %(cnorm)s + + .. versionadded:: 1.3 + %(colorbar_topomap)s + %(cbar_fmt_topomap)s + %(units_topomap)s + %(axes_evoked_plot_topomap)s + name_format : str + String format for topomap values. Defaults to "CSP%%01d". + %(title_none)s + + .. deprecated:: v1.4 + The ``title`` parameter will be removed in version 1.4. Please + use :meth:`fig.suptitle()` + instead. + %(nrows_ncols_topomap)s + + .. versionadded:: 1.3 + %(show)s Returns ------- @@ -336,6 +321,11 @@ def plot_patterns(self, info, components=None, ch_type=None, The figure. """ from .. import EvokedArray + + vlim = _warn_deprecated_vmin_vmax(vlim, vmin, vmax, '1.4') + + if units is None: + units = 'AU' if components is None: components = np.arange(self.n_components) @@ -347,26 +337,31 @@ def plot_patterns(self, info, components=None, ch_type=None, patterns = EvokedArray(self.patterns_.T, info, tmin=0) # the call plot_topomap fig = patterns.plot_topomap( - times=components, ch_type=ch_type, - vlim=(vmin, vmax), cmap=cmap, colorbar=colorbar, res=res, - cbar_fmt=cbar_fmt, sensors=sensors, - scalings=scalings, units=units, time_unit='s', - time_format=name_format, size=size, show_names=show_names, - mask_params=mask_params, mask=mask, outlines=outlines, - contours=contours, image_interp=image_interp, show=show, - average=average, sphere=sphere) + times=components, average=average, ch_type=ch_type, + scalings=scalings, sensors=sensors, show_names=show_names, + mask=mask, mask_params=mask_params, contours=contours, + outlines=outlines, sphere=sphere, image_interp=image_interp, + extrapolate=extrapolate, border=border, res=res, size=size, + cmap=cmap, vlim=vlim, cnorm=cnorm, colorbar=colorbar, + cbar_fmt=cbar_fmt, units=units, axes=axes, time_format=name_format, + nrows=nrows, ncols=ncols, show=show) + if title is not None: + warn(_TITLE_WARNING_MSG, FutureWarning) fig.suptitle(title) return fig @fill_doc - def plot_filters(self, info, components=None, ch_type=None, - vmin=None, vmax=None, cmap='RdBu_r', sensors=True, - colorbar=True, scalings=None, units='a.u.', res=64, - size=1, cbar_fmt='%3.1f', name_format='CSP%01d', - show=True, show_names=False, title=None, mask=None, - mask_params=None, outlines='head', contours=6, - image_interp=_INTERPOLATION_DEFAULT, average=None): + def plot_filters( + self, info, components=None, *, average=None, ch_type=None, + scalings=None, sensors=True, show_names=False, mask=None, + mask_params=None, contours=6, outlines='head', sphere=None, + image_interp=_INTERPOLATION_DEFAULT, + extrapolate=_EXTRAPOLATE_DEFAULT, border=_BORDER_DEFAULT, res=64, + size=1, cmap='RdBu_r', vlim=(None, None), vmin=None, vmax=None, + cnorm=None, colorbar=True, cbar_fmt='%3.1f', units=None, + axes=None, name_format='CSP%01d', title=None, nrows=1, + ncols='auto', show=True): """Plot topographic filters of components. The filters are used to extract discriminant neural sources from @@ -377,86 +372,56 @@ def plot_filters(self, info, components=None, ch_type=None, %(info_not_none)s Used for fitting. If not available, consider using :func:`mne.create_info`. components : float | array of float | None - The patterns to plot. If None, n_components will be shown. - ch_type : 'mag' | 'grad' | 'planar1' | 'planar2' | 'eeg' | None - The channel type to plot. For 'grad', the gradiometers are - collected in pairs and the RMS for each pair is plotted. - If None, then first available channel type from order given - above is used. Defaults to None. - vmin : float | callable - The value specifying the lower bound of the color range. - If None, and vmax is None, -vmax is used. Else np.min(data). - If callable, the output equals vmin(data). - vmax : float | callable - The value specifying the upper bound of the color range. - If None, the maximum absolute value is used. If vmin is None, - but vmax is not, defaults to np.min(data). - If callable, the output equals vmax(data). - cmap : matplotlib colormap | (colormap, bool) | 'interactive' | None - Colormap to use. If tuple, the first value indicates the colormap - to use and the second value is a boolean defining interactivity. In - interactive mode the colors are adjustable by clicking and dragging - the colorbar with left and right mouse button. Left mouse button - moves the scale up and down and right mouse button adjusts the - range. Hitting space bar resets the range. Up and down arrows can - be used to change the colormap. If None, 'Reds' is used for all - positive data, otherwise defaults to 'RdBu_r'. If 'interactive', - translates to (None, True). Defaults to 'RdBu_r'. - - .. warning:: Interactive mode works smoothly only for a small - amount of topomaps. - sensors : bool | str - Add markers for sensor locations to the plot. Accepts matplotlib - plot format string (e.g., 'r+' for red plusses). If True, - a circle will be used (via .add_artist). Defaults to True. - colorbar : bool - Plot a colorbar. + The patterns to plot. If ``None``, all components will be shown. + %(average_plot_evoked_topomap)s + %(ch_type_topomap)s scalings : dict | float | None The scalings of the channel types to be applied for plotting. If None, defaults to ``dict(eeg=1e6, grad=1e13, mag=1e15)``. - units : dict | str | None - The unit of the channel type used for colorbar label. If - scale is None the unit is automatically determined. - res : int - The resolution of the topomap image (n pixels along each side). - size : float - Side length per topomap in inches. - cbar_fmt : str - String format for colorbar values. - name_format : str - String format for topomap values. Defaults to "CSP%%01d". - show : bool - Show figure if True. - show_names : bool | callable - If True, show channel names on top of the map. If a callable is - passed, channel names will be formatted using the callable; e.g., - to delete the prefix 'MEG ' from all channel names, pass the - function lambda x: x.replace('MEG ', ''). If ``mask`` is not None, - only significant sensors will be shown. - title : str | None - Title. If None (default), no title is displayed. - mask : ndarray of bool, shape (n_channels, n_times) | None - The channels to be marked as significant at a given time point. - Indices set to `True` will be considered. Defaults to None. - mask_params : dict | None - Additional plotting parameters for plotting significant sensors. - Default (None) equals:: - - dict(marker='o', markerfacecolor='w', markeredgecolor='k', - linewidth=0, markersize=4) + %(sensors_topomap)s + %(show_names_topomap)s + %(mask_patterns_topomap)s + %(mask_params_topomap)s + %(contours_topomap)s %(outlines_topomap)s - contours : int | array of float - The number of contour lines to draw. If 0, no contours will be - drawn. When an integer, matplotlib ticker locator is used to find - suitable values for the contour thresholds (may sometimes be - inaccurate, use array for accuracy). If an array, the values - represent the levels for the contours. Defaults to 6. + %(sphere_topomap_auto)s %(image_interp_topomap)s - average : float | None - The time window around a given time to be used for averaging - (seconds). For example, 0.01 would translate into window that - starts 5 ms before and ends 5 ms after a given time point. - Defaults to None, which means no averaging. + %(extrapolate_topomap)s + + .. versionadded:: 1.3 + %(border_topomap)s + + .. versionadded:: 1.3 + %(res_topomap)s + %(size_topomap)s + %(cmap_topomap)s + %(vlim_plot_topomap_psd)s + + .. versionadded:: 1.3 + %(vmin_vmax_topomap)s + + .. deprecated:: v1.4 + The ``vmin`` and ``vmax`` parameters will be removed in version + 1.4. Please use the ``vlim`` parameter instead. + %(cnorm)s + + .. versionadded:: 1.3 + %(colorbar_topomap)s + %(cbar_fmt_topomap)s + %(units_topomap)s + %(axes_evoked_plot_topomap)s + name_format : str + String format for topomap values. Defaults to "CSP%%01d". + %(title_none)s + + .. deprecated:: v1.4 + The ``title`` parameter will be removed in version 1.4. Please + use :meth:`fig.suptitle()` + instead. + %(nrows_ncols_topomap)s + + .. versionadded:: 1.3 + %(show)s Returns ------- @@ -464,6 +429,11 @@ def plot_filters(self, info, components=None, ch_type=None, The figure. """ from .. import EvokedArray + + vlim = _warn_deprecated_vmin_vmax(vlim, vmin, vmax, '1.4') + + if units is None: + units = 'AU' if components is None: components = np.arange(self.n_components) @@ -475,14 +445,16 @@ def plot_filters(self, info, components=None, ch_type=None, filters = EvokedArray(self.filters_.T, info, tmin=0) # the call plot_topomap fig = filters.plot_topomap( - times=components, ch_type=ch_type, vlim=(vmin, vmax), - cmap=cmap, colorbar=colorbar, res=res, - cbar_fmt=cbar_fmt, sensors=sensors, scalings=scalings, units=units, - time_unit='s', time_format=name_format, size=size, - show_names=show_names, mask_params=mask_params, - mask=mask, outlines=outlines, contours=contours, - image_interp=image_interp, show=show, average=average) + times=components, average=average, ch_type=ch_type, + scalings=scalings, sensors=sensors, show_names=show_names, + mask=mask, mask_params=mask_params, contours=contours, + outlines=outlines, sphere=sphere, image_interp=image_interp, + extrapolate=extrapolate, border=border, res=res, size=size, + cmap=cmap, vlim=vlim, cnorm=cnorm, colorbar=colorbar, + cbar_fmt=cbar_fmt, units=units, axes=axes, time_format=name_format, + nrows=nrows, ncols=ncols, show=show) if title is not None: + warn(_TITLE_WARNING_MSG, FutureWarning) fig.suptitle(title) return fig diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py index ece75d41173..69b4fbedcc9 100644 --- a/mne/preprocessing/ica.py +++ b/mne/preprocessing/ica.py @@ -2182,24 +2182,25 @@ def copy(self): return deepcopy(self) @copy_function_doc_to_method_doc(plot_ica_components) - def plot_components(self, picks=None, ch_type=None, res=64, - vmin=None, vmax=None, cmap='RdBu_r', sensors=True, - colorbar=False, title=None, show=True, outlines='head', - contours=6, image_interp=_INTERPOLATION_DEFAULT, - inst=None, plot_std=True, - topomap_args=None, image_args=None, psd_args=None, - reject='auto', sphere=None, verbose=None): - return plot_ica_components(self, picks=picks, ch_type=ch_type, - res=res, vmin=vmin, - vmax=vmax, cmap=cmap, sensors=sensors, - colorbar=colorbar, title=title, show=show, - outlines=outlines, contours=contours, - image_interp=image_interp, - inst=inst, plot_std=plot_std, - topomap_args=topomap_args, - image_args=image_args, psd_args=psd_args, - reject=reject, sphere=sphere, - verbose=verbose) + def plot_components( + self, picks=None, ch_type=None, *, inst=None, plot_std=True, + reject='auto', sensors=True, show_names=False, contours=6, + outlines='head', sphere=None, image_interp=_INTERPOLATION_DEFAULT, + extrapolate=_EXTRAPOLATE_DEFAULT, border=_BORDER_DEFAULT, res=64, + size=1, cmap='RdBu_r', vlim=(None, None), vmin=None, vmax=None, + cnorm=None, colorbar=False, cbar_fmt='%3.2f', axes=None, + title=None, nrows='auto', ncols='auto', show=True, + topomap_args=None, image_args=None, psd_args=None, verbose=None): + return plot_ica_components( + self, picks=picks, ch_type=ch_type, inst=inst, plot_std=plot_std, + reject=reject, sensors=sensors, show_names=show_names, + contours=contours, outlines=outlines, sphere=sphere, + image_interp=image_interp, extrapolate=extrapolate, border=border, + res=res, size=size, cmap=cmap, vlim=vlim, vmin=vmin, vmax=vmax, + cnorm=cnorm, colorbar=colorbar, cbar_fmt=cbar_fmt, axes=axes, + title=title, nrows=nrows, ncols=ncols, show=show, + topomap_args=topomap_args, image_args=image_args, + psd_args=psd_args, verbose=verbose) @copy_function_doc_to_method_doc(plot_ica_properties) def plot_properties(self, inst, picks=None, axes=None, dB=True, @@ -2893,7 +2894,7 @@ def corrmap(icas, template, threshold="auto", label=None, ch_type="eeg", *, template_fig = icas[template[0]].plot_components( picks=template[1], ch_type=ch_type, title=ttl, outlines=outlines, cmap=cmap, contours=contours, - show=show, topomap_args=dict(sphere=sphere)) + show=show, sphere=sphere) else: # plotting an array template_fig = _plot_corrmap( [template], [0], [0], ch_type, icas[0].copy(), "Template", diff --git a/mne/utils/docs.py b/mne/utils/docs.py index fbb1aa19087..93205ccd1b0 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -219,6 +219,20 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): Freesurfer subject directory. """ +docdict['average_plot_evoked_topomap'] = """ +average : float | array-like of float, shape (n_times,) | None + The time window (in seconds) around a given time point to be used for + averaging. For example, 0.2 would translate into a time window that + starts 0.1 s before and ends 0.1 s after the given time point. If the + time window exceeds the duration of the data, it will be clipped. + Different time windows (one per time point) can be provided by + passing an ``array-like`` object (e.g., ``[0.1, 0.2, 0.3]``). If + ``None`` (default), no averaging will take place. + + .. versionchanged:: 1.1 + Support for ``array-like`` input. +""" + docdict['average_plot_psd'] = """\ average : bool If False, the PSDs of all channels is displayed. No averaging @@ -2241,6 +2255,24 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): a power-of-two size (can be much faster). """ +docdict['nrows_ncols_ica_components'] = """ +nrows, ncols : int | 'auto' + The number of rows and columns of topographies to plot. If both ``nrows`` + and ``ncols`` are ``'auto'``, will plot up to 20 components in a 5×4 grid, + and return multiple figures if more than 20 components are requested. + If one is ``'auto'`` and the other a scalar, a single figure is generated. + If scalars are provided for both arguments, will plot up to ``nrows*ncols`` + components in a grid and return multiple figures as needed. Default is + ``nrows='auto', ncols='auto'``. +""" + +docdict['nrows_ncols_topomap'] = """ +nrows, ncols : int | 'auto' + The number of rows and columns of topographies to plot. If either ``nrows`` + or ``ncols`` is ``'auto'``, the necessary number will be inferred. Defaults + to ``nrows=1, ncols='auto'``. +""" + # %% # O @@ -3769,9 +3801,9 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): _units = """ units : {}str | None - The units of the channel type; used for the colorbar label. Ignored if - ``colorbar=False``. If ``None`` {}the label will be "AU" indicating - arbitrary units. Default is ``None``. + The units to use for the colorbar label. Ignored if ``colorbar=False``. + If ``None`` {}the label will be "AU" indicating arbitrary units. + Default is ``None``. """ docdict['units_topomap'] = _units.format('', '') docdict['units_topomap_evoked'] = _units.format( diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py index 48be8af97f6..0a602e290e9 100644 --- a/mne/viz/tests/test_ica.py +++ b/mne/viz/tests/test_ica.py @@ -147,7 +147,7 @@ def test_plot_ica_properties(): assert 'extrapolation mode local to mean' in log, log ica.plot_properties(epochs, picks=1, dB=False, plot_std=1.5, **topoargs) fig = ica.plot_properties(epochs, picks=1, image_args={'sigma': 1.5}, - topomap_args={'res': 4, 'colorbar': True}, + topomap_args=dict(res=4, colorbar=True), psd_args={'fmax': 65.}, plot_std=False, log_scale=True, figsize=[4.5, 4.5], reject=reject)[0] diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 4d45cfaea18..62536ca2b48 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -38,7 +38,6 @@ _fake_scroll) from mne.utils import requires_sklearn, check_version - data_dir = testing.data_path(download=False) subjects_dir = op.join(data_dir, 'subjects') ecg_fname = op.join(data_dir, 'MEG', 'sample', 'sample_audvis_ecg-proj.fif') @@ -229,6 +228,30 @@ def test_plot_evoked_topomap_errors(evoked, monkeypatch): evoked.plot_topomap() +@pytest.mark.parametrize('units, scalings, expected_unit', [ + (None, None, 'µV'), + ('foo', None, 'foo'), + (None, 7., 'AU'), # non-default scaling → "AU" +]) +def test_plot_evoked_topomap_units(evoked, units, scalings, expected_unit): + """Test that colorbar units respect scalings correctly.""" + evoked.pick(['EEG 001', 'EEG 002', 'EEG 003']) + fig = evoked.plot_topomap(times=0.1, res=8, contours=0, sensors=False, + units=units, scalings=scalings) + # ideally we'd do this: + # cbar = [ax for ax in fig.axes if hasattr(ax, '_colorbar')] + # assert len(cbar) == 1 + # cbar = cbar[0] + # assert cbar.get_title() == expected_unit + # ...but not all matplotlib versions support it, and we can't use + # @requires_version because it's hard figure out exactly which MPL version + # is the cutoff since it relies on a private attribute. So for now we just + # do this: + for ax in fig.axes: + if hasattr(ax, '_colorbar'): + assert ax.get_title() == expected_unit + + @pytest.mark.parametrize('extrapolate', ('box', 'local', 'head')) def test_plot_evoked_topomap_extrapolation(evoked, extrapolate): """Test topomap extrapolation options.""" diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 1accf259a92..ceb7cd65d08 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -35,7 +35,7 @@ plt_show, _process_times, DraggableColorbar, _get_cmap, _validate_if_list_of_axes, _setup_cmap, _check_time_unit, _set_3d_axes_equal, _check_type_projs, _format_units_psd, - _prepare_sensor_names) + _prepare_sensor_names, _warn_deprecated_vmin_vmax) from ..defaults import _handle_default from ..transforms import apply_trans, invert_transform from ..io.meas_info import Info, _simplify_info @@ -1111,15 +1111,15 @@ def _plot_ica_topomap(ica, idx=0, ch_type=None, res=64, @verbose -def plot_ica_components(ica, picks=None, ch_type=None, res=64, - vmin=None, vmax=None, cmap='RdBu_r', - sensors=True, colorbar=False, title=None, - show=True, outlines='head', contours=6, - image_interp=_INTERPOLATION_DEFAULT, - inst=None, plot_std=True, - topomap_args=None, image_args=None, - psd_args=None, reject='auto', - sphere=None, *, verbose=None): +def plot_ica_components( + ica, picks=None, ch_type=None, *, inst=None, plot_std=True, + reject='auto', sensors=True, show_names=False, contours=6, + outlines='head', sphere=None, image_interp=_INTERPOLATION_DEFAULT, + extrapolate=_EXTRAPOLATE_DEFAULT, border=_BORDER_DEFAULT, res=64, + size=1, cmap='RdBu_r', vlim=(None, None), vmin=None, vmax=None, + cnorm=None, colorbar=False, cbar_fmt='%3.2f', axes=None, title=None, + nrows='auto', ncols='auto', show=True, topomap_args=None, + image_args=None, psd_args=None, verbose=None): """Project mixing matrix on interpolated sensor topography. Parameters @@ -1128,17 +1128,6 @@ def plot_ica_components(ica, picks=None, ch_type=None, res=64, The ICA solution. %(picks_ica)s %(ch_type_topomap)s - %(res_topomap)s - %(vmin_vmax_topomap)s - %(cmap_topomap)s - %(sensors_topomap)s - %(colorbar_topomap)s - title : str | None - Title to use. - %(show)s - %(outlines_topomap)s - %(contours_topomap)s - %(image_interp_topomap)s inst : Raw | Epochs | None To be able to see component properties after clicking on component topomap you need to pass relevant data - instances of Raw or Epochs @@ -1149,32 +1138,73 @@ def plot_ica_components(ica, picks=None, ch_type=None, res=64, Defaults to True, which plots one standard deviation above/below. If set to float allows to control how many standard deviations are plotted. For example 2.5 will plot 2.5 standard deviation above/below. - topomap_args : dict | None - Dictionary of arguments to ``plot_topomap``. If None, doesn't pass any - additional arguments. Defaults to None. - image_args : dict | None - Dictionary of arguments to ``plot_epochs_image``. If None, doesn't pass - any additional arguments. Defaults to None. - psd_args : dict | None - Dictionary of arguments to :meth:`~mne.Epochs.compute_psd`. If - ``None``, doesn't pass any additional arguments. Defaults to ``None``. reject : 'auto' | dict | None Allows to specify rejection parameters used to drop epochs (or segments if continuous signal is passed as inst). If None, no rejection is applied. The default is 'auto', which applies the rejection parameters used when fitting the ICA object. + %(sensors_topomap)s + %(show_names_topomap)s + %(contours_topomap)s + %(outlines_topomap)s %(sphere_topomap_auto)s + %(image_interp_topomap)s + %(extrapolate_topomap)s + + .. versionadded:: 1.3 + %(border_topomap)s + + .. versionadded:: 1.3 + %(res_topomap)s + %(size_topomap)s + + .. versionadded:: 1.3 + %(cmap_topomap)s + %(vlim_plot_topomap)s + + .. versionadded:: 1.3 + %(vmin_vmax_topomap)s + + .. deprecated:: v1.4 + The ``vmin`` and ``vmax`` parameters will be removed in version + 1.4. Please use the ``vlim`` parameter instead. + %(cnorm)s + + .. versionadded:: 1.3 + %(colorbar_topomap)s + %(cbar_fmt_topomap)s + %(axes_evoked_plot_topomap)s + title : str | None + The title of the generated figure. If ``None`` (default) and + ``axes=None``, a default title of "ICA Components" will be used. + %(nrows_ncols_ica_components)s + + .. versionadded:: 1.3 + %(show)s + topomap_args : dict | None + Dictionary of arguments to ``plot_topomap``. If None, doesn't pass any + additional arguments. Defaults to None. + + .. deprecated:: v1.4 + The ``topomap_args`` parameter will be removed in version 1.4. All + relevant topomap parameters (e.g., ``show_names``, ``extrapolate``, + ``border``, ``size``, etc) are now directly exposed in this + function's signature. + image_args : dict | None + Dictionary of arguments to pass to :func:`~mne.viz.plot_epochs_image` + in interactive mode. Ignored if ``inst`` is not supplied. If ``None``, + nothing is passed. Defaults to ``None``. + psd_args : dict | None + Dictionary of arguments to pass to :meth:`~mne.Epochs.compute_psd` in + interactive mode. Ignored if ``inst`` is not supplied. If ``None``, + nothing is passed. Defaults to ``None``. %(verbose)s Returns ------- fig : instance of matplotlib.figure.Figure | list of matplotlib.figure.Figure - - The figure object(s). Components are plotted on a grid with maximum - dimensions of 5⨉4. If more than 20 components are plotted, a new figure - will be created for each batch of 20, and a list of those figures - will be returned. + The figure object(s). Notes ----- @@ -1191,77 +1221,110 @@ def plot_ica_components(ica, picks=None, ch_type=None, res=64, if ica.info is None: raise RuntimeError('The ICA\'s measurement info is missing. Please ' 'fit the ICA or add the corresponding info object.') + # TODO ↓↓↓↓↓ remove after 1.3 release (begin) + vlim = _warn_deprecated_vmin_vmax(vlim, vmin, vmax, '1.4') + + if topomap_args: # not None, not empty dict + warn('The "topomap_args" parameter is deprecated and will be ' + 'removed in version 1.4. All relevant topomap parameters are now ' + 'directly exposed in this function\'s signature.', FutureWarning) + topomap_args = copy.copy(topomap_args) + else: + topomap_args = dict() + # TODO ↑↑↑↑↑ remove after 1.3 release (end) + + n_components = ica.mixing_matrix_.shape[1] + + # for backward compat, nrow='auto' ncol='auto' should yield 4 rows 5 cols + # and create multiple figures if more than 20 components requested + if nrows == 'auto' and ncols == 'auto': + ncols = 5 + max_subplots = 20 + elif nrows == 'auto' or ncols == 'auto': + # user provided incomplete row/col spec; put all in one figure + max_subplots = n_components + else: + max_subplots = nrows * ncols + + # handle ch_type=None + ch_type = _get_ch_type(ica, ch_type) - topomap_args = dict() if topomap_args is None else topomap_args - topomap_args = copy.copy(topomap_args) - if 'sphere' not in topomap_args: - topomap_args['sphere'] = sphere - if picks is None: # plot components by sets of 20 - ch_type = _get_ch_type(ica, ch_type) - n_components = ica.mixing_matrix_.shape[1] - p = 20 + if picks is None: figs = [] - for k in range(0, n_components, p): - picks = range(k, min(k + p, n_components)) + cut_points = range(max_subplots, n_components, max_subplots) + pick_groups = np.split(range(n_components), cut_points) + for _picks in pick_groups: fig = plot_ica_components( - ica, picks=picks, ch_type=ch_type, res=res, vmax=vmax, - cmap=cmap, sensors=sensors, colorbar=colorbar, title=title, - show=show, outlines=outlines, contours=contours, - image_interp=image_interp, inst=inst, plot_std=plot_std, - topomap_args=topomap_args, image_args=image_args, - psd_args=psd_args, reject=reject, sphere=sphere) + ica, picks=_picks, ch_type=ch_type, inst=inst, + plot_std=plot_std, reject=reject, sensors=sensors, + show_names=show_names, contours=contours, outlines=outlines, + sphere=sphere, image_interp=image_interp, + extrapolate=extrapolate, border=border, res=res, size=size, + cmap=cmap, vlim=vlim, cnorm=cnorm, colorbar=colorbar, + cbar_fmt=cbar_fmt, axes=axes, title=title, nrows=nrows, + ncols=ncols, show=show, topomap_args=topomap_args, + image_args=image_args, psd_args=psd_args, verbose=verbose) figs.append(fig) return figs else: picks = _picks_to_idx(ica.n_components_, picks, picks_on="components") - ch_type = _get_ch_type(ica, ch_type) - - cmap = _setup_cmap(cmap, n_axes=len(picks)) - data = np.dot(ica.mixing_matrix_[:, picks].T, - ica.pca_components_[:ica.n_components_]) data_picks, pos, merge_channels, names, ch_type, sphere, clip_origin = \ _prepare_topomap_plot(ica, ch_type, sphere=sphere) + + cmap = _setup_cmap(cmap, n_axes=len(picks)) + names = _prepare_sensor_names(names, show_names) outlines = _make_head_outlines(sphere, pos, outlines, clip_origin) + data = np.dot(ica.mixing_matrix_[:, picks].T, + ica.pca_components_[:ica.n_components_]) data = np.atleast_2d(data) data = data[:, data_picks] - # prepare data for iteration - fig, axes, _, _ = _prepare_trellis(len(data), ncols=5) if title is None: title = 'ICA components' - fig.suptitle(title) + user_passed_axes = axes is not None + if not user_passed_axes: + fig, axes, _, _ = _prepare_trellis(len(data), ncols=ncols, nrows=nrows) + fig.suptitle(title) - titles = list() + subplot_titles = list() for ii, data_, ax in zip(picks, data, axes): kwargs = dict(color='gray') if ii in ica.exclude else dict() comp_title = ica._ica_names[ii] if len(set(ica.get_channel_types())) > 1: comp_title += f' ({ch_type})' - titles.append(ax.set_title(comp_title, fontsize=12, **kwargs)) + subplot_titles.append(ax.set_title(comp_title, fontsize=12, **kwargs)) if merge_channels: - data_, names_ = _merge_ch_data(data_, ch_type, names.copy()) - vlim = _setup_vmin_vmax(data_, vmin, vmax) + data_, names_ = _merge_ch_data(data_, ch_type, copy.copy(names)) + # ↓↓↓ NOTE: we intentionally use the default norm=False here, so that + # ↓↓↓ we get vlims that are symmetric-about-zero, even if the data for + # ↓↓↓ a given component happens to be one-sided. + _vlim = _setup_vmin_vmax(data_, *vlim) im = plot_topomap( - data_.flatten(), pos, vlim=vlim, res=res, axes=ax, - cmap=cmap[0], outlines=outlines, contours=contours, - image_interp=image_interp, show=False, sensors=sensors, - ch_type=ch_type, **topomap_args)[0] + data_.flatten(), pos, ch_type=ch_type, sensors=sensors, + names=names, contours=contours, outlines=outlines, sphere=sphere, + image_interp=image_interp, extrapolate=extrapolate, border=border, + res=res, size=size, cmap=cmap[0], vlim=_vlim, cnorm=cnorm, + axes=ax, show=False, **topomap_args)[0] + im.axes.set_label(ica._ica_names[ii]) if colorbar: cbar, cax = _add_colorbar(ax, im, cmap, title="AU", - side="right", pad=.05, format='%3.2f') + side="right", pad=.05, format=cbar_fmt) cbar.ax.tick_params(labelsize=12) - cbar.set_ticks(vlim) + cbar.set_ticks(_vlim) _hide_frame(ax) del pos tight_layout(fig=fig) - fig.subplots_adjust(top=0.88, bottom=0.) + # TODO ↓↓↓↓↓ remove after 1.3 release (begin) + if not user_passed_axes: + fig.subplots_adjust(top=0.88, bottom=0.) + # TODO ↑↑↑↑↑ remove after 1.3 release (end) fig.canvas.draw() # add title selection interactivity - def onclick_title(event, ica=ica, titles=titles): + def onclick_title(event, ica=ica, titles=subplot_titles): # check if any title was pressed title_pressed = None for title in titles: @@ -1472,17 +1535,7 @@ def plot_evoked_topomap( automatically by checking for local maxima in global field power. If "interactive", the time can be set interactively at run-time by using a slider. - average : float | array-like of float, shape (n_times,) | None - The time window (in seconds) around a given time point to be used for - averaging. For example, 0.2 would translate into a time window that - starts 0.1 s before and ends 0.1 s after the given time point. If the - time window exceeds the duration of the data, it will be clipped. - Different time windows (one per time point) can be provided by - passing an ``array-like`` object (e.g., ``[0.1, 0.2, 0.3]``). If - ``None`` (default), no averaging will take place. - - .. versionchanged:: 1.1 - Support for ``array-like`` input. + %(average_plot_evoked_topomap)s %(ch_type_topomap)s %(scalings_topomap)s %(proj_plot)s @@ -1519,16 +1572,7 @@ def plot_evoked_topomap( String format for topomap values. Defaults (None) to "%%01d ms" if ``time_unit='ms'``, "%%0.3f s" if ``time_unit='s'``, and "%%g" otherwise. Can be an empty string to omit the time label. - nrows : int | 'auto' - The number of rows of topographies to plot. Defaults to 1. If 'auto', - obtains the number of rows depending on the amount of times to plot - and the number of cols. Not valid when times == 'interactive'. - - .. versionadded:: 0.20 - ncols : int | 'auto' - The number of columns of topographies to plot. If 'auto' (default), - obtains the number of columns depending on the amount of times to plot - and the number of rows. Not valid when times == 'interactive'. + %(nrows_ncols_topomap)s Ignored when times == 'interactive'. .. versionadded:: 0.20 %(show)s @@ -1580,7 +1624,10 @@ def plot_evoked_topomap( "times='interactive'.") # units, scalings key = 'grad' if ch_type.startswith('planar') else ch_type + default_scaling = _handle_default('scalings', None)[key] scaling = _handle_default('scalings', scalings)[key] + # if non-default scaling, fall back to "AU" if unit wasn't given by user + key = 'misc' if scaling != default_scaling else key unit = _handle_default('units', units)[key] # ch_names (required for NIRS) ch_names = names diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 33275a501a3..8ae413ce681 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -101,6 +101,20 @@ def _setup_vmin_vmax(data, vmin, vmax, norm=False): return vmin, vmax +def _warn_deprecated_vmin_vmax(vlim, vmin, vmax, version): + if vmin is not None or vmax is not None: + warn('The "vmin" and "vmax" parameters are deprecated and will be ' + f'removed in version {version}. Use the "vlim" parameter ' + 'instead.', FutureWarning) + if vlim[0] is None and vlim[1] is None: + vlim = (vmin, vmax) + else: + warn('You provided either "vmin" or "vmax" (which are ' + 'deprecated) as well as "vlim". Using "vlim" and ' + 'ignoring "vmin" and "vmax".') + return vlim + + def plt_show(show=True, fig=None, **kwargs): """Show a figure while suppressing warnings. diff --git a/tutorials/machine-learning/50_decoding.py b/tutorials/machine-learning/50_decoding.py index 8cf7fd90953..3fb0036a21f 100644 --- a/tutorials/machine-learning/50_decoding.py +++ b/tutorials/machine-learning/50_decoding.py @@ -10,8 +10,8 @@ Design philosophy ================= -Decoding (a.k.a. MVPA) in MNE largely follows the machine -learning API of the scikit-learn package. +Decoding (a.k.a. MVPA) in MNE largely follows the machine learning API of the +scikit-learn package. Each estimator implements ``fit``, ``transform``, ``fit_transform``, and (optionally) ``inverse_transform`` methods. For more details on this design, visit scikit-learn_. For additional theoretical insights into the decoding diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py index 8a95b0ab471..d6511baba9c 100644 --- a/tutorials/preprocessing/40_artifact_correction_ica.py +++ b/tutorials/preprocessing/40_artifact_correction_ica.py @@ -23,8 +23,8 @@ import os import mne -from mne.preprocessing import (ICA, create_eog_epochs, create_ecg_epochs, - corrmap) +from mne.preprocessing import (ICA, corrmap, create_ecg_epochs, + create_eog_epochs) sample_data_folder = mne.datasets.sample.data_path() sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',