zeta-ball: Manim scenes for "from Riemann to zeta" (ELI5, nine timed scenes)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv3n4yABxvJPJHFRhVA
This commit is contained in:
1 parent
a7601b040a
commit
a6cc2bba4a
1 file changed
+281
@@ -0,0 +1,281 @@
|
||||
from manim import *
|
||||
import json, numpy as np
|
||||
|
||||
D = json.load(open('durations.json'))
|
||||
BG = "#0b0a12"; GOLD = "#f0b44c"; PINK = "#e2559a"; TEAL = "#4fd1c5"; FG = "#e9e4f2"; MUTED = "#9a93ad"
|
||||
COOL = "#6f7ad8"; WARM = "#e0a050"; GREY = "#8a8a9a"
|
||||
config.background_color = BG
|
||||
|
||||
ZEROS = [14.134725, 21.022040, 25.010858, 30.424876, 32.935062, 37.586178, 40.918719, 43.327073, 48.005151, 49.773832]
|
||||
|
||||
class Timed:
|
||||
def setup_time(self, i):
|
||||
self.budget = D[i] + 0.6; self.used = 0.0
|
||||
def go(self, *anims, rt=1.0):
|
||||
self.play(*anims, run_time=rt); self.used += rt
|
||||
def rest(self, extra=0.0):
|
||||
self.wait(max(0.1, self.budget - self.used + extra))
|
||||
|
||||
def cap(text, where=DOWN, size=0.5):
|
||||
return Text(text, font="Georgia", color=MUTED).scale(size).to_edge(where, buff=0.45)
|
||||
|
||||
# ---------- the ball: radius r, resting on the plane z=0 at the origin (s = 1/2); 1 unit of s = SC manim units
|
||||
r = 1.25; SC = 4*r # s=0 at x=-2r, s=1 at x=+2r
|
||||
C = np.array([0, 0, r]); LAMP = np.array([0, 0, 2*r])
|
||||
def s_to_plane(x, y): # s = 1/2 + x + iy -> plane point
|
||||
return np.array([SC*x, SC*y, 0.0])
|
||||
def plane_to_ball(p): # inverse stereographic from the lamp
|
||||
u = p[:2] / (2*r); q = u @ u
|
||||
return C + r*np.array([2*u[0], 2*u[1], q - 1]) / (q + 1)
|
||||
def ball_to_plane(b):
|
||||
d = b - LAMP; t = -LAMP[2] / min(d[2], -1e-3)
|
||||
return LAMP + t*d
|
||||
def ball_pt(phi, th): # axis along x: poles at phi=0, pi
|
||||
return C + r*np.array([np.cos(phi), np.sin(phi)*np.cos(th), np.sin(phi)*np.sin(th)])
|
||||
def ball_curve(f, color, width=1.6, op=1.0, n=120):
|
||||
return ParametricFunction(f, t_range=[0, TAU, TAU/n], color=color, stroke_width=width, stroke_opacity=op)
|
||||
def latlong_on_ball(colfun=None):
|
||||
g = VGroup()
|
||||
for phi in np.linspace(0.25, PI-0.25, 9):
|
||||
col = COOL if phi < PI/2-1e-6 else (WARM if phi > PI/2+1e-6 else GOLD)
|
||||
g.add(ball_curve(lambda t, phi=phi: ball_pt(phi, t), col, 2.6 if abs(phi-PI/2) < 1e-6 else 1.4))
|
||||
for th in np.linspace(0, PI, 8, endpoint=False):
|
||||
g.add(ParametricFunction(lambda p, th=th: ball_pt(p, th), t_range=[0, PI, PI/60], color=GREY, stroke_width=1.1))
|
||||
return g
|
||||
def latlong_shadow():
|
||||
g = VGroup()
|
||||
for phi in np.linspace(0.25, PI-0.25, 9):
|
||||
col = COOL if phi < PI/2-1e-6 else (WARM if phi > PI/2+1e-6 else GOLD)
|
||||
def f(t, phi=phi):
|
||||
b = ball_pt(phi, t); p = ball_to_plane(b)
|
||||
return np.clip(p, -9, 9)
|
||||
g.add(ball_curve(f, col, 2.6 if abs(phi-PI/2) < 1e-6 else 1.4, n=160))
|
||||
for th in np.linspace(0, PI, 8, endpoint=False):
|
||||
def f(p, th=th):
|
||||
b = ball_pt(p, th); q = ball_to_plane(b)
|
||||
return np.clip(q, -9, 9)
|
||||
g.add(ParametricFunction(f, t_range=[0.02, PI-0.02, PI/90], color=GREY, stroke_width=1.1))
|
||||
return g
|
||||
def ball_mesh():
|
||||
s = Sphere(center=C, radius=r*0.992, resolution=(24, 48))
|
||||
s.set_fill("#1d1a2b", opacity=0.45); s.set_stroke("#3a3550", width=0.3, opacity=0.4)
|
||||
return s
|
||||
def plane_floor():
|
||||
return NumberPlane(x_range=[-7, 7, 1], y_range=[-4, 4, 1], background_line_style={"stroke_color": "#2a2638", "stroke_width": 1, "stroke_opacity": 0.6}, axis_config={"stroke_color": "#3a3550"})
|
||||
def poles_and_origins():
|
||||
p0 = Dot3D(s_to_plane(-0.5, 0), radius=0.07, color=TEAL); p1 = Dot3D(s_to_plane(0.5, 0), radius=0.07, color=TEAL)
|
||||
return p0, p1
|
||||
|
||||
class T3(Timed, ThreeDScene):
|
||||
def cam(self, phi=65, theta=-40, zoom=1.0):
|
||||
self.set_camera_orientation(phi=phi*DEGREES, theta=theta*DEGREES, zoom=zoom)
|
||||
def label(self, text, size=0.5, where=DOWN):
|
||||
t = cap(text, where, size); self.add_fixed_in_frame_mobjects(t); return t
|
||||
|
||||
# ---------- S0: the plane
|
||||
class S0(Timed, Scene):
|
||||
def construct(self):
|
||||
self.setup_time(0)
|
||||
line = NumberLine(x_range=[-6, 6, 1], length=12, color=GREY, include_numbers=True, font_size=24)
|
||||
self.go(Create(line), rt=1.6)
|
||||
plane = NumberPlane(x_range=[-7, 7, 1], y_range=[-4, 4, 1], background_line_style={"stroke_color": "#2a2638", "stroke_width": 1}, axis_config={"stroke_color": GREY})
|
||||
self.go(FadeIn(plane), line.animate.set_opacity(0.3), rt=1.8)
|
||||
d = Dot(np.array([1, 1, 0]), color=GOLD, radius=0.08)
|
||||
a1 = Arrow(ORIGIN, RIGHT, buff=0, color=GOLD, stroke_width=3); a2 = Arrow(RIGHT, RIGHT+UP, buff=0, color=GOLD, stroke_width=3)
|
||||
lab = MathTex("1 + i", color=GOLD).scale(0.8).next_to(d, UR, buff=0.1)
|
||||
self.go(GrowArrow(a1), rt=0.7); self.go(GrowArrow(a2), rt=0.7); self.go(FadeIn(d), Write(lab), rt=0.9)
|
||||
self.add(cap("the complex plane"))
|
||||
self.rest()
|
||||
|
||||
# ---------- S1: the Riemann sphere
|
||||
class S1(T3):
|
||||
def construct(self):
|
||||
self.setup_time(1)
|
||||
self.cam(62, -45, 0.95)
|
||||
floor = plane_floor(); self.add(floor)
|
||||
self.go(Wait(), rt=1.2)
|
||||
ball = ball_mesh(); self.go(FadeIn(ball), rt=2.0)
|
||||
lamp = Dot3D(LAMP, radius=0.07, color=FG); glow = Dot3D(LAMP, radius=0.16, color=FG, fill_opacity=0.25)
|
||||
self.go(FadeIn(lamp), FadeIn(glow), rt=0.8)
|
||||
tr = ValueTracker(0.0)
|
||||
def bpt():
|
||||
a = tr.get_value(); phi = 0.75 + 0.5*np.sin(a*0.9)
|
||||
return C + r*np.array([np.sin(phi)*np.cos(a), np.sin(phi)*np.sin(a), -np.cos(phi)])
|
||||
ray = always_redraw(lambda: Line(LAMP, ball_to_plane(bpt()), color=GOLD, stroke_width=1.6, stroke_opacity=0.8))
|
||||
onb = always_redraw(lambda: Dot3D(bpt(), radius=0.05, color=GOLD)); onp = always_redraw(lambda: Dot3D(ball_to_plane(bpt()), radius=0.05, color=GOLD))
|
||||
self.go(Create(ray), FadeIn(onb), FadeIn(onp), rt=0.8)
|
||||
self.go(tr.animate.set_value(2*PI), rt=5.0)
|
||||
self.go(FadeOut(ray), FadeOut(onb), FadeOut(onp), rt=0.5)
|
||||
inf = Text("∞", font="Georgia", color=FG).scale(0.7); self.add_fixed_orientation_mobjects(inf); inf.move_to(LAMP + 0.45*OUT)
|
||||
self.go(FadeIn(inf), rt=0.8)
|
||||
self.label("the Riemann sphere")
|
||||
self.begin_ambient_camera_rotation(rate=0.06)
|
||||
self.rest()
|
||||
|
||||
# ---------- S2: the zeta grid
|
||||
import mpmath
|
||||
mpmath.mp.dps = 15
|
||||
def euler(s): # the real zeta; its carrier is the 2-factor 1/(1 - 2^-s)
|
||||
w = mpmath.zeta(mpmath.mpc(s.real, s.imag)); return complex(w)
|
||||
class S2(Timed, Scene):
|
||||
def construct(self):
|
||||
self.setup_time(2)
|
||||
plane = NumberPlane(x_range=[-7, 7, 1], y_range=[-4, 4, 1], background_line_style={"stroke_color": "#2a2638", "stroke_width": 1, "stroke_opacity": 0.5}, axis_config={"stroke_color": "#3a3550"})
|
||||
self.add(plane)
|
||||
title = Text("ζ", font="Georgia", color=GOLD).scale(1.2).to_corner(UL, buff=0.5); self.add(title)
|
||||
lines = VGroup()
|
||||
for sig in np.linspace(1.1, 4.0, 9):
|
||||
col = interpolate_color(ManimColor(COOL), ManimColor(WARM), (sig-1.1)/2.9)
|
||||
lines.add(ParametricFunction(lambda t, sig=sig: np.array([sig-2.5, t, 0]), t_range=[-4.5, 4.5, 0.05], color=col, stroke_width=1.5))
|
||||
for t0 in np.linspace(-4.5, 4.5, 13):
|
||||
lines.add(ParametricFunction(lambda x, t0=t0: np.array([x-2.5, t0, 0]), t_range=[1.1, 4.0, 0.05], color=GREY, stroke_width=1.0))
|
||||
self.go(Create(lines), rt=2.0)
|
||||
self.go(Wait(), rt=1.5)
|
||||
out = VGroup()
|
||||
SC2 = 2.2
|
||||
for sig in np.linspace(1.1, 4.0, 9):
|
||||
col = interpolate_color(ManimColor(COOL), ManimColor(WARM), (sig-1.1)/2.9)
|
||||
def f(t, sig=sig):
|
||||
w = euler(complex(sig, t)); return np.array([SC2*(w.real-0.5), SC2*w.imag, 0])
|
||||
out.add(ParametricFunction(f, t_range=[-4.5, 4.5, 0.02], color=col, stroke_width=1.5))
|
||||
for t0 in np.linspace(-4.5, 4.5, 13):
|
||||
def f(x, t0=t0):
|
||||
w = euler(complex(x, t0)); return np.array([SC2*(w.real-0.5), SC2*w.imag, 0])
|
||||
out.add(ParametricFunction(f, t_range=[1.1, 4.0, 0.02], color=GREY, stroke_width=1.0))
|
||||
self.go(Transform(lines, out), rt=3.2)
|
||||
d0 = Dot(np.array([-SC2*0.5, 0, 0]), color=TEAL, radius=0.07); d1 = Dot(np.array([SC2*0.5, 0, 0]), color=TEAL, radius=0.07)
|
||||
l0 = Text("0", font="Georgia", color=TEAL).scale(0.5).next_to(d0, DOWN, buff=0.12); l1 = Text("1", font="Georgia", color=TEAL).scale(0.5).next_to(d1, DOWN, buff=0.12)
|
||||
self.go(FadeIn(d0), FadeIn(d1), FadeIn(l0), FadeIn(l1), rt=0.8)
|
||||
self.add(cap("what zeta does to a grid"))
|
||||
self.rest()
|
||||
|
||||
# ---------- S3: it is a globe on its side
|
||||
class S3(T3):
|
||||
def construct(self):
|
||||
self.setup_time(3)
|
||||
self.cam(0.001, -90, 1.0) # straight down first
|
||||
floor = plane_floor(); self.add(floor)
|
||||
sh = latlong_shadow(); self.add(sh)
|
||||
p0, p1 = poles_and_origins(); self.add(p0, p1)
|
||||
self.go(Wait(), rt=2.0)
|
||||
self.move_camera(phi=62*DEGREES, theta=-45*DEGREES, zoom=0.95, run_time=2.4); self.used += 2.4
|
||||
ball = ball_mesh(); ll = latlong_on_ball()
|
||||
self.go(FadeIn(ball), rt=1.2)
|
||||
self.go(ReplacementTransform(sh.copy(), ll), sh.animate.set_stroke(opacity=0.35), rt=2.6)
|
||||
bp0 = Dot3D(ball_pt(0, 0), radius=0.07, color=TEAL); bp1 = Dot3D(ball_pt(PI, 0), radius=0.07, color=TEAL)
|
||||
ax = Line3D(ball_pt(PI, 0), ball_pt(0, 0), color=TEAL, thickness=0.012)
|
||||
self.go(FadeIn(bp0), FadeIn(bp1), Create(ax), rt=1.0)
|
||||
self.label("a globe on its side: poles over 0 and 1")
|
||||
self.begin_ambient_camera_rotation(rate=0.05)
|
||||
self.rest()
|
||||
|
||||
# ---------- S4: the equator casts the critical line
|
||||
class S4(T3):
|
||||
def construct(self):
|
||||
self.setup_time(4)
|
||||
self.cam(60, -40, 0.95)
|
||||
floor = plane_floor(); sh = latlong_shadow().set_stroke(opacity=0.3); ball = ball_mesh(); ll = latlong_on_ball().set_stroke(opacity=0.45)
|
||||
p0, p1 = poles_and_origins(); ax = Line3D(ball_pt(PI, 0), ball_pt(0, 0), color=TEAL, thickness=0.012)
|
||||
self.add(floor, sh, ball, ll, p0, p1, ax)
|
||||
self.go(Wait(), rt=3.0)
|
||||
eq = ball_curve(lambda t: ball_pt(PI/2, t), GOLD, 4.0)
|
||||
self.go(Create(eq), rt=1.6)
|
||||
crit = Line(np.array([0, -4.2, 0]), np.array([0, 4.2, 0]), color=GOLD, stroke_width=4)
|
||||
rays = VGroup(*[Line(LAMP, ball_to_plane(ball_pt(PI/2, t)), color=GOLD, stroke_width=1, stroke_opacity=0.35) for t in np.linspace(0.3, PI-0.3, 7)])
|
||||
self.go(Create(rays), rt=1.0); self.go(Create(crit), rt=1.4); self.go(FadeOut(rays), rt=0.5)
|
||||
half = Dot3D(ORIGIN, radius=0.07, color=FG)
|
||||
self.go(FadeIn(half), rt=0.5)
|
||||
self.label("the equator casts the critical line, Re s = ½")
|
||||
self.begin_ambient_camera_rotation(rate=0.04)
|
||||
self.rest()
|
||||
|
||||
# ---------- S5: two origins
|
||||
class S5(T3):
|
||||
def construct(self):
|
||||
self.setup_time(5)
|
||||
self.cam(72, -30, 1.05)
|
||||
floor = plane_floor(); ball = ball_mesh(); ll = latlong_on_ball().set_stroke(opacity=0.35)
|
||||
eq = ball_curve(lambda t: ball_pt(PI/2, t), GOLD, 4.0); crit = Line(np.array([0, -4.2, 0]), np.array([0, 4.2, 0]), color=GOLD, stroke_width=3)
|
||||
p0, p1 = poles_and_origins()
|
||||
self.add(floor, ball, ll, eq, crit, p0, p1)
|
||||
self.go(Wait(), rt=1.5)
|
||||
bot = Dot3D(ORIGIN, radius=0.11, color="#ffffff"); top = Dot3D(LAMP, radius=0.11, color="#000000"); topring = Dot3D(LAMP, radius=0.14, color=GOLD, fill_opacity=0.0)
|
||||
topring.set_stroke(GOLD, width=2)
|
||||
self.go(FadeIn(bot), Flash(ORIGIN, color=FG, line_length=0.3), rt=1.4)
|
||||
self.go(Wait(), rt=1.2)
|
||||
self.go(FadeIn(top), FadeIn(topring), Flash(LAMP, color=GOLD, line_length=0.3), rt=1.4)
|
||||
lh = Text("½", font="Georgia", color=FG).scale(0.6); li = Text("∞", font="Georgia", color=GOLD).scale(0.7)
|
||||
self.add_fixed_orientation_mobjects(lh, li); lh.move_to(ORIGIN + 0.5*DOWN*1 + 0.1*OUT); li.move_to(LAMP + 0.45*OUT)
|
||||
self.go(FadeIn(lh), FadeIn(li), rt=0.8)
|
||||
self.label("two special points on the critical line: ½ and ∞")
|
||||
self.begin_ambient_camera_rotation(rate=0.04)
|
||||
self.rest()
|
||||
|
||||
# ---------- S6: the symmetry is a half-turn
|
||||
class S6(T3):
|
||||
def construct(self):
|
||||
self.setup_time(6)
|
||||
self.cam(60, -40, 0.95)
|
||||
floor = plane_floor(); ball = ball_mesh(); ll = latlong_on_ball().set_stroke(opacity=0.5)
|
||||
eq = ball_curve(lambda t: ball_pt(PI/2, t), GOLD, 4.0)
|
||||
bp0 = Dot3D(ball_pt(0, 0), radius=0.08, color=TEAL); bp1 = Dot3D(ball_pt(PI, 0), radius=0.08, color=PINK)
|
||||
bot = Dot3D(ORIGIN, radius=0.09, color="#ffffff"); top = Dot3D(LAMP, radius=0.09, color="#000000"); top.set_stroke(GOLD, width=2)
|
||||
axis = DashedLine(ORIGIN, LAMP + 0.6*OUT, color=GOLD, stroke_width=2)
|
||||
self.add(floor, ball, ll, eq, bp0, bp1, bot, top)
|
||||
f = MathTex(r"s \;\longleftrightarrow\; 1 - s", color=FG).scale(0.9).to_corner(UR, buff=0.5); self.add_fixed_in_frame_mobjects(f)
|
||||
self.go(FadeIn(f), rt=1.0)
|
||||
self.go(Wait(), rt=2.2)
|
||||
self.go(Create(axis), rt=0.8)
|
||||
grp = VGroup(ll, bp0, bp1)
|
||||
self.go(Rotate(grp, PI, axis=OUT, about_point=C), rt=3.0)
|
||||
self.go(Wait(), rt=0.8)
|
||||
self.go(Indicate(eq, color=GOLD, scale_factor=1.0), rt=1.2)
|
||||
self.label("the symmetry is a half-turn: poles swap, the equator stays")
|
||||
self.rest()
|
||||
|
||||
# ---------- S7: the zeros, in the plane
|
||||
class S7(Timed, Scene):
|
||||
def construct(self):
|
||||
self.setup_time(7)
|
||||
plane = NumberPlane(x_range=[-7, 7, 1], y_range=[-4, 4, 1], background_line_style={"stroke_color": "#2a2638", "stroke_width": 1, "stroke_opacity": 0.5}, axis_config={"stroke_color": "#3a3550"})
|
||||
self.add(plane)
|
||||
SCL = 0.14 # 1 unit of Im s
|
||||
crit = Line(np.array([0, -4, 0]), np.array([0, 4, 0]), color=GOLD, stroke_width=3)
|
||||
d0 = Dot(np.array([-SC*0.5, 0, 0]), color=TEAL, radius=0.06); d1 = Dot(np.array([SC*0.5, 0, 0]), color=TEAL, radius=0.06)
|
||||
half = Dot(ORIGIN, color=FG, radius=0.07)
|
||||
lab = Text("Re s = ½", font="Georgia", color=GOLD).scale(0.5).next_to(crit, RIGHT, buff=0.2).shift(2.6*DOWN)
|
||||
self.add(crit, d0, d1, half, lab)
|
||||
self.go(Wait(), rt=2.5)
|
||||
zs = VGroup()
|
||||
for g in ZEROS:
|
||||
if g*SCL > 3.9: break
|
||||
zs.add(Dot(np.array([0, g*SCL, 0]), color=FG, radius=0.06)); zs.add(Dot(np.array([0, -g*SCL, 0]), color=FG, radius=0.06))
|
||||
self.go(LaggedStart(*[GrowFromCenter(z) for z in zs], lag_ratio=0.25), rt=4.0)
|
||||
q = Text("all of them?", font="Georgia", slant=ITALIC, color=PINK).scale(0.7).to_corner(UR, buff=0.5)
|
||||
self.go(Write(q), rt=1.2)
|
||||
self.add(cap("the Riemann hypothesis: every zero is on this line"))
|
||||
self.rest()
|
||||
|
||||
# ---------- S8: the zeros on the ball, climbing to the lamp
|
||||
class S8(T3):
|
||||
def construct(self):
|
||||
self.setup_time(8)
|
||||
self.cam(68, -35, 1.0)
|
||||
floor = plane_floor(); ball = ball_mesh(); ll = latlong_on_ball().set_stroke(opacity=0.3)
|
||||
eq = ball_curve(lambda t: ball_pt(PI/2, t), GOLD, 4.0); crit = Line(np.array([0, -4.2, 0]), np.array([0, 4.2, 0]), color=GOLD, stroke_width=2, stroke_opacity=0.6)
|
||||
bot = Dot3D(ORIGIN, radius=0.09, color="#ffffff"); top = Dot3D(LAMP, radius=0.09, color="#000000"); top.set_stroke(GOLD, width=2)
|
||||
self.add(floor, ball, ll, eq, crit, bot, top)
|
||||
self.go(Wait(), rt=1.0)
|
||||
zs = VGroup()
|
||||
for g in ZEROS:
|
||||
for sg in (1, -1):
|
||||
zs.add(Dot3D(plane_to_ball(s_to_plane(0, sg*g/30.0)), radius=0.055, color=FG))
|
||||
self.go(LaggedStart(*[FadeIn(z) for z in zs], lag_ratio=0.12), rt=2.6)
|
||||
self.go(Wait(), rt=1.0)
|
||||
self.move_camera(phi=30*DEGREES, theta=-35*DEGREES, zoom=1.0, run_time=3.0); self.used += 3.0
|
||||
self.go(Flash(LAMP, color=GOLD, line_length=0.4, num_lines=16), rt=1.0)
|
||||
t1 = Text("Riemann's sphere, Riemann's hypothesis", font="Georgia", slant=ITALIC, color=FG).scale(0.6)
|
||||
self.add_fixed_in_frame_mobjects(t1); t1.to_edge(DOWN, buff=0.45)
|
||||
self.go(FadeIn(t1), rt=1.2)
|
||||
self.begin_ambient_camera_rotation(rate=0.05)
|
||||
self.rest()
|
||||
Reference in new issue
Block a user