Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trame support and new Jupyter backend #3385

Merged
merged 119 commits into from Jan 27, 2023
Merged

Trame support and new Jupyter backend #3385

merged 119 commits into from Jan 27, 2023

Conversation

banesullivan
Copy link
Member

@banesullivan banesullivan commented Sep 29, 2022

This adds streamlined support for using PyVista with Trame and adds a new Trame-based Jupyter backend.

The new Trame-based backend is the backend that we will try to centralize around per #3690 and with it's addition comes an immediate deprecation of the ipyvtklink backend.

Trame is a new framework for building lightweight, reactive web applications in a stateful manner. It has great support for VTK and thus PyVista. These changes add a trame module to PyVista that streamlines usage of the trame-vtk interface for use with a PyVista Plotter object.

While the bulk of this PR is actually focused on the new Jupyter backend, this is also provided streamlined support for using PyVista and Trame to make reactive web apps. There are a few examples web apps included under: examples_trame/

Basic Trame Web App

Screen.Recording.2022-09-29.at.12.01.39.PM.mov

Advanced Trame Web App

Screen.Recording.2023-01-21.at.12.12.57.PM.mov

New Jupyter Backend

Screen.Recording.2023-01-21.at.12.16.05.PM.mov

Jupyter Backend

See usage instructions in docs but simply

import pyvista as pv
from pyvista import examples

await pv.set_jupyter_backend('trame')

mesh = examples.load_random_hills()
arrows = mesh.glyph(scale="Normals", orient="Normals", tolerance=0.05)

p = pv.Plotter()
p.add_mesh(mesh, scalars="Elevation", cmap="terrain", smooth_shading=True)
p.add_mesh(arrows, color="black")
p.show()

Screen Shot 2023-01-21 at 12 24 35 PM

Additional Changes

This PR required a significant amount of work to PyVista and sneaks in a few changes that really could've been their own PRs. These include:

  • An an internal refactor of ren_win to render_window as a @property
  • The addition of a new suppress_rendering @property on the BasePlotter that will suppress calls to vtkRenderWindow.Render() for guarantee use of the PyVista plotter without a virtual frame buffer
  • A refactor/change to the _id_name attribute of the plotter so that it is a safe string to use as a variable name (added a P_ prefix)
  • Addition of an actors @property on BasePlotter to get all actors on the Plotter
  • A new mechanism for adding on-render callbacks. See add_on_render_callback. This has two modes (to register with actual VTK render events or be called explicitly by PyVista's BasePlotter.render() method (often to be paired with suppress_rendering)
  • Removal of the already deprecated use_ipyvtk argument and PYVISTA_USE_IPYVTK environment variable

Resolves and Closes

This PR renders many issue non-actionable as the new Trame backend handles many issue with the other existing backends

We will continue to address the remaining issues listed in #3690

Upstream work

A lot of upstream work was necessary for this new Jupyter backend and much thanks to @jourdain! Here are some (not all) of the relevant work:

Future Work

@github-actions github-actions bot added the enhancement Changes that enhance the library label Sep 29, 2022
pyvista/trame/__init__.py Outdated Show resolved Hide resolved
@codecov
Copy link

codecov bot commented Oct 5, 2022

Codecov Report

Merging #3385 (ff085d5) into main (bf670b6) will increase coverage by 0.05%.
The diff coverage is 92.29%.

@@            Coverage Diff             @@
##             main    #3385      +/-   ##
==========================================
+ Coverage   94.13%   94.19%   +0.05%     
==========================================
  Files          87       91       +4     
  Lines       19111    19581     +470     
==========================================
+ Hits        17991    18444     +453     
- Misses       1120     1137      +17     

@banesullivan
Copy link
Member Author

MNE integration test is failing as:

Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.10.7/x64/lib/python3.10/site-packages/pyvistaqt/plotting.py", line 309, in _render
    return BasePlotter.render(self, *args, **kwargs)
  File "/home/runner/work/pyvista/pyvista/pyvista/plotting/plotting.py", line 1606, in render
    if self.render_window is not None and not self._first_time:
  File "/opt/hostedtoolcache/Python/3.10.7/x64/lib/python3.10/site-packages/pyvistaqt/rwi.py", line 4[26](https://github.com/pyvista/pyvista/actions/runs/3246255962/jobs/5324798559#step:13:27), in __getattr__
    raise AttributeError(self.__class__.__name__ +
AttributeError: BackgroundPlotter has no attribute named render_window. Did you mean: '_RenderWindow'?
Fatal Python error: Aborted

Trying to decide if I should implement the render_window property in BackgroundPlotter or if I should just tag this onto BasePlotter....

@Jimmy-KL
Copy link

@banesullivan I tried this trame feature in pyvista and found a strange thing. I have a model with several data arrays, I want to want to change the data array and the data range of the model and scalar bar can be updated. First I remove the old scalar bar, then I add a new one. Here is the code for this part:

@state.change("data_array")
mesh = pv.read(os.path.join(file_path, file_anme))
mesh.set_active_scalars(mesh.array_names[0])
plotter = pv.Plotter(off_screen=True)
actor = plotter.add_mesh(mesh)
plotter.set_background('lightgrey')

def update_data_array(data_array=mesh.array_names[0], **kwargs):
    mesh.set_active_scalars(data_array)
    plotter.remove_scalar_bar()
    plotter.add_scalar_bar(title=data_array)
    min_value = mesh[data_array].min()
    max_value = mesh[data_array].max()
    plotter.update_scalar_bar_range([min_value, max_value])
    ctrl.view_update()

with SinglePageLayout(server) as layout:
    with layout.toolbar:
            vuetify.VSpacer()
            vuetify.VSelect(
                label="属性选择",
                v_model=("data_array", mesh.array_names[0]),
                items=("array_list", mesh.array_names),
                hide_details=True,
                dense=True,
                outlined=True,
                classes="pt-1 ml-2",
                style="max-width: 150px",
            )
    with layout.content:
            with vuetify.VContainer(
                fluid=True,
                classes="pa-0 fill-height",
            ):
                view = PyVistaLocalView(plotter)
                ctrl.view_update = view.update
                ctrl.view_reset_camera = view.reset_camera

This can work for a simple pyvista application and also for the remote view of trame. But if I use the local view, the old scalar bar cannot be removed unless I refresh the page.
The initial state:
image
After change the data array:
image
Refresh the page and everything is OK:
image
It maybe a problem of trame or I didn't use it properly. But I think I can give some feedback here. Or do you have a solution for this problem?

@jourdain
Copy link
Contributor

This is most likely a little issue with the local view. I would think that if you update trame-vtk it may partially fix that problem. I'm still in the process on properly fixing that.

@Jimmy-KL
Copy link

Jimmy-KL commented Dec 1, 2022

This is most likely a little issue with the local view. I would think that if you update trame-vtk it may partially fix that problem. I'm still in the process on properly fixing that.

I was using trame-vtk == 2.0.8. I updated to the latest version 2.0.9, but the model disappeared, and nothing showed. However, for remote view, it's OK.

@banesullivan banesullivan self-assigned this Dec 1, 2022
@banesullivan
Copy link
Member Author

@larsoner, thanks for all of the feedback! I think I've addressed everything you brought up, but please double-check.

Regarding the ipyvtklink deprecation: Hopefully, this is okay now considering the trame conda-forge recipes should be up in no time (thanks to you! 😄 )

I really, really want to merge this today unless something significant comes up -- please let me know what you think.

FYI, I noticed the UI menu bar issue in your screenshot and will try to sneak in a fix for that

@banesullivan
Copy link
Member Author

banesullivan commented Jan 26, 2023

For the no-serializer issue, a better approach would be to notify the user (in the GUI) that "some content could not be available for local rendering" and then offer the option to switch to remote rendering or ignore.

@jourdain, I feel like that would be best implemented in trame-vtk. What do you think?

See Kitware/trame-vtk#15

@akaszynski
Copy link
Member

Further, you can use the 'fixed_point' volume mapper, and all will work in client mode as is.

Should we just change the default mapper then if it detects that it's in a notebook with either trame or client enabled? We don't have to fix it, but I also don't like empty plots.

larsoner
larsoner previously approved these changes Jan 26, 2023
Copy link
Contributor

@larsoner larsoner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than one remaining bug found via my minimal testing, LGTM!

pyvista/trame/jupyter.py Outdated Show resolved Hide resolved
server_proxy_enabled=server_proxy_enabled,
server_proxy_prefix=server_proxy_prefix,
**kwargs,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... Add this

Suggested change
)
)
for ki, key in enumerate(('width', 'height')):
if key not in kwargs:
kwargs[key] = f'{plotter.window_size[ki]}px'

Or something similar somewhere. FWIW this code fixes the window sizing for MNE-Python

Screenshot from 2023-01-26 12-33-12

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, nice idea!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ehh... I really want the default to be 100% rather than matching the plotter size. With these changes it makes the output not so elegant by default:

Screen Shot 2023-01-26 at 11 45 02 AM

Where as with width: 100%; height: 600px; its much nicer no matter the window size:

Screen Shot 2023-01-26 at 11 47 51 AM

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted this suggestion

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But didn't it look that way with ipyvtklink already, suggesting that this maintains previous behavior?

Okay with me in principle for 100% -- 50% didn't seem to work all that well -- assuming there is some nice way in the public API to easily specify a fixed size. What is it with the current code? It seems like the current version completely ignores Plotter(..., window_size=...) which seems unexpected/not backward compatible...

One way out of this might be to have Plotter(..., window_size='auto') as a backward compat default which means "(600px, 600px) for all backends except trame, which will use ('100%', '600px')" or something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@larsoner, are you okay with me merging as is and following up in a separate PR (before the release) to expose a better mechanism for controlling the widget size?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking here: #3879

Will try to address ASAP

@banesullivan
Copy link
Member Author

@akaszynski

Should we just change the default mapper then if it detects that it's in a notebook with either trame or client enabled? We don't have to fix it, but I also don't like empty plots.

We could... but I'd rather try to just fix this upstream and follow up with this sort of patch if necessary

Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
banesullivan and others added 2 commits January 26, 2023 11:43
Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
Copy link
Member

@akaszynski akaszynski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reapproving. Really appreciate everyone's feedback here, and if there are any issues post-merge we'll be quick to fix them in follow-up PRs.

@akaszynski
Copy link
Member

@banesullivan and @larsoner, let's merge this (after 24 ish hours green) and then follow-up with smaller PRs. I've had my fair share of 2,000+ LOC PRs and I think it will be easier to track issues and fix them in individual PRs.

@banesullivan
Copy link
Member Author

I think it will be easier to track issues and fix them in individual PRs.

Exactly. This PR is touching a bunch of code at this point and it'll be much easier to track and review further changes as new PRs

Copy link
Contributor

@larsoner larsoner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

@banesullivan banesullivan merged commit 36ab0e1 into main Jan 27, 2023
@banesullivan banesullivan deleted the feat/trame branch January 27, 2023 16:49
This was referenced Jan 31, 2023
@bilke
Copy link

bilke commented Feb 6, 2023

First thanks for this awesome new backend! But now a question: is the Trame backend available in the conda package? I get No module named 'trame' errros.

@jourdain
Copy link
Contributor

jourdain commented Feb 6, 2023

It should but it could be a WIP. I know that @banesullivan is working toward streamlining some of those conda recipes.

@banesullivan
Copy link
Member Author

The trame recipes just landed on conda-forge a few hours ago.

conda install -c conda-forge 'pyvista>=0.38.1' matplotlib ipywidgets trame jupyterlab

Should get you everything you need (just confirmed in a fresh env locally)

@bilke
Copy link

bilke commented Feb 7, 2023

Dear @banesullivan, thanks installation worked so far but I always get a 404 on the trame widget, e.g.:

[W 2023-02-07 08:20:40.754 ServerApp] 404 GET /proxy/33025/index.html?ui=P_0x7f8b6409aa10_0&reconnect=auto (3dd9915b860a49639fc672524abd9e7d@141.65.34.101) 0.57ms referer=http://[IP]:8888/lab

I tried with and without PYVISTA_TRAME_SERVER_PROXY_ENABLED="True".

@banesullivan
Copy link
Member Author

Would you please open a new issue to track this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment