Related
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
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
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.
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.
Goal:
To start multiple animations all at once.
Expected Result:
All animation defined and started at once must run parallel.
Actual Result:
The only animation that has been started first shows, the below animations doesn't even start.
The Code for Reference:
class KivySplash(Screen):
def __init__(self, **kwargs):
super(KivySplash, self).__init__(**kwargs)
anim1 = MyAnimation(duration=4, opacity=0)
anim1.bind(on_complete=self.on_anim1_complete)
self.animation = MyAnimation(duration=3) + MyAnimation(duration=4, opacity=1) + MyAnimation(duration=5) + anim1
self.img1 = Image(source=os.path.join(original_dir, "Kivy-logo-black-512.png"), opacity=0)
self.img2 = Image(source=os.path.join(original_dir, "python-powered-w-200x80.png"))
self.label1 = Label(text="Powered by:", font_size=48)
box_layout = BoxLayout(orientation="vertical")
box_layout1 = BoxLayout()
box_layout.add_widget(self.label1)
box_layout1.add_widget(self.img1)
box_layout1.add_widget(self.img2)
box_layout.add_widget(box_layout1)
self.add_widget(box_layout)
def on_anim1_complete(self, *args):
do_nothing(self, *args)
if self.img1 in self.animation.animated_widgets:
pass
def on_enter(self, *args):
self.animation.start(self.img1)
self.animation.start(self.img2)
Thanking You.
I believe you have encountered a bug in the kivy Animation. If you are just using a simple Animation, then starting that Animation on multiple Widgets should work fine. The bug happens when you are using a Sequence (Animations connected with '+'). Sequences work by running the first Animation and binding an internal on_complete method that starts the next Animation in the Sequence. When you call start, that on_complete method is bound. But as soon as the first Animation on the first Widget in the Sequence completes, the second Animation is started and the on_complete method is unbound. Now, when the first Animation on the second Widget completes, the on_complete is not called (is was unbound after the first Widget completed), and the second Animation is not started.
Here is the code from Sequence:
def on_anim1_complete(self, instance, widget):
self.anim1.unbind(on_complete=self.on_anim1_complete)
self.anim2.start(widget)
In your case, it looks like the Animation is not starting on the second Widget, but because your first Animation doesn't actually animate anything, you don't see it.
Unfortunately, there are not many alternatives to avoid this problem.
You can build a copy of the entire Animation a second time (copy() or deepcopy() will not work), and just use two different animations (one for each Widget).
You can do your own sequencing by just using simple Animations and use your own on_complete to start the next Animation. Conveniently, the on_complete arguments includes the animated widget that you need for the next start() call.
In some situations, you may be able to animate a single container (like a Layout). Since that is only animating a single Widget, the sequencing should work correctly.
I would like to make a python tkinter window with custom-moving widgets on a canvas to simulate motion. For now, I have one canvas, and one not-moving oval widget. I am having problems at the base level; mainloop(). I understand that it runs in wait for the user to do something, but I am having a hard time seeing:
How to control/see exactly what code mainloop() is reiterating (where, and only tkinter?);
How to properly interrupt it and return to it from another function, if it doesn't do it itself;
What code should be reiterated? All tkinter objects, or only updating changing ones? Use some kind of update operation instead? Finally;
What is the functionality difference between tkinter.mainloop() and window.mainloop()? Perhaps the previous questions will answer.
I have minor experience with Swift, and started learning the very similar Python yesterday evening. I've tried probably hundred of mutations to my code, which currently is in the test stage. I have moved everything in and out of the apparent range of the mainloop, and even got several hundred tiny Python windows all over the screen. Everything does one of two things: it does nothing, or gives me an error. Since I don't know what is even running, or if it is running, I can't diagnose anything. My goal is simply to move a circle one hundred pixels repeatedly. I've scanned around for sources, but—it may be me—a clear one is scarce. I have my code here all marked up. This page is closest to what I am looking for: Move a ball inside Tkinter Canvas Widget (simple Arkanoid game). Everything appears to be under mainloop. So, everything is redrawn every pass? Here, unfortunately, is my whole script; I can't only show pieces. It, for some reason, only brings up a small window, not a full-screen one. (Edit: I seem to have lost the screen size code)
import tkinter
import time
# Initial values for circle's corners and start idicator ('b'):
x1 = 10
y1 = 10
x2 = 210
y2 = 210
b = 0
# Window ('window')
window = tkinter.Tk()
# Canvas ('area')
area = tkinter.Canvas(window, width=1368, height=650)
area.place(x=0, y=0)
# Ovals to be placed on 'area'
oval1 = area.create_oval(x1,y1,x2,y2,fill='#42befe')
oval2 = area.create_oval(100,10,300,210,fill='#d00000')
# Turns b to 1 to start shifting when 'butt' is pressed:
def startFunc():
b = 1
print('b = 1')
# My button to activate 'startFunc'
butt = tkinter.Button(window, text='Start movement', command=startFunc)
butt.pack()
# Adjusts the x and y coordinates when they are fed in:
def Shift(A, B, C, D):
print('Shift activated.')
window.after(1000)
print('Edit and return:')
A += 100
B += 100
C += 100
D += 100
return(A, B, C, D)
# Problems start about here: my Mainloop section;
# I have little idea how this is supposed to be.
while True:
if b == 1:
# Takes adjusted tuple
n = Shift(x1, y1, x2, y2)
print('Returned edited tuple.')
# Changes coordinates
x1 = n[0]
y1 = n[1]
x2 = n[2]
y2 = n[3]
print(f'{x1}, {y1}, {x2}, and {y2}')
# Reiterate moving oval
oval1 = area.create_oval(x1,y1,x2,y2,fill='#42befe')
#Does this re-run 'window' relations outside here, or only within the 'while'?
window.mainloop()
It ought to show a 1368 by 650 window, not a tiny one. The button does nothing but print, which means the final 'while' is not running, despite the mainloop. It want it to loop inside the 'while' line, which should adjust coordinates and move my blue circle. The iteration may NOT touch the initial values, or else it would reset them.
In effect, calling mainloop is the same as if you added this to your code instead of calling mainloop():
while the_program_is_running():
event = wait_for_event()
process_the_event(event)
As a rule of thumb, mainloop() should be called exactly once after the UI has initialized and you are ready for the user to start interacting with your program. When it exits, you typically won't have any code after it, and your program will exit.
How to control/see exactly what code mainloop() is reiterating (where, and only tkinter?);
I don't know what you mean by "reiterating". It doesn't run any code except it's own internal code. It simply waits for events, and then dispatches them to handlers.
How to properly interrupt it and return to it from another function, if it doesn't do it itself;
It's exceedingly rare to do this in a running program. Typically, calling mainloop is the last thing your program does before the user starts interacting with it, and as soon as it exits your program quits.
However, to answer the specific answer of how to interrupt it, you can call the quit method of the root window. That will cause the most recent call to mainloop() to return.
What code should be reiterated? All tkinter objects, or only updating changing ones? Use some kind of update operation instead?
That question is hard to answer because it doens't make much sense. When you call mainloop(), it will watch for all events on all tkinter objects.
What is the functionality difference between tkinter.mainloop() and window.mainloop()
They have exactly the same effect and behavior. Tkinter oddly chose to make mainloop available from any widget. The most common way to call it is from either the tkinter module itself, or from the root window.
My goal is simply to move a circle one hundred pixels repeatedly.
The normal way to do that is to create a function that moves it one hundred pixels. Then, that function (-- or a function that calls it -- can put itself on an event queue to be run in the future.
For example, the following code will move a canvas object 100 pixels every second until the program exits:
def move_object():
the_canvas.move(item_id, 100, 0)
the_canvas.after(1000, move_object)
When it is called, it will move the item 100 pixels to the right. Then, it will place a new call to itself on the event queue to be picked up and handled in approximately 1000 milliseconds.
There are many working examples of using after on this site, including the question you linked to in your question.
Everything appears to be under mainloop. So, everything is redrawn every pass?
No, not exactly. The only objects that are redrawn are things that need to be redrawn. Moving objects on a canvas, resizing a window, dragging another window over your window, etc, all place an event on the event queue that tells tkinter "this object needs to be redrawn". The processing of that event happens automatically by mainloop. If nothing is happening in your application, nothing gets redrawn by mainloop.
It ought to show a 1368 by 650 window, not a tiny one
That is because you haven't given the main window a size. You've given the canvas a size, but you're using place which won't cause the containing window to grow or shrink to fit. As a beginner, you should completely avoid place and instead use pack or grid, because pack and grid will both automatically size your window to fit everything inside.
While it's tempting to use place for its perceived simplicity, in reality it usually requires you to do a lot more work than if you used one of the other geometry managers, and it results in a GUI that isn't particularly responsive to change.
while True:
You should almost never do this in tkinter. Tkinter -- and almost all event based programs -- rely on a steady flow of events. When you have an infinite loop, it cannot process those events. You can put an explicit call to update the screen inside your loop, but that is inefficient and should be avoided. If you need to do something periodically, create a function that encapsulates the body of your loop, then use after to get mainloop to run it while it is processing events.
window.after(1000)
You should almost never use after this way without a second argument. This usage is functionally no different than calling time.sleep(1) in that it prevents mainloop from processing events. You should structure your code to allow for a steady stream of events to be processed by mainloop.
while True: ... window.mainloop()
You definitely need to avoid calling mainloop inside a loop. A well behaved tkinter program should call mainloop() exactly once.
I am working with PyQt5 and have some hard times using QOpenGLWidget. The problem is that the only way to draw anything in QOpenGLWidget is to use its paintGL() method, which seems to be broken. Here's what happens: once the program starts the widget refreshes itself exactly 4 times and stops. The only way to make it work again is to change the active window (switch it to terminal or anything), then it draws 2 next frames. Switch the window back - get next 2 frames - and so on. Does anyone have a clue what is happening there? Or maybe how to avoid the issue?
I solved the problem by creating a BasicTimer object which is by deafult bound to QOpenGLWidget.timerEvent() method. In the timerEvent method which runs each timer tick I then call update() method for the widget to refresh itself. Here's a code snippet that should give you a general idea:
from PyQt5.QtWidgets import QOpenGLWidget
from PyQt5.QtCore import QBasicTimer
class OpenGLWidget(QOpenGLWidget):
def __init__(self):
self._timer = QBasicTimer() # creating timer
self._timer.start(1000 / 60, self) # setting up timer ticks to 60 fps
def paintGL(self):
pass # some painting code here
def timerEvent(self, QTimerEvent):
self.update() # refreshing the widget
For my class, I am creating a "Mandelbrot Explorer" program. There is one main issue: I lose control of the GUI (all written in Tkinter/Ttk, in Python 2.7) when actually drawing to the Canvas.
Here is my code:
# There is some code above and below, but only this is relevant
for real, imag in graph.PlaneIteration(self.graph.xMin, self.graph.xMax, resolution, self.graph.yMin, self.graph.yMax, resolution, master = self.graph, buffer_action = self.graph.flush):
# the above line iterates on the complex plane, updating the Canvas for every x value
c = complex(real, imag)
function, draw, z, current_iter = lambda z: z**2 + c, True, 0, 1
while current_iter <= iterations:
z = function(z)
if abs(z) > limit:
draw = False
break
current_iter += 1
self.progressbar.setValue(100 * (real + self.graph.xMax) / total)
color = self.scheme(c, current_iter, iterations, draw)
# returns a hex color value
self.graph.plot(c, color)
# self.graph is an instance of my custom class (ComplexGraph) which is a wrapper
# around the Canvas widget
# self.graph.plot just creates a line on the Canvas:
# self.create_line(xs,ys,xs+1,ys+1, fill=color)
My issue is that when run, the graphing takes a while - about 30 seconds. In this time, I cannot use the GUI. If I try to, the window freezes and only unfreezes once the drawing is done.
I tried using threading (I enclosed the entirety of the upper code in a function, thread_process):
thread.start_new_thread(thread_process, ())
However, the problem remains.
Is there a way to fix this? Thanks!
You can execute your loop "threaded" with Tkinter by implicitly returning to Tkinter's main loop execution after every point your draw. Do this by using widget.after to register the next function call:
plane = graph.PlaneIteration(...)
def plotNextPoint():
try:
real, imag = plane.next()
except StopIteration:
return
c = complex(real, imag)
...
self.graph.plot(c, color)
self.graph.after(0, plotNextPoint)
plotNextPoint()
This way, after each point you draw, the Tkinter mainloop will run again and update the display before calling your plotNextPoint function again. If this is too slow, try wrapping the body of plotNextPoint in a for _ in xrange(n) loop to draw n points between redraws.
You're right about the cause of the problem—the GUI event loop is not running while you're busy running this code.
And you're right about threading being a good solution. (The other major solution is to break the job up into smaller subtasks and have each one schedule the next. For a more detailed overview of the options and all of the wrinkles, see Why your GUI app freezes.)
But it's not quite as simple as putting the whole thing on a thread.
Unfortunately, Tkinter (like many GUI frameworks) is not free-threaded. You cannot call methods on any GUI objects from a background thread. If you do, different things happen on different platforms and versions, ranging from blocking the main thread to crashing the program to raising exceptions.
Also, remember that, even without Tkinter, you can't safely share mutable objects between threads without some kind of synchronization. And you're doing exactly that with the Tkinter objects, right?
The Tkinter wiki explains one way to get around both of these problems at once in Tkinter and Threads: Create a Queue, have the background thread put messages on it, and have the main thread check it every so often (e.g., by using after to schedule a nonblocking get every 100ms until the background thread is done).
If you don't want to come up with a "protocol" for passing data from the background thread to the main thread, remember that in Python, a bound method, or a tuple of a bound method and some arguments, it perfectly good, passable data. So, instead of calling self.graph.plot(c, color), you can just self.q.put((self.graph.plot, c, color)).
The library mtTkinter wraps this all up for you, making it look like Tkinter is free-threaded by using a Queue in the background. It isn't highly tested or frequently maintained, but even if it doesn't work in the future it still makes great sample code.