-
Notifications
You must be signed in to change notification settings - Fork 47
Description
Hello Matthias,
I read the email you sent to the sympy mailing list. I'm the person who attempted to modernize the sympy plotting module. Unfortunately, that attempt failed midway through it, leaving stuffs in undesirable conditions, thought in working condition according to the test implemented on that submodule.
I took the time to look at your notebook and what you did back then was incorrect. I guess you would like the notebook to work as before, but instead I'm going to explain what was wrong and what could be improved.
Wrong approach
p = sp.plot(0, (x, 0, 2 * sp.pi), show=False)
backend = MatplotlibBackend(p)
plt.close(backend.fig) # Avoid showing empty plot here
def func(frame):
backend.ax.clear()
p[0].expr = sp.sin(x - (frame / 5))
# If there are multiple plots, also change p[1], p[2] etc.
backend.process_series()
ani = FuncAnimation(backend.fig, func, frames=10)- You created a plot object,
p, which only serves the purpose of accessing the underlying data series withp[0]. - With
backend = MatplotlibBackend(p)you constructed another object (repetitive, because it was already available withp._backendif only it was ever documented). - Inside
func:- With
p[0].expr = sp.sin(x - (frame / 5))you assumed it is safe to change the expression of the data series. It is a very strong assumption: all other sympy objects operates on the concept of immutability. backend.process_series(): you handled the process of generating numerical data and add it to the plot to thebackendobject. The main problem is that you have to clear the axes at each time frame, thus rebuilding every matplotlib renderer at each time frame. Even for a few line expressions, this would quickly become a performance bottleneck.
- With
Back in the day, I did exactly the same mistakes. Why?
- sympy plotting module was/is criminally undocumented.
- it requires fewer lines of code with respect to the correct approach.
Correct approach
This requires more lines of code, a lot of book-keeping with matplotlib handles, drowning yourself into matplotlib's documentation in order to find the correct update method for each handle (line, scatter, surface, contour, ...). But, it is the safest choice, and the one that always work.
import sympy as sp
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
plt.rcParams['animation.html'] = 'jshtml'
# create a symbolic expression
sp.var("x, frame")
expr = sp.sin(x - (frame / 5))
# convert it to a numerical function
f = sp.lambdify([x, frame], expr)
# evaluate the function to get numerical data
xn = np.linspace(0, 2*np.pi)
yn = f(xn, 0)
# create the figure, axes, handles
fig, ax = plt.subplots()
# without this, two pictures would be shown on the notebook
plt.close(fig)
line, = ax.plot(xn, yn)
ax.set_xlabel("x")
ax.set_ylabel("y")
def func(frame):
# update the handles without deleting whatever was plotted previously.
yn = f(xn, frame)
line.set_data(xn, yn)
ani = FuncAnimation(fig, func, frames=10)
aniEasiest way to animate sympy expressions
If you, like me, hate the repetitive process of book-keeping matplotlib's handles in order to create animations, maybe you would enjoy a simpler api. I created an improved plotting module for symbolic expression, which supports animations. This module follows the procedure illustrated in the above section Correct approach, so performance should not be an issue.
I'm going to show a few examples taken from your notebook. The above example becomes:
%matplotlib widget
from sympy import *
from spb import *
var("x, t")
p = plot(
sin(x - t/5), (x, 0, 2 * pi),
params={t: (0, 10)}, # t is the time parameter (frame in your code example)
animation=True
)Note the %matplotlib widget command. In order to support multiple plotting libraries, spb animations are based on ipywidgets. Without this command, the animation won't show on the screen (more likely, you will get some error). Instead, if you want to show the FuncAnimation object:
from sympy import *
from spb import *
import matplotlib.pyplot as plt
plt.rcParams['animation.html'] = 'jshtml'
var("x, t")
p = plot(
sin(x - t/5), (x, 0, 2 * pi),
params={t: (0, 10)}, # t is the time parameter (frame in your code example)
animation=True,
show=False
)
plt.close(p.fig)
p.get_FuncAnimation()Note that I had to create a hidden animation with show=False. Then, I had to close the figure with plt.close(p.fig), otherwise there would be two figures shown on the notebook cell. Unfortunately, this is standard Jupyter Notebook behavior: it listen to a particular attribute of Matplotlib, and when a new figure is created, it immediately shows it on the screen.
Another example:
p = plot(
sin(x), prange(x, 0, 2 * pi * (1 + t/5)), # prange stands for parametric range
params={t: (0, 10)}, # t is the time parameter (frame in your code example)
animation={"fps": 10},
show=False
)
plt.close(p.fig)
p.get_FuncAnimation()Another one, reusing an existing figure/axes:
fig, ax = plt.subplots(2, 1)
ax[0].plot([4, 9, 7, 20, 6, 33, 13, 23, 16, 62, 8])
p = plot(
sin(x), prange(x, 0, 2 * pi * (1 + t/5)), # prange stands for parametric range
params={t: (0, 10)},
animation={"fps": 10},
show=False,
ax=ax[1]
)
p.get_FuncAnimation()