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

how does BackgroundPlotter work? #139

Open
andrea-bistacchi opened this issue Mar 9, 2020 · 19 comments
Open

how does BackgroundPlotter work? #139

andrea-bistacchi opened this issue Mar 9, 2020 · 19 comments
Labels
API How things work or the architecture in general plotting General plotting/rendering topics

Comments

@andrea-bistacchi
Copy link

Dear All, I am trying to develop a Qt QTableView to show and edit interactively some data about the PyVista objects that we visualize, at the same time, in the BackgroundPlotter.

Basically we need another Qt window that is able to work in background exactly as the BackgroundPlotter.

The question is: how do you obtain the "active in background" behavior in the BackgroundPlotter? Can you please point us to the relevant lines of code or to some documentation that you have used to develop it?

Thanks very much!

@GuillaumeFavelier GuillaumeFavelier added API How things work or the architecture in general plotting General plotting/rendering topics labels Mar 17, 2020
@GuillaumeFavelier
Copy link

That's a very interesting question @bistek ! And probably @akaszynski is way more knowledgeable than me on this matter but I can still try to redirect you to the part of the code which I believe manage to initialize Qt without holding the context:

https://github.com/pyvista/pyvista/blob/master/pyvista/plotting/qt_plotting.py#L647-L663

Notice that in the case of ipython more code is needed (magic() and qt_for_kernel seem to hold the key):

from IPython import get_ipython
ipython = get_ipython()
ipython.magic('gui qt')

from IPython.external.qt_for_kernel import QtGui
QtGui.QApplication.instance()

So it's not your typical:

from PyQt5.QtWidgets import *
app = QApplication([])
...
app.exec_()

From what I know QApplication.instance() will return the current application if it exists otherwise app = QApplication(['']) will create a new one.

@andrea-bistacchi
Copy link
Author

Thanks very much!

@akaszynski
Copy link
Member

akaszynski commented Mar 18, 2020

@GuillaumeFavelier has it right. For applications outside of ipython, the trick is to grab the current QApplication or create one with:

app = QApplication.instance()
if not app:  # pragma: no cover
    app = QApplication([''])

We then create a main window and a frame and populate it with our vtk widget:

app_window = MainWindow()
frame = QFrame()

# add vtk widget to frame

frame.setLayout(vlayout)
app_window.setCentralWidget(self.frame)

Finally, we show this window

app_window.show()

At no point do we run _exec as we don't want to block the main python process. Things do get tricky sometimes with callbacks as the Qt thread sometimes conflicts with VTK, so if you implement something, signals are your friend.

@Jonas231
Copy link

Jonas231 commented Mar 25, 2020

Hi,
I have an issue concerning the background plotter:
How can I ensure that all axes have the same scaling?
I do the following:

### pyvista plotting
if 1:
    plotter = pv.BackgroundPlotter(shape=(1, 1))
    #plotter.subplot(0, 0)
    pv.create_axes_orientation_box(line_width=1, text_scale=0.366667, edge_color='black', x_color=None, y_color=None, z_color=None, xlabel='X', ylabel='Y', zlabel='Z', x_face_color='red', y_face_color='green', z_face_color='blue', color_box=False, label_color=None, labels_off=False, opacity=0.5)
    
    Sfactor = 150
    plotter.add_mesh(mesh)
    
    plotter.add_bounding_box()
    plotter.add_axes()
    plotter.add_bounds_axes()
    plotter.show()

And I get:
grafik

If the axes are scaled the same: How can I ensure that the distance between the ticks of the axes is the same?

@akaszynski
Copy link
Member

Would it be possible for you to please post your data here so I can try to reproduce this on my end?

@ttsesm
Copy link

ttsesm commented May 20, 2020

@akaszynski so if I understand it correctly in order to show a BackgroundPlotter() I need to add it inside a QApplication()? Is that correct?

Cannot be shown by itself as a standalone window as with Plotter()?

and another question is it possible to keep the a Plotter() window open and interactive and return to script to finish with the rest of code?

@akaszynski
Copy link
Member

akaszynski commented May 20, 2020

@akaszynski so if I understand it correctly in order to show a BackgroundPlotter() I need to add it inside a QApplication()? Is that correct?

Take a look at
https://github.com/pyvista/pyvista-gui/blob/91693b8e950eefeb5c86c144d14f5b2223543c45/pyvista_gui/gui.py#L108

and another question is it possible to keep the a Plotter() window open and interactive and return to script to finish with the rest of code?

The best way to do this is to use BackgroundPlotter. There's a way to do it with Plotter, but it's not very stable.

@ttsesm
Copy link

ttsesm commented May 20, 2020

To be honest I did not really get the part of the code in the link, could you please elaborate a bit more. An example also I think would be quite insightful, if possible.

@akaszynski
Copy link
Member

Here's a basic example:

"""Basic example demonstrating QtInteractor usage"""
import sys

from PyQt5.QtWidgets import QApplication, QMainWindow
from pyvista.plotting import QtInteractor
import pyvista

class PvWindow(QMainWindow):
    """ Primary GUI object """

    def __init__(self):
        """ Generate window with a dock """
        QMainWindow.__init__(self)

        # initialize the plotter
        self._plotter = QtInteractor(self)
        self.setCentralWidget(self._plotter.interactor)

        # add a mesh
        self._plotter.add_mesh(pyvista.Sphere())
        self._plotter.add_axes()


app = QApplication(sys.argv)
window = PvWindow()
window.show()
app.exec_()

@ttsesm
Copy link

ttsesm commented May 20, 2020

I am sorry but still I do not understand how this is connected with the BackgroundPlotter(). For example, I have in my script the following code snippet:

plotter = pyvista.BackgroundPlotter()
plotter.add_mesh(pyvista.Sphere())
plotter.show()

If I use run mode on PyCharm and I do not get any window and script terminates, if instead I use debug mode and put a checkpoint afterwards I am getting a window. If I use simple Plotter() I get a window both in run/debug mode.

Ultimately, what I want to achieve is to have BackgroundPlotter() showing in run mode as well and where I can interact while the script on the back continues and finishes with the code (I am not sure whether the latter is possible though, I see also other people struggling to achieve that with matplotlib for example). Apologies if my questions seem to simple :$.

@akaszynski
Copy link
Member

akaszynski commented May 20, 2020

Sorry, I was confused about your question and I think I understand now.

plotter = pyvista.BackgroundPlotter()
plotter.add_mesh(pyvista.Sphere())
plotter.show()

If you run the above via a script (i.e. $ python script.py) the window will open and immediately close afterwards. The BackgroundPlotter runs in a non-blocking thread, meaning that when your script ends, the BackgroundPlotter is basically running in a daemon thread and the python environment will immediately terminate and clean up any vtk resources. As you found, it's possible to circumvent that by blocking the script manually using a breakpoint, but that won't fix the issue.

I recommend using PyCharm's console and creating an instance of BackgroundPlotter and then running your commands within the IDE's console. If you were looking for the instance of the BackgroundPlotter to update while a script is in action, you'll have to introduce some additional callbacks within the script:

import time

import pyvista
plotter = pyvista.BackgroundPlotter()
mesh = pyvista.Sphere()
plotter.add_mesh(mesh)
plotter.show()

# demonstrate non-blocking events
for i in range(100):
    mesh.points *= 1.01
    plotter.render()
    plotter.app.processEvents()

plotter.add_text('sleeping...')
time.sleep(3)  # demonstrate blocking event

Let me know if this makes sense.

@ttsesm
Copy link

ttsesm commented May 21, 2020

Ok, that's better, but not exactly what I would like. Is it possible to keep the window open and be able to interact with with even if the script is done. For example take a Matlab development session with figure windows that come up with a responsive command prompt. Could that be possible to be done with BackgroundPlotter() and I guess multiprocessing in different threads or something.

And a second question does the BackgroudnPlotter() has any blocking argument by itself? Something like show(block = True) so that you do not need to add extra blocking events.

@akaszynski
Copy link
Member

Is it possible to keep the window open and be able to interact with with even if the script is done

You'll want to use the following at the end of the script. This effectively hands control over to the pyqt thread until the pyqt window is closed.

plotter.app.exec_()

And a second question does the BackgroudnPlotter() has any blocking argument by itself? Something like show(block = True) so that you do not need to add extra blocking events.

I'm afraid not at the moment. If you're running an interactive session (i.e. python in a console) this isn't an issue, but for your use case we'd have to add in additional hooks within BackgroundPlotter to ensure that any changes in the plotter are reflected in the window. This is something we can can add in the future.

@ttsesm
Copy link

ttsesm commented May 22, 2020

@akaszynski thanks for the feedback. I was checking on plotter.app.exec_() but it seems to just create a blocking event and not allowing the script to continue. For example in the following snippet:

import pyvista
plotter = pyvista.BackgroundPlotter()
mesh = pyvista.Sphere()
plotter.add_mesh(mesh)
plotter.show()

plotter.app.exec_()

print("hello!!!")

it will not continue to `print("hello!!!") until I terminate the window. As I said I would like to have the window available while the script continues or even terminates.

@akaszynski
Copy link
Member

At the moment, the only way you'll be able to pull up a background plotting window that updates and is interactive while your script runs is by adding:

plotter.render()
plotter.app.processEvents()

When you'd like the plot to update. It's not optimal, and we're going to have to add this feature in a future release. At the end of the script, if you want to keep the background plotter open and not have it just close, add plotter.app.exec_(). It's going to block execution, so it should be at the end of the script.

@ttsesm
Copy link

ttsesm commented May 22, 2020

@akaszynski thanks for the feedback. It will be nice to see that in a future release.

@ttsesm
Copy link

ttsesm commented May 25, 2020

For complement I've managed to have the plotting window in a different process with multiprocessing, detached from the main process. Thus, I can keep open and interact with the plotting window while the main script continues its work and/or even terminates:

import time
import multiprocessing
import os
import sys
import pyvista

def plot_mesh(data):
    print("entered plotting process")
    plotter = pyvista.BackgroundPlotter()
    mesh = pyvista.Sphere()
    plotter.add_mesh(mesh)
    plotter.show()
    plotter.app.exec_()
    print("exiting plotting process")

if __name__ == "__main__":
    print("starting __main__")
    proc = multiprocessing.Process(target=plot_mesh, args=([],))
    proc.daemon = False
    proc.start()
    time.sleep(1)
    print("exiting main")
    os._exit(0) # this exits immediately with no cleanup or buffer flushing

@andrea-bistacchi
Copy link
Author

Very interesting! Have you tried adding or removing actors to the renderer? E.g with plot_mesh.add_mesh(...) etc.

Thanks!

@ttsesm
Copy link

ttsesm commented May 25, 2020

If you render anything within the plot_mesh() and before `plotter.app_exec()' it works.

On the other hand if you do it outside and once the main process is finished then I cannot see how to communicate to the BackgroundPlotter(). I do not know if by retrieving/knowing the child process PID you could communicate to it somehow. However, there is also the `plotter.app_exec()' restriction, which means that you cannot render any further mesh on the plot unless you bypass it, which means closing the window.

Thus, I do not see how you could add multiple meshes on the same window like in a Matlab-wise development session. If anyone is aware of how possibly we could be doing that, I would appreciate any feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API How things work or the architecture in general plotting General plotting/rendering topics
Projects
None yet
Development

No branches or pull requests

5 participants