Euler's identity as a walk¶
The terms of the exponential series spiral toward −1 in the complex plane. Letting the angle vary carries their sum around the unit circle.
examples/euler_identity.py
"""e^{iπ} = −1, as a walk.
The exponential is a sum: eˣ = 1 + x + x²/2! + x³/3! + ⋯. Put x = iπ and draw the terms as
arrows, tip to tail. Each term is the last one times iπ/n, and multiplying by i turns an arrow a
quarter turn, so the walk turns a quarter turn at every step; the arrows grow while π/n > 1,
then n! wins and they shrink, and the walk spirals in on a single point: −1. Put x = it instead:
as t changes the walk reshapes, and its end runs around the unit circle, at angle t from 1, so
e^{it} = cos t + i sin t. Halfway round, at t = π, it is −1: e^{iπ} + 1 = 0.
"""
import numpy as np
import manimgx as m
UNIT = 1.0 # scene units per unit of the complex plane
ZERO = np.array([-1.3, -0.45, 0.0]) # where 0 is on screen
TERMS = 20 # enough for |t| ≤ π: the next term is below 10⁻⁷
COLORS = m.color_gradient(
[m.BLUE, m.TEAL, m.GREEN, m.YELLOW, m.GOLD, m.RED, m.MAROON, m.PURPLE], 10
)
COLUMN = 4.35 # the x of the formulas' column
WALL = 11.25
def terms(x: complex, count: int = TERMS) -> np.ndarray:
"""The terms xⁿ/n! of the exponential series, n = 0 … count − 1: each the last times x/n."""
out = np.empty(count, dtype=complex)
out[0] = 1.0
for n in range(1, count):
out[n] = out[n - 1] * x / n
return out
def point(z: complex) -> np.ndarray:
"""Where the complex number z is on screen."""
return ZERO + UNIT * np.array([z.real, z.imag, 0.0])
def color(n: float) -> m.ManimColor:
"""The color of term n (between two terms, a blend of theirs)."""
k = min(int(n), len(COLORS) - 1)
following = COLORS[min(k + 1, len(COLORS) - 1)]
return m.interpolate_color(COLORS[k], following, n - int(n))
def term_arrow(start: complex, end: complex, n: float) -> m.Arrow:
return m.Arrow(
point(start),
point(end),
buff=0,
stroke_width=8,
tip_length=0.3,
color=color(n),
)
def walk(x: complex) -> m.VGroup:
"""The terms of e^x as arrows tip to tail, leaving out those too short to see."""
sums = np.concatenate([[0], np.cumsum(terms(x))])
return m.VGroup(
*(
term_arrow(sums[n], sums[n + 1], n)
for n in range(TERMS)
if UNIT * abs(sums[n + 1] - sums[n]) > 0.02
)
)
def next_term(arrow: m.Arrow, n: int, x: complex) -> m.UpdateFromAlphaFunc:
"""Turn a copy of term n − 1 into term n: its tail slides to the walk's end while it turns
and stretches by x/n, continuously (by the powers (x/n)^α, α from 0 to 1)."""
values = terms(x, n + 1)
tail_from = complex(np.sum(values[: n - 1]))
def update(mob: m.Mobject, alpha: float) -> None:
tail = tail_from + alpha * values[n - 1]
vector = values[n - 1] * (x / n) ** alpha
mob.become(term_arrow(tail, tail + vector, n - 1 + alpha))
return m.UpdateFromAlphaFunc(arrow, update)
class EulerIdentity(m.Scene):
def construct(self) -> None:
plane = m.ComplexPlane(
x_range=[-5, 2, 1],
y_range=[-3.5, 3.5, 1],
x_length=7 * UNIT,
y_length=7 * UNIT,
background_line_style={
"stroke_color": m.BLUE_D,
"stroke_width": 1.5,
"stroke_opacity": 0.5,
},
)
plane.shift(point(-1.5 + 0j) - plane.get_center())
series = m.MathTex(r"e^{x} = \sum_{n=0}^{\infty} \frac{x^n}{n!}", font_size=56)
series.move_to(np.array([COLUMN, 2.6, 0.0]))
x_is = m.MathTex(r"{{x}} = {{i \pi}}", font_size=56)
x_is.next_to(series, m.DOWN, buff=0.6)
self.play(m.Create(plane, lag_ratio=0.1), m.Write(series), run_time=1.2)
self.play(m.FadeIn(x_is), run_time=0.5)
# the walk for x = iπ, term by term: each a quarter turn from the last
x = 1j * np.pi
values = terms(x)
sums = np.concatenate([[0], np.cumsum(values)])
names = [
"1",
r"i\pi",
r"\frac{(i\pi)^2}{2!}",
r"\frac{(i\pi)^3}{3!}",
r"\frac{(i\pi)^4}{4!}",
]
labels = m.VGroup()
for n, name in enumerate(names):
direction = values[n] / abs(values[n])
outward = np.array([direction.imag, -direction.real, 0.0]) # its right side
middle = point((sums[n] + sums[n + 1]) / 2)
label = m.MathTex(name, font_size=44, color=COLORS[n])
labels.add(label.next_to(middle, outward, buff=0.2))
arrows = [term_arrow(0, 1, 0)]
self.play(m.GrowArrow(arrows[0]), m.FadeIn(labels[0]), run_time=0.8)
for n in range(1, 12):
arrow = arrows[-1].copy()
self.add(arrow)
arrows.append(arrow)
if n < len(names):
self.play(next_term(arrow, n, x), m.FadeIn(labels[n]), run_time=1.0)
else:
self.play(next_term(arrow, n, x), run_time=0.45 if n < 8 else 0.25)
rest = walk(x)[12:]
self.add(rest)
total = complex(sums[-1]) # the whole sum: −1, to the last digit drawn
end = m.Dot(point(total), radius=0.09, color=m.WHITE)
sign = "-" if total.real < 0 else "+"
end_label = m.MathTex(
"{{" + sign + "}}{{" + f"{abs(total.real):.0f}" + "}}", font_size=48
)
end_label.next_to(end, m.DL, buff=0.12)
self.play(m.FadeIn(end, scale=0.5), m.Write(end_label), run_time=0.8)
self.wait(0.6)
# let the exponent vary: the walk for x = it, as t runs once round and back
t = m.ValueTracker(np.pi)
x_it = m.MathTex(r"{{x}} = {{i t}}", font_size=56).move_to(x_is)
t_value = m.DecimalNumber(np.pi, num_decimal_places=2, font_size=56)
t_row = m.VGroup(m.MathTex("t =", font_size=56), t_value)
t_row.arrange(m.RIGHT, buff=0.2).next_to(x_it, m.DOWN, buff=0.45)
t_value.add_updater(lambda d: d.set_value(t.get_value()))
moving = m.always_redraw(lambda: walk(1j * t.get_value()))
tip = m.always_redraw(
lambda: m.Dot(point(np.exp(1j * t.get_value())), radius=0.09)
)
trace = m.TracedPath(tip.get_center, stroke_color=m.YELLOW, stroke_width=6)
self.remove(*arrows, rest, end)
self.add(trace, moving, tip)
self.play(
m.FadeOut(labels),
m.FadeOut(end_label),
m.TransformMatchingTex(x_is, x_it),
m.FadeIn(t_row),
run_time=1.0,
)
self.play(t.animate.set_value(-np.pi), run_time=5.5)
circle_law = m.MathTex(r"e^{it} = \cos t + i \sin t", font_size=48)
circle_law.next_to(t_row, m.DOWN, buff=0.75)
self.play(
t.animate(run_time=5.0).set_value(np.pi),
m.Write(circle_law, run_time=1.5),
)
# halfway round
minus_one = end_label.copy()
self.play(m.FadeIn(minus_one), run_time=0.5)
identity = m.MathTex(r"{{ e^{i\pi} }} {{=}} {{-}}{{1}}", font_size=80)
identity.move_to(np.array([COLUMN, -2.75, 0.0]))
self.play(
m.FadeIn(identity[0]),
m.FadeIn(identity[1]),
m.TransformFromCopy(minus_one[0], identity[2]),
m.TransformFromCopy(minus_one[1], identity[3]),
run_time=1.3,
)
final = m.MathTex(r"{{ e^{i\pi} }} {{+}} {{1}} {{=}} {{0}}", font_size=80)
final.move_to(identity)
self.play(m.TransformMatchingTex(identity, final), run_time=1.6)
self.wait(2.0)
if __name__ == "__main__":
EulerIdentity().render("euler_identity.mp4")