跳转至

切伦科夫光

在水中超光速运动的电荷 留下一锥蓝光。影片度量锥角并与公式对照。

examples/cherenkov_cone.py
"""跑得比光快:水中的电荷在其后方留下一锥蓝光。

光在水中以 c/n 传播(n = 1.33)。电荷经过的每个点发出一个以 c/n 增长的子波:当 β < 1/n 时
它们层层嵌套;当 β > 1/n 时它们堆积成一个锥面,即光学音爆,光沿其法线以 Cherenkov 角
cos θ = 1/(nβ) 离开(Cherenkov 1934;Frank & Tamm 1937),在水中当 β → 1 时为 41°。
屏幕上的 θ 在三个速度下拟合到所绘波前;光子以 θ 角离开径迹,亮度如 Frank–Tamm 所述
(∝ sin²θ)。当电荷减速到 c/n 以下时光熄灭,已发射的光壳落在一面光电倍增管墙上
形成一个环:这就是 Super-Kamiokande 看到一个 μ 子的方式。
"""

from collections.abc import Callable

import numpy as np

import manimgx as m

N_WATER = 1.33
WAVE = 1.15  # c/n:每个子波的增长速度(每秒场景单位数)
LIGHT = N_WATER * WAVE  # c
LIFE = 2.8  # 子波在电荷后方绘制的秒数
# 光子在此年龄被吸收(水的衰减,已缩短)
PHOTON_LIFE = 3.5
TICK = 1 / 60
RING_EVERY = 6  # 绘制子波之间的模拟刻度数(0.1 秒)
# 光子每个刻度在这些方位角离开:每个方位角描出锥面的一条线
AZIMUTHS = 96
PHOTON_CAP = AZIMUTHS * 220
APEX_X = 2.0  # 相机跟随电荷时电荷在屏幕上的位置
FIT_AGES = (0.15, 0.8)  # 包络面拟合到电荷正后方的子波
PMT_PITCH = 0.15
PMT_RADIUS = 3.4
BRAKE = 2.4  # 电荷在距墙此距离处开始减速
WATER = "#020a17"
RING_BLUE = "#8fd3ff"
CHERENKOV = ["#ffffff", "#9ddcff", "#3fa9ff", "#1f5fd6"]
HIT_TIME = ["#fff3b0", "#ffb347", "#ff5e62", "#c84bff", "#4b6bff"]


def colormap(values: np.ndarray, stops: list[str]) -> np.ndarray:
    """[0, 1] 范围内值的 (n, 4) RGBA 行,通过颜色站点插值。"""
    rgb = np.array([m.ManimColor(s).to_rgb() for s in stops])
    x = np.clip(values, 0, 1) * (len(stops) - 1)
    i = np.minimum(x.astype(int), len(stops) - 2)
    f = (x - i)[:, None]
    out = np.ones((len(values), 4))
    out[:, :3] = rgb[i] * (1 - f) + rgb[i + 1] * f
    return out


def cloud(count: int, size: float) -> m.PMobject:
    mob = m.PMobject(stroke_width=size)
    mob.add_points(np.zeros((count, 3)), rgbas=np.zeros((count, 4)))
    return mob


class Tank:
    """电荷、其子波和其光子,在实验室参考系中;相机跟随电荷
    (`shift` 是绘制在屏幕 x = 0 处的实验室 x)直到 `follow` 被动画化为 0。
    """

    def __init__(self, beta: m.ValueTracker, follow: m.ValueTracker) -> None:
        self.beta = beta
        self.follow = follow
        self.time = 0.0
        self.ticks = 0
        self.x = 0.0
        self.speed = beta.get_value()  # 测量得到:每刻度距离除以 c
        self.shift = -APEX_X
        self.wall = np.inf
        self.brake_time = np.inf  # 电荷开始减速的时刻
        rings = int(LIFE / (RING_EVERY * TICK)) + 2
        self.ring_x = np.zeros(rings)
        self.ring_t = np.full(rings, -np.inf)
        self.photon_x = np.zeros(PHOTON_CAP)  # 径迹上的发射点
        self.photon_dir = np.zeros((PHOTON_CAP, 3))
        self.photon_t = np.full(PHOTON_CAP, -np.inf)
        # 每个光子的亮度:Frank–Tamm,∝ sin²θ 每单位长度
        self.photon_w = np.zeros(PHOTON_CAP)
        self.alive = np.zeros(PHOTON_CAP, bool)
        self.next_photon = 0
        # 墙的光电倍增管:圆盘内的方形网格管阵,面向径迹
        g = np.arange(-PMT_RADIUS, PMT_RADIUS + 1e-9, PMT_PITCH)
        yy, zz = np.meshgrid(g, g, indexing="ij")
        self.pmt_side = len(g)
        self.pmt_inside = (np.hypot(yy, zz) <= PMT_RADIUS).ravel()
        self.pmt_yz = np.stack([yy.ravel(), zz.ravel()], axis=1)
        self.pmt_flash = np.zeros(len(self.pmt_yz))
        self.pmt_charge = np.zeros(len(self.pmt_yz))
        self.pmt_first = np.full(len(self.pmt_yz), np.inf)

    # ── 模拟:一个 60 Hz 刻度 ──────────────────────────────────────────────────────
    def step(self, dt: float) -> None:
        if dt <= 0:
            return
        beta = self.beta.get_value()
        if beta < 0.985 and self.x > 0 and np.isfinite(self.wall):
            self.brake_time = min(self.brake_time, self.time)
        before = self.x
        self.x += beta * LIGHT * dt
        self.speed = (self.x - before) / dt / LIGHT
        self.shift += self.follow.get_value() * (self.x - before)
        self.time += dt
        self.ticks += 1
        if beta > 0.3 and self.ticks % RING_EVERY == 0:
            k = int(np.argmin(self.ring_t))  # 复用最旧的槽位
            self.ring_x[k], self.ring_t[k] = self.x, self.time
        if N_WATER * beta > 1:  # 比水中光快:它发光
            self.emit(beta)
        self.alive &= self.time - self.photon_t < PHOTON_LIFE
        self.absorb()
        self.pmt_flash *= np.exp(-dt / 0.35)

    def emit(self, beta: float) -> None:
        """光子以 Cherenkov 角离开径迹,环绕四周。"""
        cos_t = 1 / (N_WATER * beta)
        sin2 = 1 - cos_t**2
        count = AZIMUTHS
        idx = (self.next_photon + np.arange(count)) % PHOTON_CAP
        self.next_photon = (self.next_photon + count) % PHOTON_CAP
        az = np.linspace(0, 2 * np.pi, count, endpoint=False)
        sin_t = np.sqrt(sin2)
        self.photon_w[idx] = beta * sin2 / (1 - 1 / N_WATER**2)
        self.photon_dir[idx] = np.stack(
            [np.full(count, cos_t), sin_t * np.cos(az), sin_t * np.sin(az)], axis=1
        )
        self.photon_x[idx] = self.x
        self.photon_t[idx] = self.time
        self.alive[idx] = True

    def photons(self) -> np.ndarray:
        """所有光子槽位的实验室位置。"""
        age = self.time - self.photon_t
        pos = WAVE * np.where(np.isfinite(age), age, 0)[:, None] * self.photon_dir
        pos[:, 0] += self.photon_x
        return pos

    def absorb(self) -> None:
        """本刻度穿越墙平面的光子被最近的管子捕获。"""
        if not np.isfinite(self.wall):
            return
        pos = self.photons()
        crossed = self.alive & (pos[:, 0] >= self.wall)
        if not crossed.any():
            return
        idx = np.nonzero(crossed)[0]
        # 每个光子与平面相遇的位置
        age = (self.wall - self.photon_x[idx]) / (WAVE * self.photon_dir[idx, 0])
        yz = WAVE * age[:, None] * self.photon_dir[idx, 1:]
        cell = np.round((yz + PMT_RADIUS) / PMT_PITCH).astype(int)
        cell = np.clip(cell, 0, self.pmt_side - 1)
        tube = cell[:, 0] * self.pmt_side + cell[:, 1]
        np.add.at(self.pmt_flash, tube, self.photon_w[idx])
        np.add.at(self.pmt_charge, tube, self.photon_w[idx])
        self.pmt_first[tube] = np.minimum(self.pmt_first[tube], self.time)
        self.alive[idx] = False

    # ── 测量 ─────────────────────────────────────────────────────────────────────
    def fitted_angle(self) -> float | None:
        """拟合到所绘波前的 θ:过电荷、与 0.15–0.8 秒前发射的子波相切的
        最小二乘直线;光沿其法线离开。当电荷仍在自身子波内时
        返回 None:此时没有锥面。"""
        age = self.time - self.ring_t
        chosen = (age > FIT_AGES[0]) & (age < FIT_AGES[1])
        if chosen.sum() < 3:
            return None
        behind = self.x - self.ring_x[chosen]
        radius = WAVE * age[chosen]
        s = radius / behind
        if np.any(s >= 1):
            return None
        # 从电荷出发的直线与每个波前相切的点(电荷在原点)
        tx = -behind + radius * s
        ty = radius * np.sqrt(1 - s * s)
        alpha = np.arctan(-(tx * ty).sum() / (tx * tx).sum())
        return float(np.pi / 2 - alpha)


class CherenkovCone(m.ThreeDScene):
    def construct(self) -> None:
        self.camera.background_color = WATER
        self.set_camera_orientation(phi=0, theta=-90 * m.DEGREES, zoom=0.92)
        beta = m.ValueTracker(0.60)
        follow = m.ValueTracker(1.0)  # 1:相机随电荷同行
        glow = m.ValueTracker(0.0)  # 光子的可见度
        rings_on = m.ValueTracker(1.0)
        fit_on = m.ValueTracker(0.0)
        track_on = m.ValueTracker(1.0)
        wall_on = m.ValueTracker(0.0)
        charge_on = m.ValueTracker(1.0)
        tank = Tank(beta, follow)
        for _ in range(int(LIFE / TICK)):  # 以一整列子波开始
            tank.step(TICK)

        def here(lab_x: float) -> np.ndarray:
            return np.array([lab_x - tank.shift, 0.0, 0.0])

        # ── 子波:环形缓冲区每个槽位一个圆 ────────────────────────────────
        template = m.Circle(radius=1).points.copy()
        circles = [
            m.Circle(radius=1, stroke_width=1.7, color=RING_BLUE) for _ in tank.ring_t
        ]
        rings = m.VGroup(*circles)

        def draw_rings(_: m.Mobject) -> None:
            ages = tank.time - tank.ring_t
            for circle, age, x in zip(circles, ages, tank.ring_x, strict=True):
                if not 0 <= age < LIFE:
                    circle.set_stroke(opacity=0)
                    continue
                circle.points = template * max(WAVE * age, 0.01) + here(x)
                fade = (1 - age / LIFE) ** 1.4
                circle.set_stroke(opacity=0.9 * fade * rings_on.get_value())

        rings.add_updater(draw_rings)

        # ── 锥面上的光子 ──────────────────────────────────────────────────────────────
        photons = cloud(PHOTON_CAP, 2.6)
        photon_halo = cloud(PHOTON_CAP, 8.0)  # 每个光子周围的柔和光晕

        def draw_photons(mob: m.Mobject) -> None:
            pos = tank.photons()
            pos[:, 0] -= tank.shift
            age = np.nan_to_num((tank.time - tank.photon_t) / PHOTON_LIFE, posinf=1)
            rows = colormap(age, CHERENKOV)
            # 更亮的带(一起发射的光子)沿锥面滑下:每条带是一个
            # 子波与锥面相切之处
            emitted = np.where(tank.alive, tank.photon_t, 0)
            pulse = 0.65 + 0.35 * np.cos(2 * np.pi * 2.5 * emitted)
            fade = np.clip((1 - age) / 0.3, 0, 1) * tank.photon_w * pulse
            shown = np.where(tank.alive, fade, 0) * glow.get_value()
            for layer, alpha in ((mob, 1.0), (photon_halo, 0.07)):
                rows[:, 3] = alpha * shown
                layer.points = pos
                layer.paint = layer.paint.but(fill=rows.copy())

        photons.add_updater(draw_photons)

        # ── 水中微粒:它们显示运动 ──────────────────────────────────────────────
        rng = np.random.default_rng(3)
        speck_lab = rng.uniform([-12, -4.5, -4.5], [12, 4.5, 4.5], (420, 3))
        specks = cloud(len(speck_lab), 1.8)

        def draw_specks(mob: m.Mobject) -> None:
            pos = speck_lab.copy()
            pos[:, 0] = (pos[:, 0] - tank.shift + 12) % 24 - 12
            rows = np.tile([0.55, 0.7, 0.85, 0.0], (len(pos), 1))
            rows[:, 3] = 0.3 * np.clip((11.5 - np.abs(pos[:, 0])) / 2, 0, 1)
            mob.points = pos
            mob.paint = mob.paint.but(fill=rows)

        specks.add_updater(draw_specks)

        # ── 电荷、其光晕及其径迹 ──────────────────────────────────────────────
        charge = m.Dot3D(radius=0.07, color=m.WHITE)
        halos = [cloud(1, w) for w in (12, 24, 44)]
        track = m.VMobject(stroke_color=m.WHITE, stroke_width=1.5)

        def draw_charge(mob: m.Mobject) -> None:
            p = here(tank.x)
            shown = charge_on.get_value()
            mob.move_to(p)
            mob.set_opacity(shown)
            for halo, a in zip(halos, (0.5, 0.18, 0.07), strict=True):
                halo.points = p[None, :]
                halo.paint = halo.paint.but(
                    fill=np.array([[0.62, 0.86, 1.0, a * shown]])
                )
            track.set_points_as_corners([p - np.array([7.5, 0, 0]), p])
            track.set_stroke(opacity=0.3 * track_on.get_value())

        charge.add_updater(draw_charge)

        # ── 拟合包络面、一条光线及其角度 ──────────────────────────────────────────
        def fit_marks() -> m.VGroup:
            theta = tank.fitted_angle()
            show = fit_on.get_value()
            if theta is None or show < 0.01:
                return m.VGroup()
            alpha = np.pi / 2 - theta
            apex = here(tank.x)
            lines = m.VGroup(
                *[
                    m.DashedLine(
                        apex,
                        apex
                        + 3.4 * np.array([-np.cos(alpha), sign * np.sin(alpha), 0]),
                        dash_length=0.12,
                        stroke_width=2.4,
                        color=m.GOLD,
                    )
                    for sign in (1, -1)
                ]
            )
            # 一条从电荷后方 2.6 处到包络面的光线,垂直于包络面
            back = 2.6
            source = apex - np.array([back, 0, 0])
            foot = apex + back * np.cos(alpha) * np.array(
                [-np.cos(alpha), np.sin(alpha), 0]
            )
            ray = m.Arrow(
                source,
                foot,
                buff=0,
                stroke_width=3,
                color=m.GOLD,
                max_tip_length_to_length_ratio=0.12,
            )
            arc = m.Arc(radius=0.55, start_angle=0, angle=theta, arc_center=source)
            arc.set_stroke(m.GOLD, 2)
            label = m.MathTex(r"\theta", font_size=30, color=m.GOLD).move_to(
                source + 0.82 * np.array([np.cos(theta / 2), np.sin(theta / 2), 0])
            )
            marks = m.VGroup(lines, ray, arc, label)
            marks.set_opacity(show)
            return marks

        fit = m.always_redraw(fit_marks)

        # ── 光电倍增管墙 ─────────────────────────────────────────────────────────────
        inside = tank.pmt_inside
        pmts = cloud(int(inside.sum()), 8.0)

        def draw_wall(mob: m.Mobject) -> None:
            x = tank.wall - tank.shift if np.isfinite(tank.wall) else 40.0
            yz = tank.pmt_yz[inside]
            mob.points = np.column_stack([np.full(len(yz), x), yz])
            on = wall_on.get_value()
            first = tank.pmt_first[inside]
            hit = np.isfinite(first)
            # 按到达时间着色,跨越光环落下的那半秒
            start = tank.brake_time + 2.55 if np.isfinite(tank.brake_time) else 0.0
            timed = colormap(np.where(hit, (first - start) / 0.55, 0), HIT_TIME)
            caught = tank.pmt_charge[inside]  # 捕获的光子,按 Frank–Tamm 加权
            light = np.clip(caught / 4.0, 0, 1)[:, None]
            flash = np.clip(tank.pmt_flash[inside] / 2.0, 0, 1)[:, None]
            rows = np.tile([0.3, 0.36, 0.45, 0.0], (len(yz), 1))
            rows[:, :3] = rows[:, :3] * (1 - light) + timed[:, :3] * light
            rows[:, :3] = rows[:, :3] * (1 - flash) + flash
            lit = np.maximum(0.3 + 0.7 * light, flash)[:, 0]
            rows[:, 3] = on * np.where(hit, lit, 0.2)
            mob.paint = mob.paint.but(fill=rows)

        pmts.add_updater(draw_wall)

        driver = m.ValueTracker(0)
        driver.add_updater(lambda _, dt: tank.step(dt))

        # ── 平视显示器 ─────────────────────────────────────────────────────────────────
        title = m.Text("Outrunning light", font_size=38).to_corner(m.UL)
        subtitle = m.Text(
            "a charge moving faster than light does in water", font_size=22
        ).set_color(m.GREY_B)
        subtitle.next_to(title, m.DOWN, aligned_edge=m.LEFT, buff=0.15)

        law = m.MathTex(r"\cos\theta = \frac{1}{n\beta}", font_size=40)
        medium = m.MathTex(r"n = 1.33\ \text{(water)}", font_size=26).set_color(
            m.GREY_B
        )
        law_block = m.VGroup(law, medium).arrange(m.DOWN, buff=0.15)
        law_block.to_corner(m.UR).shift(0.1 * m.LEFT)

        def readout(tex: str, value: m.DecimalNumber) -> m.VGroup:
            return m.VGroup(m.MathTex(tex, font_size=30), value).arrange(
                m.RIGHT, buff=0.15
            )

        beta_value = m.DecimalNumber(0.6, num_decimal_places=3, font_size=30)
        beta_value.add_updater(lambda d: d.set_value(tank.speed))
        fitted_value = m.DecimalNumber(
            0, num_decimal_places=1, font_size=30, unit=r"^\circ", color=m.GOLD
        )
        formula_value = m.DecimalNumber(
            0, num_decimal_places=1, font_size=30, unit=r"^\circ"
        )

        def fitted(d: m.DecimalNumber) -> None:
            theta = tank.fitted_angle()
            d.set_value(0 if theta is None else np.degrees(theta))
            d.set_opacity(0 if theta is None else 1)

        def formula(d: m.DecimalNumber) -> None:
            ok = N_WATER * tank.speed > 1
            d.set_value(np.degrees(np.arccos(1 / (N_WATER * tank.speed))) if ok else 0)
            d.set_opacity(1 if ok else 0)

        fitted_value.add_updater(fitted)
        formula_value.add_updater(formula)
        rows = m.VGroup(
            readout(r"\beta =", beta_value),
            readout(r"\theta_{\text{fit}} =", fitted_value),
            readout(r"\arccos\tfrac{1}{n\beta} =", formula_value),
        ).arrange(m.DOWN, aligned_edge=m.LEFT, buff=0.18)
        rows.next_to(law_block, m.DOWN, buff=0.4).align_to(law_block, m.LEFT)
        no_cone = m.Text("no cone", font_size=24).set_color(m.GREY_B)
        no_cone.next_to(rows[1][0], m.RIGHT, buff=0.15)
        no_cone.add_updater(
            lambda t: t.set_opacity(0 if tank.fitted_angle() is not None else 1)
        )
        no_angle = m.Text("none", font_size=24).set_color(m.GREY_B)
        no_angle.next_to(rows[2][0], m.RIGHT, buff=0.15)
        no_angle.add_updater(
            lambda t: t.set_opacity(0 if N_WATER * tank.speed > 1 else 1)
        )

        # θ(β):定律、阈值、实时拟合和测量点
        axes = m.Axes(
            x_range=[0.6, 1.0, 0.1],
            y_range=[0, 45, 15],
            x_length=3.4,
            y_length=2.0,
            tips=False,
            axis_config={"stroke_width": 1.5, "color": m.GREY_B},
        )
        axes.to_corner(m.DR).shift(0.25 * m.UP + 0.35 * m.LEFT)
        threshold = 1 / N_WATER
        curve = axes.plot(
            lambda b: np.degrees(np.arccos(min(1.0, 1 / (N_WATER * b)))),
            x_range=[threshold, 1.0, 0.002],
            color=CHERENKOV[2],
            stroke_width=2.5,
        )
        cut = m.DashedLine(
            axes.c2p(threshold, 0), axes.c2p(threshold, 45), stroke_width=1.5
        ).set_color(m.GREY_B)
        labels = m.VGroup(
            m.MathTex(r"\beta", font_size=28).next_to(
                axes.c2p(1.0, 0), m.RIGHT, buff=0.15
            ),
            m.MathTex(r"1", font_size=24).next_to(axes.c2p(1.0, 0), m.DOWN, buff=0.12),
            m.MathTex(r"\tfrac{1}{n}", font_size=26).next_to(
                axes.c2p(threshold, 0), m.DOWN, buff=0.12
            ),
            m.MathTex(r"\theta", font_size=28).next_to(
                axes.c2p(0.6, 45), m.UP, buff=0.12
            ),
            m.MathTex(r"45^\circ", font_size=22).next_to(
                axes.c2p(0.6, 45), m.LEFT, buff=0.12
            ),
        )
        live = m.Dot(radius=0.06, color=m.GOLD)

        def track_live(d: m.Mobject) -> None:
            theta = tank.fitted_angle()
            b = float(np.clip(tank.speed, 0.6, 1.0))
            d.move_to(axes.c2p(b, 0 if theta is None else np.degrees(theta)))

        live.add_updater(track_live)
        plot = m.VGroup(axes, curve, cut, labels)

        # 半透明点将其后绘制的内容遮挡在后面:最后添加这些
        self.add(driver, specks, track, rings, pmts, charge, fit)
        self.add(photons, photon_halo, *halos)
        readouts = m.VGroup(rows, no_cone, no_angle, live)
        self.add_fixed_in_frame_mobjects(title, subtitle, law_block, readouts, plot)
        self.remove(title, subtitle, law_block, readouts, plot)

        def stamp() -> None:
            """将此速度下的拟合角放到图上。"""
            theta = tank.fitted_angle()
            if theta is None:
                return
            dot = m.Dot(
                axes.c2p(tank.speed, np.degrees(theta)), radius=0.07, color=m.WHITE
            )
            ring = m.Circle(radius=0.13, stroke_width=2, color=m.WHITE).move_to(dot)
            self.add_fixed_in_frame_mobjects(dot, ring)
            self.remove(dot, ring)
            self.play(m.FadeIn(dot, scale=2), m.FadeIn(ring, scale=0.4), run_time=0.4)

        # 0–3 秒:低于阈值时子波层层嵌套
        self.play(m.Write(title), m.FadeIn(subtitle), run_time=1.5)
        self.play(m.FadeIn(law_block), m.FadeIn(readouts), m.FadeIn(plot), run_time=1.2)
        self.wait(0.3)
        # 3–12 秒:加速,穿过阈值;在三个速度下拟合锥面
        self.play(beta.animate.set_value(0.80), run_time=1.8)
        self.play(fit_on.animate.set_value(1), run_time=0.8)
        self.wait(0.6)
        stamp()
        self.play(beta.animate.set_value(0.90), run_time=1.2)
        self.wait(0.9)
        stamp()
        self.play(beta.animate.set_value(0.99), run_time=1.2)
        self.wait(0.9)
        stamp()
        # 12–18 秒:三维中的锥面,由其光子照亮
        self.move_camera(
            phi=64 * m.DEGREES,
            theta=-118 * m.DEGREES,
            zoom=1.0,
            frame_center=np.array([0.7, -0.15, 0.3]),
            run_time=3.5,
            added_anims=[
                glow.animate.set_value(1),
                rings_on.animate.set_value(0.3),
                track_on.animate.set_value(0.5),
                fit_on.animate.set_value(0),
            ],
        )
        orbit = 4.5
        tank.wall = tank.x + BRAKE + beta.get_value() * LIGHT * orbit
        self.play(
            self.camera.theta_tracker.animate.set_value(-72 * m.DEGREES),
            self.camera.phi_tracker.animate.set_value(70 * m.DEGREES),
            run_time=orbit,
        )
        # 20–23 秒:电荷减速到 c/n 以下:锥面闭合并变暗;相机
        # 放手并转向墙,已发射的光在那里落成一个环
        swing = 2.6
        wall_x = tank.wall - tank.shift - 0.15  # 相机停止后它停在哪里

        def within(
            seconds: float, rate: Callable[[float], float] = m.smooth
        ) -> Callable[[float], float]:
            """一个在摆动的前 `seconds` 内完成的速率函数。"""
            return lambda t: rate(min(1.0, t * swing / seconds))

        self.move_camera(
            phi=84 * m.DEGREES,
            theta=-10 * m.DEGREES,
            zoom=0.9,
            frame_center=np.array([wall_x, 1.33, 0.33]),
            run_time=swing,
            added_anims=[
                beta.animate(rate_func=within(1.6, m.rush_from)).set_value(0.0),
                follow.animate(rate_func=within(0.8, m.rush_from)).set_value(0.0),
                wall_on.animate(rate_func=within(1.2)).set_value(1),
                rings_on.animate.set_value(0),
                track_on.animate.set_value(0),
                charge_on.animate.set_value(0),
            ],
        )
        self.play(
            m.FadeOut(readouts),
            self.camera.theta_tracker.animate.set_value(-18 * m.DEGREES),
            run_time=3.0,
            rate_func=m.linear,
        )
        closing = m.Text(
            "Super-Kamiokande sees a stopping muon as a ring like this", font_size=26
        )
        closing.to_edge(m.DOWN, buff=0.3).to_edge(m.LEFT, buff=0.4)
        self.add_fixed_in_frame_mobjects(closing)
        self.remove(closing)
        self.play(
            m.FadeIn(closing, shift=0.2 * m.UP),
            self.camera.theta_tracker.animate.set_value(-22 * m.DEGREES),
            run_time=1.4,
            rate_func=m.linear,
        )
        self.play(
            self.camera.theta_tracker.animate.set_value(-30 * m.DEGREES),
            run_time=3.2,
            rate_func=m.linear,
        )


if __name__ == "__main__":
    CherenkovCone().render("cherenkov_cone.mp4")