Repository: mne-tools/mne-python

Issue:
make property plot launched from ica.plot_sources(raw,) an option.
If you use `ica.plot_sources` with a `raw` instance and then right click on the ch_name you get an `mne.viz.plot_ica_properties` plot where fixed length epochs have been generate from the raw. This takes a long time and isn't tremendously meaningful for raw data.

In my use case I'm only checking for artifacts (EOG/ECG/ etc) so what I want to see is the just the topoplot (note this is the old behavior).

would it be possible to optionally choose the new (`plot_ica_properties`) or the old (just topomap) behavior?




Discussion:
@bloyl do you mean to automatically triage which plot we generate based on the instance type? Or do you mean let the user pick when they instantiate the plot?
I don't have a preference.

I think for raw instances id like to be able to get the just topoplot
behavior. I can't think of when I would use the plot_ica_propeties with a
raw instance. For epochs the plot_ica_properties is probably what I want.

If noone wants the plot_ica_properties behavior for raw. I'd be fine with
the triage of
Raw-> plot_topo
Epochs-> plot_ica_properties

If we want both behaviors available for raw we'd obviously need a user
option.

On Tue, Mar 16, 2021, 2:47 PM Daniel McCloy ***@***.***>
wrote:

> @bloyl <https://github.com/bloyl> do you mean to automatically triage
> which plot we generate based on the instance type? Or do you mean let the
> user pick when they instantiate the plot?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mne-tools/mne-python/issues/9110#issuecomment-800517866>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABKTXHMCJAG5O7LW5JB5GLTTD6RVLANCNFSM4ZJA2MIQ>
> .
>

OK, let's go with 

> Raw-> plot_topo
> Epochs-> plot_ica_properties

Sounds good.

On Tue, Mar 16, 2021, 4:28 PM Daniel McCloy ***@***.***>
wrote:

> OK, let's go with
>
> Raw-> plot_topo
> Epochs-> plot_ica_properties
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mne-tools/mne-python/issues/9110#issuecomment-800581350>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABKTXHKG52LNIKKRB2AMBXLTD65NJANCNFSM4ZJA2MIQ>
> .
>

assigned to @vagechirkov and @anaradanovic 
@drammock @bloyl I was actually super happy to have the plot_properties come up when right clicking on raw ica sources with the latest version. We need this in our lab, where we reject noise components with the ica on reference channels method. The properties plot is here the best, and more informative about whether sth is noise than just a topo plot.
See the example: https://mne.tools/stable/auto_examples/preprocessing/plot_find_ref_artifacts.html#sphx-glr-auto-examples-preprocessing-plot-find-ref-artifacts-py
I do tons of these plots cleaning our data, and the new feature was a big time saver for us. Would be sad to see it disappear again.

I also don't know why it would be wise to have different behavior for epochs or raw.

> @drammock @bloyl I was actually super happy to have the plot_properties come up when right clicking on raw ica sources with the latest version. We need this in our lab, where we reject noise components with the ica on reference channels method. The properties plot is here the best, and more informative about whether sth is noise than just a topo plot.
> See the example: https://mne.tools/stable/auto_examples/preprocessing/plot_find_ref_artifacts.html#sphx-glr-auto-examples-preprocessing-plot-find-ref-artifacts-py
> I do tons of these plots cleaning our data, and the new feature was a big time saver for us. Would be sad to see it disappear again.
> 
> I also don't know why it would be wise to have different behavior for epochs or raw.

I think this only applies to the interface, but not the function itself.

Here is an example for ```Raw```:

![image](https://user-images.githubusercontent.com/42982039/111509432-04576500-874d-11eb-91b1-c6a837f40b7a.png)

I know, but I mean exactly the right-click behavior in the interactive interface. I have to do a lot of property plots for each raw file, looking at several components. It made it so much easier just right-clicking on them, instead of having to call a seperate function and to pass the right component number or a list of them.
Moreover, the additional information provided by the plots (rather than just the topo) is helpful or meaningful also for people who "just" look for eye or heart artifact components; I don't think they "take a long time" (maybe half a second longer); and there sure was a reasoning behind making that change for the latest version (as topo only was the default before).

Resolving patch:
diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py
--- a/mne/viz/_figure.py
+++ b/mne/viz/_figure.py
@@ -45,7 +45,8 @@
 import numpy as np
 from matplotlib.figure import Figure
 from .epochs import plot_epochs_image
-from .ica import _create_properties_layout
+from .ica import (_create_properties_layout, _fast_plot_ica_properties,
+                  _prepare_data_ica_properties)
 from .utils import (plt_show, plot_sensors, _setup_plot_projector, _events_off,
                     _set_window_title, _merge_annotations, DraggableLine,
                     _get_color_list, logger, _validate_if_list_of_axes,
@@ -871,7 +872,13 @@ def _create_ica_properties_fig(self, idx):
         fig = self._new_child_figure(figsize=(7, 6), fig_name=None,
                                      window_title=f'{ch_name} properties')
         fig, axes = _create_properties_layout(fig=fig)
-        self.mne.ica.plot_properties(self.mne.ica_inst, picks=pick, axes=axes)
+        if not hasattr(self.mne, 'data_ica_properties'):
+            # Precompute epoch sources only once
+            self.mne.data_ica_properties = _prepare_data_ica_properties(
+                self.mne.ica_inst, self.mne.ica)
+        _fast_plot_ica_properties(
+            self.mne.ica, self.mne.ica_inst, picks=pick, axes=axes,
+            precomputed_data=self.mne.data_ica_properties)
 
     def _create_epoch_image_fig(self, pick):
         """Show epochs image for the selected channel."""
diff --git a/mne/viz/ica.py b/mne/viz/ica.py
--- a/mne/viz/ica.py
+++ b/mne/viz/ica.py
@@ -319,14 +319,26 @@ def plot_ica_properties(ica, inst, picks=None, axes=None, dB=True,
     -----
     .. versionadded:: 0.13
     """
-    from ..io.base import BaseRaw
-    from ..epochs import BaseEpochs
+    return _fast_plot_ica_properties(ica, inst, picks=picks, axes=axes, dB=dB,
+                                     plot_std=plot_std,
+                                     topomap_args=topomap_args,
+                                     image_args=image_args, psd_args=psd_args,
+                                     figsize=figsize, show=show,
+                                     reject=reject,
+                                     reject_by_annotation=reject_by_annotation,
+                                     verbose=verbose, precomputed_data=None)
+
+
+def _fast_plot_ica_properties(ica, inst, picks=None, axes=None, dB=True,
+                              plot_std=True, topomap_args=None,
+                              image_args=None, psd_args=None, figsize=None,
+                              show=True, reject='auto', precomputed_data=None,
+                              reject_by_annotation=True, *, verbose=None):
+    """Display component properties."""
     from ..preprocessing import ICA
-    from ..io import RawArray
 
     # input checks and defaults
     # -------------------------
-    _validate_type(inst, (BaseRaw, BaseEpochs), "inst", "Raw or Epochs")
     _validate_type(ica, ICA, "ica", "ICA")
     _validate_type(plot_std, (bool, 'numeric'), 'plot_std')
     if isinstance(plot_std, bool):
@@ -367,49 +379,11 @@ def plot_ica_properties(ica, inst, picks=None, axes=None, dB=True,
 
     # calculations
     # ------------
-
-    if isinstance(inst, BaseRaw):
-        # when auto, delegate reject to the ica
-        from ..epochs import make_fixed_length_epochs
-        if reject == 'auto':
-            reject = getattr(ica, 'reject_', None)
-        if reject is None:
-            drop_inds = None
-            dropped_indices = []
-            # break up continuous signal into segments
-            epochs_src = make_fixed_length_epochs(
-                ica.get_sources(inst),
-                duration=2,
-                preload=True,
-                reject_by_annotation=reject_by_annotation,
-                proj=False,
-                verbose=False)
-        else:
-            data = inst.get_data()
-            data, drop_inds = _reject_data_segments(data, ica.reject_,
-                                                    flat=None, decim=None,
-                                                    info=inst.info,
-                                                    tstep=2.0)
-            inst_rejected = RawArray(data, inst.info)
-            # break up continuous signal into segments
-            epochs_src = make_fixed_length_epochs(
-                ica.get_sources(inst_rejected),
-                duration=2,
-                preload=True,
-                reject_by_annotation=reject_by_annotation,
-                proj=False,
-                verbose=False)
-            # getting dropped epochs indexes
-            dropped_indices = [(d[0] // len(epochs_src.times)) + 1
-                               for d in drop_inds]
-        kind = "Segment"
+    if isinstance(precomputed_data, tuple):
+        kind, dropped_indices, epochs_src, data = precomputed_data
     else:
-        drop_inds = None
-        epochs_src = ica.get_sources(inst)
-        dropped_indices = []
-        kind = "Epochs"
-
-    data = epochs_src.get_data()
+        kind, dropped_indices, epochs_src, data = _prepare_data_ica_properties(
+            inst, ica, reject_by_annotation, reject)
     ica_data = np.swapaxes(data[:, picks, :], 0, 1)
     dropped_src = ica_data
 
@@ -469,6 +443,80 @@ def set_title_and_labels(ax, title, xlab, ylab):
     return all_fig
 
 
+def _prepare_data_ica_properties(inst, ica, reject_by_annotation=True,
+                                 reject='auto'):
+    """Prepare Epochs sources to plot ICA properties.
+
+    Parameters
+    ----------
+    ica : instance of mne.preprocessing.ICA
+        The ICA solution.
+    inst : instance of Epochs or Raw
+        The data to use in plotting properties.
+    reject_by_annotation : bool, optional
+        [description], by default True
+    reject : str, optional
+        [description], by default 'auto'
+
+    Returns
+    -------
+    kind : str
+        "Segment" for BaseRaw and "Epochs" for BaseEpochs
+    dropped_indices : list
+        Dropped epochs indexes.
+    epochs_src : instance of Epochs
+        Segmented data of ICA sources.
+    data : array of shape (n_epochs, n_ica_sources, n_times)
+        A view on epochs ICA sources data.
+    """
+    from ..io.base import BaseRaw
+    from ..io import RawArray
+    from ..epochs import BaseEpochs
+
+    _validate_type(inst, (BaseRaw, BaseEpochs), "inst", "Raw or Epochs")
+    if isinstance(inst, BaseRaw):
+        # when auto, delegate reject to the ica
+        from ..epochs import make_fixed_length_epochs
+        if reject == 'auto':
+            reject = getattr(ica, 'reject_', None)
+        if reject is None:
+            drop_inds = None
+            dropped_indices = []
+            # break up continuous signal into segments
+            epochs_src = make_fixed_length_epochs(
+                ica.get_sources(inst),
+                duration=2,
+                preload=True,
+                reject_by_annotation=reject_by_annotation,
+                proj=False,
+                verbose=False)
+        else:
+            data = inst.get_data()
+            data, drop_inds = _reject_data_segments(data, ica.reject_,
+                                                    flat=None, decim=None,
+                                                    info=inst.info,
+                                                    tstep=2.0)
+            inst_rejected = RawArray(data, inst.info)
+            # break up continuous signal into segments
+            epochs_src = make_fixed_length_epochs(
+                ica.get_sources(inst_rejected),
+                duration=2,
+                preload=True,
+                reject_by_annotation=reject_by_annotation,
+                proj=False,
+                verbose=False)
+            # getting dropped epochs indexes
+            dropped_indices = [(d[0] // len(epochs_src.times)) + 1
+                               for d in drop_inds]
+        kind = "Segment"
+    else:
+        drop_inds = None
+        epochs_src = ica.get_sources(inst)
+        dropped_indices = []
+        kind = "Epochs"
+    return kind, dropped_indices, epochs_src, epochs_src.get_data()
+
+
 def _plot_ica_sources_evoked(evoked, picks, exclude, title, show, ica,
                              labels=None):
     """Plot average over epochs in ICA space.


Test patch: