Kitware / trame-vtk

VTK/ParaView widgets for trame
BSD 3-Clause "New" or "Revised" License
17 stars 8 forks source link

View not automatically updating when two trame apps running in same jupyter notebook #60

Closed cardinalgeo closed 10 months ago

cardinalgeo commented 10 months ago

Hi all,

As suggested by @jourdain, I'm transitioning the discussion here to an issue in the trame-vtk repo. The issue is as follows:

When I run two trame apps within a jupyter notebook, only the last is automatically updated upon changing, e.g., the opacity of an actor. The first is updated only upon clicking within the viewer. In contrast, I'd expect each to update automatically, independent of the number of trame apps running. Here's a simple example that illustrates the issue:

# cell 1
from trame.app import get_server
from trame.ui.vuetify import SinglePageLayout
from trame.widgets import vuetify
import pyvista as pv

class Cone():
    def __init__(self, name):
        server = get_server(name)

        p = pv.Plotter()
        p.add_mesh(pv.Cone(), name="cone")
        self.plotter = p

        with SinglePageLayout(server) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
                    pv.trame.PyVistaRemoteView(p)

            self.server = server
            self.ui = layout

    def update_opacity(self, value): 
        self.plotter.actors["cone"].prop.opacity = value
        self.plotter.update()

# cell 2
cone_1 = Cone("1")
await cone_1.ui.ready
cone_1.ui

# cell 3
cone_2 = Cone("2")
await cone_2.ui.ready
cone_2.ui

# cell 4
cone_1.update_opacity(0.5) # cone opacity doesn't change without clicking 

# cell 5
cone_2.update_opacity(0.5) # cone opacity changes automatically

In the previous discussion, @jourdain noted that it was an issue with remote vtk and having more than one server within the same interpreter and thought that using a child_server might provide a quick fix.

cardinalgeo commented 10 months ago

Indeed, the following example, which produces a child_server from the first app for use with the second, results in the expected behavior (i.e., for both apps, the opacity of the actor updates automatically and without requiring a click).


# cell 1
from trame.app import get_server
from trame.ui.vuetify import SinglePageLayout
from trame.widgets import vuetify
import pyvista as pv

class Cone():
    def __init__(self, server=None):
        server = get_server(server)

        p = pv.Plotter()
        p.add_mesh(pv.Cone(), name="cone")
        self.plotter = p

        with SinglePageLayout(server) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
                    pv.trame.PyVistaRemoteView(p)

            self.server = server
            self.ui = layout

    def update_opacity(self, value): 
        self.plotter.actors["cone"].prop.opacity = value
        self.plotter.update()

    def make_child_server(self): 
        child_server = self.server.create_child_server()

        return child_server

# cell 2 
cone_1 = Cone()
await cone_1.ui.ready
cone_1.ui

# cell 3
child_server = cone_1.make_child_server()
cone_2 = Cone(child_server)
await cone_2.ui.ready
cone_2.ui

# cell 4 
cone_1.update_opacity(0.5) # cone opacity changes automatically

# cell 5
cone_2.update_opacity(0.5) # cone opacity changes automatically
cardinalgeo commented 10 months ago

However, trying the same child_server approach with a different trame app results in failure. In the example below, a cone app, as above, and a scatter plot app are run in the same notebook. The latter stalls at the loading phase when a child_server is used (and succeeds when get_server() is used without the child_server):

Specs: MacOS Ventura, M1 python==3.10.0 ipykernel==6.27.1 jupyter==1.0.0 plotly==5.18.0 pyvista==0.43.0 trame==3.4.0 trame-vtk==2.6.2 trame-vuetify==2.3.1 trame-plotly==3.0.2

Example:


# cell 1
from trame.app import get_server
from trame.ui.vuetify import SinglePageLayout
from trame.widgets import vuetify
import pyvista as pv

class Cone():
    def __init__(self, server=None):
        server = get_server(server)

        p = pv.Plotter()
        p.add_mesh(pv.Cone(), name="cone")
        self.plotter = p

        with SinglePageLayout(server) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
                    pv.trame.PyVistaRemoteView(p)

            self.server = server
            self.ui = layout

    def update_opacity(self, value): 
        self.plotter.actors["cone"].prop.opacity = value
        self.plotter.update()

    def make_child_server(self): 
        child_server = self.server.create_child_server()

        return child_server

# cell 2
cone_1 = Cone()
await cone_1.ui.ready
cone_1.ui

# cell 3
from plotly import express as px
from trame.widgets import plotly
import pandas as pd

class ScatterPlot:
    def __init__(self, data, x, y, server="cat", *args, **kwargs):
        server = get_server(server)
        self.ctrl = server.controller

        self.data = data
        fig = px.scatter(data, x=x, y=y, **kwargs)
        self.fig = fig

        with SinglePageLayout(server) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True,classes="fill-height pa-0 ma-0"):
                    html_plot = plotly.Figure()
                    self.ctrl.plotly_plot_view_update = html_plot.update

        self.server = server
        self.ui = layout

        self.ctrl.plotly_plot_view_update(fig)

# cell 4
data = pd.DataFrame({"x":[0,1,2], "y":[0,1,2]})
child_server = cone_1.make_child_server()

scatter = ScatterPlot(data, "x", "y", server=child_server)
await scatter.ui.ready
scatter.ui

Any ideas on why the child_server approach fails here, as well as what a more flexible solution to this general dilemma would be?

jourdain commented 10 months ago

Well make_child_server should be used with a prefix to prevent state collision (purpose of child server). Then the main UI should also be used with a different template name. Otherwise, the ui(s) will collide.

Not sure if it relates to what you are seeing (being stuck), but definitely something to consider when properly testing this use case.

jourdain commented 10 months ago

BTW, thanks for reporting in the issue.

cardinalgeo commented 10 months ago

Thanks for the corrections Seb — I've implemented them in the example below (I think!), though they do not resolve the "stuck" ScatterPlot app.

# cell 1
from trame.app import get_server
from trame.ui.vuetify import SinglePageLayout
from trame.widgets import vuetify
import pyvista as pv

class Cone():
    def __init__(self, name=None, server=None):
        self.name = name
        server = get_server(server)
        server.client_type = "vue2"

        p = pv.Plotter()
        p.add_mesh(pv.Cone(), name="cone")
        self.plotter = p

        with SinglePageLayout(server, template_name=self.name) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
                    pv.trame.PyVistaRemoteView(p)

            self.server = server
            self.ui = layout

    def update_opacity(self, value): 
        self.plotter.actors["cone"].prop.opacity = value
        self.plotter.update()

    def make_child_server(self): 
        child_server = self.server.create_child_server(prefix=self.name)

        return child_server

# cell 2
cone_1 = Cone(name="cone_1")
await cone_1.ui.ready
cone_1.ui

# cell 3
from plotly import express as px
from trame.widgets import plotly
import pandas as pd

class ScatterPlot:
    def __init__(self, data, x, y, name=None, server="cat", *args, **kwargs):
        self.name = name
        server = get_server(server)
        server.client_type="vue2"
        self.ctrl = server.controller

        self.data = data
        fig = px.scatter(data, x=x, y=y, **kwargs)
        self.fig = fig

        with SinglePageLayout(server, template_name=self.name) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True,classes="fill-height pa-0 ma-0"):
                    html_plot = plotly.Figure()
                    self.ctrl.plotly_plot_view_update = html_plot.update

        self.server = server
        self.ui = layout

        self.ctrl.plotly_plot_view_update(fig)

# cell 4
data = pd.DataFrame({"x":[0,1,2], "y":[0,1,2]})
child_server = cone_1.make_child_server()

scatter = ScatterPlot(data, "x", "y", name="scatter", server=child_server)
await scatter.ui.ready
scatter.ui
cardinalgeo commented 10 months ago

Interestingly, when I run two of the ScatterPlot apps in the same jupyter notebook, the "stuck loading" problem disappears and everything works fine! Any ideas on why this would be the case (i.e., that running two Cone apps works, two ScatterPlot apps works, but a Cone app and ScatterPlot app doesn't)? If this is moving away from the original issue, I can open a new one (here? or in vtk-server)?

Two ScatterPlot example:

# cell 1
from trame.app import get_server
from trame.ui.vuetify import SinglePageLayout
from trame.widgets import vuetify, plotly
import pyvista as pv
from plotly import express as px
import pandas as pd

class ScatterPlot:
    def __init__(self, data, x, y, name=None, server="cat", *args, **kwargs):
        self.name = name
        server = get_server(server)
        server.client_type="vue2"
        self.ctrl = server.controller

        self.data = data
        fig = px.scatter(data, x=x, y=y, **kwargs)
        self.fig = fig

        with SinglePageLayout(server, template_name=self.name) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True,classes="fill-height pa-0 ma-0"):
                    html_plot = plotly.Figure()
                    self.ctrl.plotly_plot_view_update = html_plot.update

        self.server = server
        self.ui = layout

        self.ctrl.plotly_plot_view_update(fig)

    def make_child_server(self): 
        child_server = self.server.create_child_server(prefix=self.name)

        return child_server

# cell 2
data_1 = pd.DataFrame({"x":[0,1,2], "y":[0,1,2]})

scatter_1 = ScatterPlot(data_1, "x", "y", name="scatter_1")
await scatter_1.ui.ready
scatter_1.ui

# cell 3
data_2 = pd.DataFrame({"x":[0,1,2], "y":[0,1,2]})
child_server = scatter_1.make_child_server()

scatter_2 = ScatterPlot(data_2, "x", "y", name="scatter_1", server=child_server)
await scatter_2.ui.ready
scatter_2.ui
jourdain commented 10 months ago

They are indeed different problems.

But since you are using the same server (child), you don't need to await the second app. If you skip it, does it work? If so, I just need to fix the logic on the ready part with child server....

cardinalgeo commented 10 months ago

I commented out await scatter.ui.ready in the Cone-ScatterPlot example, but the "stuck loading" problem persists. I'll go ahead and open another issue on this child_server problem so that the current issue can stay on topic for the more general problem of running two apps in a jupyter notebook (i.e., without needing to use a child_server). Would this child_server issue be best opened in this repo or trame-server (or another)?

jourdain commented 10 months ago

Sorry looking at your code and you are missing the point of the prefix for the child server.

server = get_server()
s1 = server.create_child_server("s1")
s2 = server.create_child_server("s2")

app1 = ScatterPlot(s1)
app2 = ScatterPlot(s2)

The prefix is "the same thing" as get_server("s1") and get_server("s2").

But then if you don't assign a template_name= for your layout, they will collide.

cardinalgeo commented 10 months ago

Sorry about that — typo on my part! Fixing it still produces the same behavior, in that the ScatterPlot-ScatterPlot example behaves correctly as it did before. Fortunately, I did not make that mistake in the the previous, Cone-ScatterPlot example (in that each passes a different name for both the prefix and template_name). Unfortunately, it means that example is still producing the "stuck loading" behavior.

cardinalgeo commented 10 months ago

Actually, if I try the pattern you've outlined (i.e., creating the server followed by the child server(s), and then passing the child server(s) when instantiating the app object(s)), a different error arises — the iframe is blank with the error message JS Error => ReferenceError: view_P_0x1467b3640_1Id is not defined at the bottom. An example that reproduces this:

# cell 1
from trame.app import get_server
from trame.ui.vuetify import SinglePageLayout
from trame.widgets import vuetify
import pyvista as pv

class Cone():
    def __init__(self, name=None, server=None):
        self.name = name
        p = pv.Plotter()
        p.add_mesh(pv.Cone(), name="cone")
        self.plotter = p

        with SinglePageLayout(server, template_name=self.name) as layout:
            with layout.content:
                with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
                    pv.trame.PyVistaRemoteView(p)

            self.server = server
            self.ui = layout

# cell 2
server = get_server()
server.client_type = "vue2"
s1 = server.create_child_server(prefix="s1")

# cell 3
cone_1 = Cone(name="cone_1", server=s1)
await cone_1.ui.ready
cone_1.ui

Just to clarify, would it be best to split off these child_server problems into a separate issue (and in which repo)?

jourdain commented 10 months ago

It seems to me that there is 3 issue:

  1. remote rendering with vtk inside the same interpreter but with different server (trame-vtk issue)
  2. child_server not working for you (trame-server issue but could be reported on trame) Just tested and it was fine on my end.
  3. the locking thing (tested and it seems to be working with me.
jourdain commented 10 months ago

I was aware of the 1. I would like to reproduce the locking issue. But doesn't seem to be an issue on my machine unless I'm not doing what you are doing. Then the child_server it is a separate topic.

jourdain commented 10 months ago

Steps to reproduce that issue

from trame.app import get_server
from trame.decorators import TrameApp, change
from trame.widgets import vuetify, vtk as vtk_widgets
from trame.ui.vuetify import SinglePageLayout

from vtkmodules.vtkFiltersSources import vtkConeSource
from vtkmodules.vtkRenderingCore import (
    vtkRenderer,
    vtkRenderWindow,
    vtkRenderWindowInteractor,
    vtkPolyDataMapper,
    vtkActor,
)

# VTK factory initialization
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch  # noqa
import vtkmodules.vtkRenderingOpenGL2  # noqa

# -----------------------------------------------------------------------------

@TrameApp()
class Cone:
    def __init__(self, server_or_name=None):
        self.server = get_server(server_or_name, client_type="vue2")
        self._vtk_rw, self._vtk_cone = self._vtk_setup()
        self.ui = self._generate_ui()

    @property
    def ctrl(self):
        return self.server.controller

    @property
    def state(self):
        return self.server.state

    @change("resolution")
    def on_resolution_change(self, resolution, **kwargs):
        self._vtk_cone.SetResolution(resolution)
        self.ctrl.view_update()

    @property
    def resolution(self):
        return self.state.resolution

    @resolution.setter
    def resolution(self, v):
        with self.state:
            self.state.resolution = v

    def reset_resolution(self):
        self.resolution = 6

    def _vtk_setup(self):
        renderer = vtkRenderer()
        renderWindow = vtkRenderWindow()
        renderWindow.AddRenderer(renderer)
        renderWindow.OffScreenRenderingOn()

        renderWindowInteractor = vtkRenderWindowInteractor()
        renderWindowInteractor.SetRenderWindow(renderWindow)
        renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()

        cone_source = vtkConeSource()
        mapper = vtkPolyDataMapper()
        actor = vtkActor()
        mapper.SetInputConnection(cone_source.GetOutputPort())
        actor.SetMapper(mapper)
        renderer.AddActor(actor)
        renderer.ResetCamera()
        renderWindow.Render()

        return renderWindow, cone_source

    def _generate_ui(self):
        with SinglePageLayout(self.server) as layout:
            layout.title.set_text("Trame demo")
            with layout.toolbar as toolbar:
                toolbar.dense = True
                vuetify.VSpacer()
                vuetify.VSlider(
                    v_model=("resolution", 6),
                    min=3,
                    max=60,
                    step=1,
                    hide_details=True,
                    style="max-width: 300px;",
                )
                with vuetify.VBtn(icon=True, click=self.reset_resolution):
                    vuetify.VIcon("mdi-lock-reset")
                with vuetify.VBtn(icon=True, click=self.ctrl.view_reset_camera):
                    vuetify.VIcon("mdi-crop-free")

            with layout.content:
                with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
                    view = vtk_widgets.VtkRemoteView(self._vtk_rw)
                    self.ctrl.view_update = view.update
                    self.ctrl.view_reset_camera = view.reset_camera

            return layout

c1 = Cone("c1")
await c1.ui.ready
c1.ui.iframe_style = "border: none; width: 100%; height: 200px;"
c1.ui

c2 = Cone("c2")
await c2.ui.ready
c2.ui.iframe_style = "border: none; width: 100%; height: 200px;"
c2.ui
jourdain commented 10 months ago

BTW, I just fixed it... I just need to clean the code and check that I didn't break some use cases...

cardinalgeo commented 10 months ago

Steps to reproduce that issue

^I'm not sure I understand which issue is which per your wording. I can see that your code reproduces the behavior that prompted the original discussion in the main Trame repo — Is this referring to the locking issue or the vtk remote rendering issue?

BTW, I just fixed it... I just need to clean the code and check that I didn't break some use cases...

Ah great! So you've resolved the issue from the original discussion (as you reproduced above)?

jourdain commented 10 months ago

fixed in 2.7.0

cardinalgeo commented 9 months ago

Can confirm that the original multi-server problem is resolved on my end!

cardinalgeo commented 9 months ago

Let me know if it's still worth creating issues for the child server problem (e.g., the example referenced below still fails for me):

Actually, if I try the pattern you've outlined (i.e., creating the server followed by the child server(s), and then passing the child server(s) when instantiating the app object(s)), a different error arises — the iframe is blank with the error message JS Error => ReferenceError: view_P_0x1467b3640_1Id is not defined at the bottom.

jourdain commented 9 months ago

You can but when I tested on my end (with my code and scenario), it was fine. So you will need to provide the full example that is failing for you.