两木块数出 π¶
两个木块与一面墙之间的弹性碰撞 数出 π 的数位。每次碰撞都被模拟,质量比决定 显示多少位数。
examples/colliding_blocks.py
"""两个木块数出 π 的数字。
一个小木块静止在墙和朝它滑来的大木块之间。每次碰撞都是完全弹性的,且没有摩擦:小木块在墙和
大木块之间反弹,直到大木块回头且再没有什么能追上它。数一数碰撞声。如果大木块比小木块重 100ᵈ 倍,
则计数结果为 π 的前 d + 1 位数字:3, 31, 314, 3141, …(Galperin,2003)。原因是一个圆:在
坐标 √m₁v₁ 和 √m₂v₂ 下,能量使状态保持在一个圆上,每次碰撞使它沿圆前进相同的角度 arctan √(m₁/m₂)。
此处的每一次碰撞都是模拟的,计数器对它们进行计数。
"""
import numpy as np
import manimgx as m
WALL = 11.8 # README 墙的 5 秒从此开始
FLOOR = -1.6
WALL_X = -5.6
SPEED = 2.2 # 大木块的起始速度,每秒屏幕单位数
RUNS = [
(1, 1.0, 1.4),
(100, 1.4, 1.6),
(10_000, 1.9, 1.6),
(1_000_000, 2.3, 1.6),
] # (质量比,其边长,间距)
def simulate(
ratio: float, small: float, big: float, gap: float
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""碰撞时刻,以及每次碰撞后各木块左边缘和速度(起始也算一次),小木块在前。"""
m1, m2 = 1.0, ratio
x1, x2 = WALL_X + 1.2, WALL_X + 1.2 + small + gap
v1, v2 = 0.0, -SPEED
t = 0.0
times, positions, velocities = [0.0], [(x1, x2)], [(v1, v2)]
while True:
# 下一次碰撞:两木块相遇,或小木块到达墙
meet = (x2 - x1 - small) / (v1 - v2) if v1 > v2 else np.inf
wall = (x1 - WALL_X) / -v1 if v1 < 0 else np.inf
dt = min(meet, wall)
if not np.isfinite(dt):
break
t += dt
x1, x2 = x1 + v1 * dt, x2 + v2 * dt
if meet <= wall:
v1, v2 = (
((m1 - m2) * v1 + 2 * m2 * v2) / (m1 + m2),
((m2 - m1) * v2 + 2 * m1 * v1) / (m1 + m2),
)
else:
v1 = -v1
times.append(t)
positions.append((x1, x2))
velocities.append((v1, v2))
return np.array(times), np.array(positions), np.array(velocities), np.array([0])
class CollidingBlocks(m.Scene):
def construct(self) -> None:
floor = m.Line([WALL_X, FLOOR, 0], [7.2, FLOOR, 0], stroke_width=3)
wall = m.Line([WALL_X, FLOOR, 0], [WALL_X, 3.0, 0], stroke_width=3)
hatches = m.VGroup(
*(
m.Line([WALL_X, y, 0], [WALL_X - 0.3, y - 0.3, 0], stroke_width=2)
for y in np.arange(FLOOR + 0.3, 3.0, 0.3)
)
)
counter_label = m.Tex("collisions:", font_size=60)
counter = m.Integer(0, font_size=72, color=m.YELLOW)
readout = m.VGroup(counter_label, counter).arrange(m.RIGHT, buff=0.3)
readout.move_to([0.8, 3.0, 0])
counter_place = counter.get_left()
self.play(
m.Create(floor),
m.Create(wall),
m.Create(hatches),
m.FadeIn(readout),
run_time=1,
)
record = m.VGroup() # 迄今为止的计数,在计数器下方
for ratio, side, gap in RUNS:
small_side = 0.7
times, positions, velocities, _ = simulate(ratio, small_side, side, gap)
clock = m.ValueTracker(0.0)
small = m.Square(
small_side,
fill_color=m.BLUE_E,
fill_opacity=1,
stroke_color=m.BLUE_B,
stroke_width=3,
)
big = m.Square(
side,
fill_color=m.BLUE_D,
fill_opacity=1,
stroke_color=m.BLUE_B,
stroke_width=3,
)
small.add(m.MathTex("1", font_size=36).move_to(small))
mass = f"{ratio:,}".replace(",", r"{,}")
big.add(m.MathTex(mass, font_size=40 if ratio < 10**6 else 32).move_to(big))
def place(
_: m.Mobject,
small: m.Mobject = small,
big: m.Mobject = big,
times: np.ndarray = times,
positions: np.ndarray = positions,
velocities: np.ndarray = velocities,
clock: m.ValueTracker = clock,
) -> None:
t = clock.get_value()
k = int(np.searchsorted(times, t, side="right")) - 1
x1, x2 = positions[k] + velocities[k] * (t - times[k])
small.move_to([x1 + small.width / 2, FLOOR + small.height / 2, 0])
big.move_to([x2 + big.width / 2, FLOOR + big.height / 2, 0])
counter.set_value(k)
counter.move_to(counter_place, aligned_edge=m.LEFT)
blocks = m.VGroup(small, big)
blocks.add_updater(place)
place(blocks)
self.play(m.FadeIn(blocks, shift=0.3 * m.DOWN), run_time=0.6)
# 直到大木块离开画面,或最后一次碰撞后片刻
last = times[-1]
leave = (7.4 - positions[-1][1]) / max(velocities[-1][1], 1e-9) + last
duration = min(leave, last + 1.6)
self.play(
clock.animate.set_value(duration), run_time=duration, rate_func=m.linear
)
blocks.clear_updaters()
count = len(times) - 1
entry = m.MathTex(rf"{mass} : {count}", font_size=40, color=m.GREY_A)
record.add(entry)
record.arrange(m.DOWN, aligned_edge=m.RIGHT, buff=0.22)
record.move_to([WALL_X + 0.6, 2.75, 0], aligned_edge=m.UL)
self.play(
m.FadeOut(blocks), m.TransformFromCopy(counter, entry), run_time=0.8
)
digits = m.MathTex(r"\pi = 3.14159\ldots", font_size=64, color=m.YELLOW)
digits.next_to(readout, m.DOWN, buff=0.5)
self.play(m.Write(digits), run_time=1.2)
self.wait(2)
if __name__ == "__main__":
CollidingBlocks().render("colliding_blocks.mp4")