Search code examples
pythonvisualizationvispy

Vispy: Using timers for visualizations wrapped in functions/classes


I am currently working on using Vispy to add visualization capabilities to a Python simulation library. I have managed to get some basic visualizations running with the data from the simulations, and am now looking at wrapping it in functions/classes so users of the libraries easily visualize the simulation (by passing the data in a specific format or something) without having to code it themselves.

However, I am having trouble figuring out the right way/best practice to get the timers working properly to update the objects as they change in time.

For example, when running the visualization as script, an example of how I have implemented the timer is using global variables and iterators similar to how it is done in the Vispy Scene "Changing Line Colors" demo on the Vispy website :

def on_timer(event):
    global colormaps, line, text, pos
    color = next(colormaps)
    line.set_data(pos=pos, color=color)
    text.text = color

timer = app.Timer(.5, connect=on_timer, start=True)

But when I wrap the entire visualization script in a function/class, I am having trouble getting the timer to work correctly given the difference in scopes of the variables now. If anyone could give some idea of the best way to achieve this that would be great.

EDIT:

I have made an extremely simplified visualization that might be similar to some of the expected use cases. The code when run as a stand alone python script is:

import numpy as np
from vispy import app, scene

# Reproducible data (oscillating wave)
time = np.linspace(0, 5, 300)
rod_positions = []

for t in time:

    wave_position = []
    for i in range(100):
        wave_position.append([0, i, 20 * (np.cos(t + i / 10))])

    rod_positions.append(wave_position)

rod_positions = np.array(rod_positions)

# Prepare canvas
canvas = scene.SceneCanvas(keys="interactive", size=(800, 600), bgcolor="black")
canvas.measure_fps()

# Set up a view box to display the image with interactive pan/zoom
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera()

rod = scene.visuals.Tube(
    points=rod_positions[0], radius=5, closed=False, tube_points=16, color="green",
)
view.add(rod)
view.camera.set_range()

text = scene.Text(
    f"Time: {time[0]:.4f}",
    bold=True,
    font_size=14,
    color="w",
    pos=(80, 30),
    parent=canvas.central_widget,
)

update_counter = 0
max_updates = len(time) - 1

def on_timer_update(event):

    global update_counter

    update_counter += 1

    if update_counter >= max_updates:

        timer.stop()
        canvas.close()

    # Update new rod position and radius

    rod_new_meshdata = scene.visuals.Tube(
        points=rod_positions[update_counter],
        radius=5,
        closed=False,
        tube_points=16,
        color="green",
    )._meshdata
    rod.set_data(meshdata=rod_new_meshdata)

    text.text = f"Time: {time[update_counter]:.4f}"

# Connect timer to app
timer = app.Timer("auto", connect=on_timer_update, start=True)

if __name__ == "__main__":

    canvas.show()
    app.run()

My attempt at wrapping this in a class is the following:

import numpy as np
from vispy import app, scene


class Visualizer:

    def __init__(self, rod_position, time) -> None:
        
        self.rod_positions = rod_position
        self.time = time

        self.app = app.application.Application()

        # Prepare canvas
        self.canvas = scene.SceneCanvas(keys="interactive", size=(800, 600), bgcolor="black")
        self.canvas.measure_fps()

        # Set up a view box to display the image with interactive pan/zoom
        self.view = self.canvas.central_widget.add_view()
        self.view.camera = scene.TurntableCamera()

        self.update_counter = 0
        self.max_updates = len(time) - 1

    def _initialize_objects(self):

        self.rod = scene.visuals.Tube(
            points=self.rod_positions[0], radius=5, closed=False, tube_points=16, color="green",
        )
        self.view.add(self.rod)
        self.view.camera.set_range()

        self.text = scene.Text(
            f"Time: {self.time[0]:.4f}",
            bold=True,
            font_size=14,
            color="w",
            pos=(80, 30),
            parent=self.canvas.central_widget,
        )


    def update_timer(self, event):

        self.update_counter += 1

        if self.update_counter >= self.max_updates:

            self.timer.stop()
            self.canvas.close()

        # Update new rod position and radius

        rod_new_meshdata = scene.visuals.Tube(
            points=self.rod_positions[self.update_counter],
            radius=5,
            closed=False,
            tube_points=16,
            color="green",
        )._meshdata
        self.rod.set_data(meshdata=rod_new_meshdata)

        self.text.text = f"Time: {self.time[self.update_counter]:.4f}"

    def run(self):

        self._initialize_objects()
        # Connect timer to app
        self.timer = app.Timer("auto", connect=self.update_timer, start=True, app=self.app)
        self.canvas.show()
        self.app.run()


if __name__ == "__main__":

    # Reproducible data (oscillating wave)
    time = np.linspace(0, 5, 150)
    rod_positions = []

    for t in time:

        wave_position = []
        for i in range(100):
            wave_position.append([0, i, 20 * (np.cos(2 * t + i / 10))])

        rod_positions.append(wave_position)

    rod_positions = np.array(rod_positions)

    Visualizer = Visualizer(rod_positions, time)
    Visualizer.run()

It seems to be working now which is good. However this is a minimal reproduction of my problem, so I would just like to make sure that this is the optimal/intended way. As a side note, I also feel as if my way of updating the rod by generating the meshdata and updating the rod in the view with this meshdata is not optimal and slowing the visualization down (it runs at around 10fps). Is the update loop itself optimal?

Thanks


Solution

  • Thanks for updating your question with your example. It makes it much easier to answer and continue our conversation. I started a pull request for VisPy about this topic where I add some examples that use Qt timers and Qt background threads. I hope to finish the PR in the next month or two so if you have any feedback please comment on it:

    https://github.com/vispy/vispy/pull/2339

    Timers

    In general timers are a good way to update a visualization in a GUI event loop friendly way. However, in most GUI frameworks the timer is executed in the current GUI thread. This has the benefit that you can call GUI/drawing functions directly (they are in the same thread), but has the downside that any time you spend in your timer callback is time you're taking away from the GUI framework in order to respond to user interaction and redraw the application. You can see this in my timer example in the above PR if you uncomment the extra sleep line. The GUI basically stops responding while it is running the timer function.

    I have a little message on GUI threads and event loops in the vispy FAQ here.

    Threads

    Threads are a good way around the limitations of timers, but come with a lot of complexities. Threads are also usually best done in a GUI-framework specific manner. I can't tell from your post if you are doing Qt or some other backend, but you will probably get the most bang for your buck by using the GUI framework specific thread functionality rather than generic python threads. If an application that works across GUI frameworks is what you're looking for then this likely isn't an option.

    The benefit of using something like a QThread versus a generic python thread is that you can use things like Qt's signals and slots for sending data/information between the threads.

    Note that as I described in the FAQ linked above that if you use threads you won't be able to call the set_data methods directly in your background thread(s). You'll need to send the data to the main GUI thread and then have it call the set_data method.

    Suggestions

    1. You are correct that your creation of a new Tube visual that you only use for extracting mesh data is a big slow down. The creation of a Visual is doing a lot of low-level GL stuff to get ready to draw the Visual, but then you don't use it so that work is essentially wasted time. If you can find a way to create the MeshData object yourself without the Visual you should see a speed up, but if you have a lot of data switching to threads is likely still needed.
    2. I'm not sure how much this example's design resembles what you're really doing, but I would consider keeping the Application, Timer, and Visualizer classes separate. I'm not sure I can give you a good reason why, but something about creating these things in a singe class worries me. In general I think the programming concept of dependency injection applies here: don't create your dependencies inside your code, create them outside and pass them as arguments to your code. Or in the case of the Application and Timer, connect them to the proper methods on your class. If you have a much larger application then it may make sense to have a class that wraps all of the Vispy canvas and data updating logic, another one just for the outer GUI window, and then another for a data generation class. Then in the bottom if main block of code create your application, your timer, your canvas wrapper, your main window (passing the canvas wrapper if needed), and connect them all together.

    Other thoughts

    This is a problem I want to make easier for vispy users and scientific python programmers in general so let me know what I'm missing here and we'll see what kind of ideas we can come up with. Feel free to comment and provide feedback on my pull request. Do you have other ideas for what the examples should be doing? I have plans for additional fancier examples in that PR, but if you have thoughts about what you'd like to see let me know.