Polynomials become the sine¶
Each new Taylor term makes a polynomial follow more of the sine wave. The growing formula and its graph change together.
examples/taylor_series.py
"""Polynomials that turn into the sine.
Near 0, sin x looks like x. Subtract x³/3! and the cubic bends back down with the sine; add
x⁵/5! and it follows the next turn; every term fixes the error the one before it left. The
Taylor polynomial of degree 2n + 1 matches sin x and its first 2n + 1 derivatives at 0, and its
error is at most |x|^(2n+3)/(2n + 3)!: a factorial outruns any power, so each new term hugs the
wave over a wider stretch, and the whole infinite series is the sine everywhere.
"""
from math import factorial
import numpy as np
import manimgx as m
WALL = 9.4 # the polynomial chases the sine out to the edges
DEGREES = list(range(1, 41, 2)) # 1, 3, …, 39
# the terms written out one by one, to x¹⁹/19!; the rest rush in at the end
WRITTEN = 10
X_MAX = 11.0 # the axes run over |x| ≤ X_MAX
Y_MAX = 3.0 # and |y| ≤ Y_MAX; a polynomial is cut where it leaves them
COLORS = m.color_gradient([m.YELLOW, m.RED, m.PURPLE_A], WRITTEN)
XS = np.linspace(-X_MAX, X_MAX, 2201)
def term(degree: int, x: np.ndarray) -> np.ndarray:
"""The series' term of this (odd) degree: ±x^degree / degree!."""
sign = -1 if degree % 4 == 3 else 1
return sign * x**degree / factorial(degree)
def term_tex(degree: int) -> str:
if degree == 1:
return "x"
sign = "-" if degree % 4 == 3 else "+"
return sign + rf" \frac{{x^{{{degree}}}}}{{{degree}!}}"
def color(k: int) -> m.ManimColor:
"""The color of the polynomial with k + 1 terms, and of its last term."""
return COLORS[min(k, WRITTEN - 1)]
def series_tex(count: int) -> m.MathTex:
"""sin x ≈ the first `count` terms; past seven, the middle ones go into ⋯."""
shown = list(range(count))
if count > 7:
shown = [0, 1, 2, 3, -1, count - 1]
parts = [r"\sin x", r"\approx"]
parts += [r"+ \cdots" if k < 0 else term_tex(DEGREES[k]) for k in shown]
tex = m.MathTex(*parts, font_size=50)
tex[0].set_color(m.BLUE)
for part, k in zip(tex[2:], shown, strict=True):
if k >= 0:
part.set_color(color(k))
return tex.move_to(3.25 * m.UP)
class TaylorSeries(m.Scene):
def construct(self) -> None:
axes = m.Axes(
x_range=[-X_MAX, X_MAX, np.pi],
y_range=[-Y_MAX, Y_MAX, 1],
x_length=13.4,
y_length=5.4,
tips=False,
axis_config={"stroke_width": 3, "color": m.GREY_B},
).move_to(0.7 * m.DOWN)
sine = axes.plot(
np.sin, x_range=[-X_MAX, X_MAX, 0.02], color=m.BLUE, stroke_width=6
)
# the polynomial: all the terms below `reach`, and the next one scaled by its fraction
reach = m.ValueTracker(1.0)
def polynomial() -> m.VMobject:
done = int(reach.get_value())
s = reach.get_value() - done
ys = np.sum([term(d, XS) for d in DEGREES[:done]], axis=0)
if s > 0 and done < len(DEGREES):
ys = ys + s * term(DEGREES[done], XS)
# keep the stretch around 0 inside the axes, cut where it leaves them
inside = np.abs(ys) <= Y_MAX
middle = len(XS) // 2
left_out = np.nonzero(~inside[:middle])[0]
right_out = np.nonzero(~inside[middle:])[0]
lo = left_out[-1] + 1 if len(left_out) else 0
hi = middle + right_out[0] if len(right_out) else len(XS)
xs, ys_in = list(XS[lo:hi]), list(ys[lo:hi])
if lo > 0: # the exact point where it leaves, on each side
a, b = lo, lo - 1
bound = np.sign(ys[b]) * Y_MAX
t = (bound - ys[a]) / (ys[b] - ys[a])
xs.insert(0, XS[a] + t * (XS[b] - XS[a]))
ys_in.insert(0, bound)
if hi < len(XS):
a, b = hi - 1, hi
bound = np.sign(ys[b]) * Y_MAX
t = (bound - ys[a]) / (ys[b] - ys[a])
xs.append(XS[a] + t * (XS[b] - XS[a]))
ys_in.append(bound)
shade = color(done - 1)
if s > 0 and done < len(DEGREES):
shade = m.interpolate_color(color(done - 1), color(done), s)
# thinner once past the written terms, so the sine shows around it
width = 7 - 3 * min(1.0, max(0.0, (reach.get_value() - WRITTEN) / 4))
curve = m.VMobject(stroke_color=shade, stroke_width=width)
curve.set_points_as_corners(axes.c2p(np.array(xs), np.array(ys_in)).T)
return curve
poly = m.always_redraw(polynomial)
series = series_tex(1)
self.add(axes)
self.play(m.Create(sine), run_time=1.6)
self.play(m.Create(poly), m.Write(series), run_time=1.2)
self.remove(poly)
poly = m.always_redraw(polynomial)
self.add(poly)
# each new term bends the polynomial along one more turn of the wave
for count in range(2, WRITTEN + 1):
new_series = series_tex(count)
self.play(
reach.animate.set_value(float(count)),
m.TransformMatchingTex(series, new_series),
run_time=1.6,
)
series = new_series
self.wait(0.8)
# all the terms: the series is the sine
whole = m.MathTex(
r"\sin x",
"=",
r"\sum_{n=0}^{\infty} \frac{(-1)^n\, x^{2n+1}}{(2n+1)!}",
font_size=56,
)
whole[0].set_color(m.BLUE)
whole.move_to(3.2 * m.UP)
# every term at once: the polynomial lies on the sine all the way across
self.play(reach.animate.set_value(float(len(DEGREES))), run_time=2.5)
name, approx, terms = series[0], series[1], m.VGroup(*series[2:])
self.remove(series)
self.add(name, approx, terms)
self.play(m.FadeOut(terms, shift=0.4 * m.UP), run_time=0.8)
self.play(
m.ReplacementTransform(approx, whole[1]),
name.animate.move_to(whole[0]),
m.FadeIn(whole[2], shift=0.4 * m.UP),
run_time=1.4,
)
self.wait(2)
if __name__ == "__main__":
TaylorSeries().render("taylor_series.mp4")