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