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:
teahubxyzandClaude Fable 5.1 committed 2026-10-27 20:46:27 +00:00
1 parent a7601b040a
commit a6cc2bba4a
1 file changed
+281
+281
View File
@@ -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()