resonance icon indicating copy to clipboard operation
resonance copied to clipboard

Matplotlib 2.1 includes a JS Widget for animations

Open moorepants opened this issue 7 years ago • 13 comments

See: http://matplotlib.org/users/whats_new.html#interactive-js-widgets-for-animation

This would likely work better than what we are doing.

moorepants avatar Oct 08 '17 04:10 moorepants

It is a pretty slick interface and will play back with more reliable timing. I think it would also make the animations interactive in the served notebooks. It would also remove the need for the notebook backend.

But as far as I can tell it's pre-rendering the HTML5 video, so on my laptop it takes almost 3 minutes to render ~1000 frames of a pretty sparse animation.

ixjlyons avatar Oct 08 '17 16:10 ixjlyons

Ah, the time to render. That's not that fun. What code did you type to get it to work?

moorepants avatar Oct 08 '17 19:10 moorepants

I tried this out with the example I have in the nonlinear systems branch. Open notebook and do:

from resonance.tests.test_nonlinear_systems import sys, sample_rate

ani = sys.animate_configuration(interval=1.0 / sample_rate * 1000)  # interval should be in milliseconds

from IPython.display import HTML

HTML(ani.to_jshtml())

It works but it doesn't play the full 5 seconds. It stops around 3.5 seconds or so and plays really fast. It isn't clear to me how the number of frames, the interval kwarg, and fps kwarg interact, as it seems to affect how many frames are played. I generate 151 frames that are spaced in simulation time 1/30 seconds apart. I set the interval kwarg to 1/30*1000 milliseconds and the animation doesn't seem to play all of the frames.

I'm not quite sure how to fix it. But it didn't take too awful long to build the HTML, and the controls make it quite nice.

moorepants avatar Oct 08 '17 21:10 moorepants

Here's an example:

import numpy as np
from matplotlib import animation
from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
from IPython.display import HTML

def animate(frames, fps):
    dt = 1 / fps
    theta = np.linspace(0, 2*np.pi, frames)
    
    fig, ax = plt.subplots(1, 1)
    rect = Rectangle((0, 0), 1, 0.1)
    ax.add_patch(rect)
    ax.set_xlim(-1, 1)
    ax.set_ylim(-1, 1)
    ax.set_aspect('equal')
    
    def update(i):
        rect.angle = np.rad2deg(theta[i])
        return rect,
    
    return animation.FuncAnimation(fig, update, frames=frames, interval=dt)

frames = 300
fps = 30
HTML(animate(frames, fps).to_jshtml(fps=fps))

This plays with correct timing for me. Took about 30 seconds to render on my (somewhat old) i7 with 2 cores/4 threads.

ixjlyons avatar Oct 08 '17 22:10 ixjlyons

Also note that matplotlib.patches.Rectangle now has a proper angle attribute. Careful using hidden attributes ;)

ixjlyons avatar Oct 08 '17 22:10 ixjlyons

So is my fps / 1000 incorrect above? The docs says interval is supposed to be milliseconds between frames. If fps is "frames per second" don't you need a factor of 1000 somewhere? The source for to_jshtml() shows a factor of 1000 that is used if you don't pass in fps. In your example you set the interval to a number in seconds, not in milliseconds, right?

moorepants avatar Oct 08 '17 23:10 moorepants

If I remove the factor of 1000 in my example I'm still not able to get it to animate the full 5 seconds.

moorepants avatar Oct 08 '17 23:10 moorepants

Figured out the 5 second issue. If the frames arg to FuncAnimation is a generator or iterable that has no __len__ attribute, you need to give FuncAnimation a save_count, otherwise it defaults to 100.

I'll submit a PR to matplotlib noting this in the docs.

Still not sure about the interval/fps stuff. I'll investigate more later.

ixjlyons avatar Oct 09 '17 02:10 ixjlyons

Looks like interval is indeed supposed to be in milliseconds. I guess I just fixed the off-by-one(thousand) error by specifying fps correctly in the to_jshtml call :)

So it appears that generating the video will respect your interval by default, but you can override it by specifying fps.

ixjlyons avatar Oct 09 '17 03:10 ixjlyons

I just got this error when I increased save_count to 151:

IOPub data rate exceeded.
The notebook server will temporarily stop sending output
to the client in order to avoid crashing it.
To change this limit, set the config variable
`--NotebookApp.iopub_data_rate_limit`.

moorepants avatar Oct 09 '17 04:10 moorepants

Looks like the above was probably fixed in notebook > 5.1.

https://github.com/jupyter/notebook/issues/2287

moorepants avatar Oct 09 '17 04:10 moorepants

This is also cool because the animations show up in the rendered notebooks.

moorepants avatar Oct 31 '17 14:10 moorepants

Indeed. I thought I mentioned that somewhere but I can't seem to find it.

Anyway, it may be worth adding a convenience function to resonance that does the conversion and display for you, like

from resonance.functions import display_animation
ani = sys.animate_configuration()
display_animation(ani)

because I personally can never remember the imports and function calls needed. Or alternatively just add a kwarg to animate_configuration to do this. Changing the return type based on an argument might be a bad design choice though.

One thing I didn't quite get fully figured out in homework 3 (animating the bike wheel on a pivot) was getting the HTML5 video to display without also showing the animation from the sys.animate_configuration() call. I've solved this before by adding plt.ioff()/plt.ion() calls to the cell generating the Figure and then generating and displaying the video in a separate cell. There's probably a better way to do it than that though.

ixjlyons avatar Oct 31 '17 16:10 ixjlyons