Repository: mne-tools/mne-python

Issue:
BUG: pyvista backend on headless machine
I'm trying to setup the Pyvista backend for plotting on a headless machine with Jupyter port redirection.

After setting up a new [server environment](https://raw.githubusercontent.com/mne-tools/mne-python/main/server_environment.yml), 

and (painfully*) installing pyvista with pyqt5 via
```
conda install -c conda-forge pyqt vtk pyvista 
```
(* `pip install pyqt5` or `pyqt` as well as `conda install install pyqt` and `sudo apt-get install python-pyqt5` all lead to pyvista to fail, or miss a python or C dependency one way or another),

Pyvista asked to install `pyvistaqt`, which I did.

However, I now face an `AttributeError`:

```python
import mne
mne.viz.set_3d_backend("notebook")

data_dir = mne.datasets.sample.data_path()
subjects_dir = data_dir + '/subjects'
stc_path = data_dir + '/MEG/sample/sample_audvis-meg-eeg'
stc = mne.read_source_estimate(stc_path, 'sample')
stc.plot(hemi='lh', 
         subjects_dir=subjects_dir, 
         initial_time=0.07,
         size=(800, 600), 
         backend='pyvista')
```

```python
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-1-39692bcd540a> in <module>
      6 stc_path = data_dir + '/MEG/sample/sample_audvis-meg-eeg'
      7 stc = mne.read_source_estimate(stc_path, 'sample')
----> 8 stc.plot(hemi='lh', 
      9          subjects_dir=subjects_dir,
     10          initial_time=0.07,

~/.conda/envs/bm/lib/python3.8/site-packages/mne/source_estimate.py in plot(self, subject, surface, hemi, colormap, time_label, smoothing_steps, transparent, alpha, time_viewer, subjects_dir, figure, views, colorbar, clim, cortex, size, background, foreground, initial_time, time_unit, backend, spacing, title, show_traces, src, volume_options, view_layout, add_data_kwargs, brain_kwargs, verbose)
    649              src=None, volume_options=1., view_layout='vertical',
    650              add_data_kwargs=None, brain_kwargs=None, verbose=None):
--> 651         brain = plot_source_estimates(
    652             self, subject, surface=surface, hemi=hemi, colormap=colormap,
    653             time_label=time_label, smoothing_steps=smoothing_steps,

<decorator-gen-153> in plot_source_estimates(stc, subject, surface, hemi, colormap, time_label, smoothing_steps, transparent, alpha, time_viewer, subjects_dir, figure, views, colorbar, clim, cortex, size, background, foreground, initial_time, time_unit, backend, spacing, title, show_traces, src, volume_options, view_layout, add_data_kwargs, brain_kwargs, verbose)

~/.conda/envs/bm/lib/python3.8/site-packages/mne/viz/_3d.py in plot_source_estimates(stc, subject, surface, hemi, colormap, time_label, smoothing_steps, transparent, alpha, time_viewer, subjects_dir, figure, views, colorbar, clim, cortex, size, background, foreground, initial_time, time_unit, backend, spacing, title, show_traces, src, volume_options, view_layout, add_data_kwargs, brain_kwargs, verbose)
   1778     else:
   1779         with use_3d_backend(backend):
-> 1780             return _plot_stc(
   1781                 stc, overlay_alpha=alpha, brain_alpha=alpha,
   1782                 vector_alpha=alpha, cortex=cortex, foreground=foreground,

~/.conda/envs/bm/lib/python3.8/site-packages/mne/viz/_3d.py in _plot_stc(stc, subject, surface, hemi, colormap, time_label, smoothing_steps, subjects_dir, views, clim, figure, initial_time, time_unit, background, time_viewer, colorbar, transparent, brain_alpha, overlay_alpha, vector_alpha, cortex, foreground, size, scale_factor, show_traces, src, volume_options, view_layout, add_data_kwargs, brain_kwargs)
   1956             TimeViewer(brain)
   1957         else:  # PyVista
-> 1958             brain.setup_time_viewer(time_viewer=time_viewer,
   1959                                     show_traces=show_traces)
   1960     else:

~/.conda/envs/bm/lib/python3.8/site-packages/mne/viz/_brain/_brain.py in setup_time_viewer(***failed resolving arguments***)
    655         self._configure_scalar_bar()
    656         self._configure_shortcuts()
--> 657         self._configure_picking()
    658         self._configure_tool_bar()
    659         self._configure_dock()

~/.conda/envs/bm/lib/python3.8/site-packages/mne/viz/_brain/_brain.py in _configure_picking(self)
   1219                 self.act_data_smooth[hemi] = (act_data, smooth_mat)
   1220 
-> 1221         self._renderer._update_picking_callback(
   1222             self._on_mouse_move,
   1223             self._on_button_press,

~/.conda/envs/bm/lib/python3.8/site-packages/mne/viz/backends/_pyvista.py in _update_picking_callback(self, on_mouse_move, on_button_press, on_button_release, on_pick)
    669                                  on_button_release,
    670                                  on_pick):
--> 671         self.plotter.iren.AddObserver(
    672             vtk.vtkCommand.RenderEvent,
    673             on_mouse_move

AttributeError: 'NoneType' object has no attribute 'AddObserver'
``` 

Here is my `mne.sys_info()`
```
Platform:      Linux-5.4.0-52-generic-x86_64-with-glibc2.10
Python:        3.8.8 | packaged by conda-forge | (default, Feb 20 2021, 16:22:27)  [GCC 9.3.0]
Executable:    /private/home/jeanremi/.conda/envs/bm/bin/python
CPU:           x86_64: 80 cores
Memory:        503.8 GB

mne:           0.23.dev0
numpy:         1.20.2 {blas=NO_ATLAS_INFO, lapack=lapack}
scipy:         1.6.2
matplotlib:    3.4.1 {backend=module://ipykernel.pylab.backend_inline}

sklearn:       0.24.1
numba:         0.53.1
nibabel:       3.2.1
nilearn:       0.7.1
dipy:          Not found
cupy:          Not found
pandas:        1.2.4
mayavi:        Not found
pyvista:       0.29.0 {pyvistaqt=0.3.0, OpenGL 3.3 (Core Profile) Mesa 18.3.1 via llvmpipe (LLVM 7.0, 256 bits)}
vtk:           9.0.0
PyQt5:         5.12.3
```

May relate to https://github.com/mne-tools/mne-python/issues/8659
cc @GuillaumeFavelier


Discussion:
I'm really surprised you needed to install pyvistaqt because pyqt should be stripped from the server_environment.yml conf entirely. This should be the first bug in my opinion. Could you give more details about the installation procedure? Or at which step exactly were you asked to install pyvistaqt? 

Also, you don't need to specify backend=pyvista in stc.plot (since you ask for notebook beforehand, it should enough) because this will switch to the qt-dependant 3d backend.
I was asked to install pyvistaqt only when I called the mne plotting function.
So I think it's because of backend=pyvista.
 Did you try without?
Re-started a fresh install to verify whether pyvistaqt + backend=pyvista made the issue.

It's working fine on dev now.

Thanks @GuillaumeFavelier ! 
Actually, I'm reopening it, because I don't think the error should occur. On a headless, if you specify `stc.plot(backend='pyvista')` it asks to install pyvistaqt which breaks everything.
relatedly, we can't plot flat surfaces on headless with pyvista in the notebook ,because it requires a `backend='pyvista'`
If you do `backend='notebook'` does it work?
... this makes me think we should have two backends, `pyvistaqt` and `notebook`. Both use PyVista under the hood but it makes it clearer which flavor you're going to get. We can keep `backend='pyvista'` as an alias for `backend='pyvistaqt'` but just not advertise it in the docs. WDYT @GuillaumeFavelier ?
`backend="notebook"` fails because it's not in the options:
~/.conda/envs/bm/lib/python3.8/site-packages/mne/viz/_3d.py

`1759     _check_option('backend', backend,
   1760                   ['auto', 'matplotlib', 'mayavi', 'pyvista'])
   `
There is something I don't understand here. Which backend do you want to use, `pyvista` or `notebook`?
AFAIK, it's not currently possible to use PyVistaQt offline. I discussed something similar in https://github.com/bloyl/mne_docker/issues/1#issuecomment-804976259

+1 for your idea @larsoner It seems the current situation is not clear enough so changing to `pyvistaqt` could help

Also this `_check_option` should be updated indeed.
That seems like a bug, if you hack in `'notebook'` does it work?

Resolving patch:
diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py
--- a/mne/viz/_3d.py
+++ b/mne/viz/_3d.py
@@ -1757,7 +1757,7 @@ def plot_source_estimates(stc, subject=None, surface='inflated', hemi='lh',
                                     raise_error=True)
     subject = _check_subject(stc.subject, subject)
     _check_option('backend', backend,
-                  ['auto', 'matplotlib', 'mayavi', 'pyvista'])
+                  ['auto', 'matplotlib', 'mayavi', 'pyvista', 'notebook'])
     plot_mpl = backend == 'matplotlib'
     if not plot_mpl:
         if backend == 'auto':
@@ -2448,7 +2448,7 @@ def _check_views(surf, views, hemi, stc=None, backend=None):
             _validate_type(stc, SourceEstimate, 'stc',
                            'SourceEstimate when a flatmap is used')
         if backend is not None:
-            if backend != 'pyvista':
+            if backend not in ('pyvista', 'notebook'):
                 raise RuntimeError('The PyVista 3D backend must be used to '
                                    'plot a flatmap')
     if (views == ['flat']) ^ (surf == 'flat'):  # exactly only one of the two


Test patch: