Skip to content

Value Error [Errno 27] raised when combining to movie file #2366

@roymina

Description

@roymina

Describe the bug

Value Error [Errno 27] raised when combining to movie file.
The bug seems to be resolution related. No problems when using the -pql parameter.
Code:

from manim import *
import math
import numpy as np

class QuinticUnsolvability(Scene):
    def construct(self):
        # self.next_section(skip_animations=True)
        # 台词:这个视频中,我们将不用伽罗瓦理论来证明五次方程的不可解性。你需要知道复数相关的知识、代数基本定理。
        text1 = Text(r"韦达定理、复数相关知识(欧拉公式、复数运算)").move_to(UP)
        text2 = Text(r"代数基本定理:一元n次方程恰好有n个复数根").move_to(DOWN)
        self.play(Write(text1), Write(text2))
        self.wait(4)
        self.play(FadeOut(text1), FadeOut(text2))


        
        text = Text("Chapter 0: 复数开根", font_size=80)
        self.play(FadeIn(text))
        self.wait(2)
        self.play(FadeOut(text))

        # 台词:我们知道,对一个数x进行开n次方根,实际上就是在找一个数y,使得y的n次方等于x。比如说,我们对2开立方根。
        text = MathTex(r"\sqrt[n]{x} = y \Leftrightarrow y^n = x")
        self.play(Write(text))
        self.wait(3)
        self.play(Transform(text, MathTex(r"\sqrt[3]{2} = y \Leftrightarrow y^3 = 2")))
        # 台词:在实数域内,这个方程只有一个根,2的立方根只有一个值。但是,根据代数基本定理,这个三次方程一定有3个根。既然实数域内只有一个根,那么剩下的两个根一定是复数根。它们在哪里呢?
        self.wait(12)
        self.play(FadeOut(text))

        # 台词:我们假设2的立方根是一个复数z,它与x轴成夹角theta。我们可以用欧拉公式将其写成这样。
        text = MathTex(r"z = re^{i\theta}").shift(UP * 2)
        self.play(Write(text))
        self.wait(3)
        # 现在,z的立方就是这样,它等于2。
        text2 = MathTex(r"z^3 = r^3 e^{i3\theta}").shift(UP * 0.5)
        self.play(Write(text2))
        self.wait(3)
        text3 = MathTex(r"=2").next_to(text2, RIGHT)
        self.play(Write(text3))
        # 我们把2也看作一个复数,用欧拉公式表示。注意辐角可能等于2k派,其中k是任意整数。
        text4 = MathTex(r"z^3=r^3 e^{i3\theta}=2\cdot e^{i\cdot 2k\pi},k\in\mathbb{Z}").shift(DOWN * 1)
        self.play(Write(text4))
        self.play(FadeOut(text), FadeOut(text2), FadeOut(text3), text4.animate.move_to(UP * 2))
        self.wait(3)
        # 分别让等式两边的模长、幅角相等。
        text5 = MathTex(r"r^3=2, 3\theta=0+2k\pi, k\in\mathbb{Z}").shift(UP * 0.5)
        self.play(Write(text5))
        self.wait(2)

        # 由此,我们可以得到r和theta的值。
        text6 = MathTex(r"r=\sqrt[3]{2}, \theta=\frac{2k\pi}{3}, k\in\mathbb{Z}").shift(DOWN * 1)
        self.play(Write(text6))
        self.wait(3)

        # 台词:现在,我们可以得到2的立方根的三个值了。它们分别对应这3个红点。
        plane = ComplexPlane(
            x_range=[-12, 12, 1],
            y_range=[-6, 6, 1],
            background_line_style={
                "stroke_color": BLUE,
                "stroke_width": 1,
                "stroke_opacity": 0.3
            }
        ).scale(0.8)
        plane.add_coordinates()
        self.play(Create(plane), FadeOut(text4), FadeOut(text5), text6.animate.move_to(UP * 2))
        z = Dot(plane.c2p(2), color = YELLOW)
        self.play(Create(z))
        def get_roots(z, n):
            roots = []
            unit = math.cos(2 * math.pi / n) + math.sin(2 * math.pi / n) * 1j
            for k in range(n):
                root = z ** (1/n) * (unit ** k)
                roots.append(root)
            return roots
        roots = VGroup(*[
            Dot(color=RED).move_to(plane.n2p(pos)) for pos in get_roots(2, 3)
        ])
        self.play(Create(roots))
        self.wait(2)

        # 台词:当我们移动黄点时,红点也会跟着移动。当黄点绕原点转一圈时,红点会转三分之一圈。这是因为黄点的辐角增加了2派,它的三次方根的辐角就会增加2派的三分之一。
        def update_roots(mob):
            new_roots = get_roots(plane.p2n(z.get_center()), 3)
            for dot, pos in zip(mob, new_roots):
                dot.move_to(plane.n2p(pos))
        roots.add_updater(update_roots)
        self.play(z.animate.move_to(plane.c2p(2, 2)), FadeOut(text6))
        self.play(z.animate.move_to(plane.c2p(-2, 2)))
        self.play(z.animate.move_to(plane.c2p(-2, -2)))
        self.play(z.animate.move_to(plane.c2p(2, -2)))
        self.play(z.animate.move_to(plane.c2p(2, 0)))
        self.play(FadeOut(z), FadeOut(roots), FadeOut(plane))
        for mob in self.mobjects:
            mob.clear_updaters()


        # self.next_section(skip_animations=True)
        text = Text("Chapter 1: 二次方程", font_size=80)
        self.play(FadeIn(text))
        self.wait(2)
        self.play(FadeOut(text))
        # 台词:现在,我们来研究二次方程。我们知道,二次方程具有这样的形式。为了简化,我们把等式除以a,使得最高次项的系数为1。这样一来,我们就可以用求根公式求出它的根。
        quad_eq = MathTex(r"ax^2+bx+c=0").move_to(UP * 2)
        self.play(Write(quad_eq))
        self.wait(3)
        quad_eq2 = MathTex(r"x^2+c_1x+c_0=0").next_to(quad_eq, DOWN)
        self.play(Write(quad_eq2))
        self.wait(5)
        solved_eq = MathTex(r"x=\frac{-c_1\pm\sqrt{c_1-4c_0}}{2}").next_to(quad_eq2, DOWN)
        self.play(Write(solved_eq))
        self.wait(3)
        # 台词:二次方程的根可以是复数,系数a、b、c也可以为复数。让我们在复平面上画出一个二次方程的根。根据韦达定理,我们可以求出二次方程的系数。接下来我们要证明的是,不存在仅使用加减乘除的二次方程求根公式。
        def create_point(pos, label, color=YELLOW, fill_opacity=1.0):
            dot_inst = Dot(plane.n2p(pos), color=color, fill_opacity=fill_opacity)
            label_inst = label.next_to(Dot(plane.n2p(pos), color=color), UR)
            label_inst.add_updater(lambda m: m.next_to(dot_inst.get_center(), UR))
            group = VGroup(dot_inst, label_inst)
            group.dot = dot_inst
            group.label = label_inst
            return group
        self.wait(4)
        self.play(FadeOut(quad_eq), quad_eq2.animate.move_to(DOWN * 3 + LEFT * 5), FadeOut(solved_eq), FadeIn(plane))
        root_vals = [0.7 + 0.9j, -1.3 + 1.9j]
        root_dots = [create_point(pos, MathTex(f"x_{i+1}")) for i, pos in enumerate(root_vals)]
        self.play(*[Create(root) for root in root_dots])
        self.wait(3)
        vieta = MathTex(r"c_1 = x_1 + x_2, c_0 = x_1 \cdot x_2").next_to(quad_eq2, UR)
        self.play(FadeIn(vieta))
        self.wait(1)
        def decide_coeffs(roots):
            coeffs = [1 + 0j]
            for r in roots:
                new_coeffs = [0] * (len(coeffs) + 1)
                for i in range(len(coeffs)):
                    new_coeffs[i] += coeffs[i] * (-r)
                    new_coeffs[i + 1] += coeffs[i]
                coeffs = new_coeffs
            return coeffs[:len(coeffs) - 1]

        coeffs = decide_coeffs(root_vals)
        coeff_dots = [create_point(coeffs[0], MathTex(r"c_0"), color=RED),
                      create_point(coeffs[1], MathTex(r"c_1"), color=RED)]
        self.play(*[Create(dot) for dot in coeff_dots])
        self.wait(3)
        self.play(FadeOut(vieta))
        # 证明方法就是假设我们现在有不含根号的求根公式F1和F2,比如长这样的,它们仅使用四则运算分别计算出方程的两个根。
        text2 = MathTex(r"x_{2(jiade)}'=F_2(c_0, c_1)=c_1-\frac{c_0}{2c_1}+5.4321").move_to(DOWN * 2 + LEFT * 4).scale(0.7)
        text1 = MathTex(r"x_{1(jiade)}'=F_1(c_0, c_1)=c_1+\frac{c_0}{2c_1}+5.4321").move_to(DOWN + LEFT * 4).scale(0.7)
        self.play(Write(text1), Write(text2))
        c0 = plane.p2n(coeff_dots[0].dot.get_center())
        c1 = plane.p2n(coeff_dots[1].dot.get_center())
        def get_x1n(c0, c1):
            return c1 + c0 / (2 * c1) + 5.4321
        def get_x2n(c0, c1):
            return c1 - c0 / (2 * c1) + 5.4321
        x1n = create_point(get_x1n(c0, c1), MathTex("x_{1(jiade)}'").scale(0.7), color=WHITE, fill_opacity=0.5)
        x2n = create_point(get_x2n(c0, c1), MathTex("x_{2(jiade)}'").scale(0.7), color=WHITE, fill_opacity=0.5)
        self.play(Create(x1n))
        self.play(Create(x2n))
        self.wait(6)
        # 注意现在我们是用韦达定理从两个根推出方程系数,再将系数代入假的求根公式中得出两个假根。接下来是关键的一步操作,我们交换二次方程的两个根x1和x2的位置。
        def permute_anims(roots, permutations):
            anims = []
            new_roots = roots.copy()
            for i in permutations:
                if(len(i) <= 2):
                    # 只有两个根,一个沿圆形轨道,另一个沿直线
                    dot1 = roots[i[0]].dot
                    dot2 = roots[i[1]].dot
                    center = (dot1.get_center() + dot2.get_center()) / 2
                    radius = np.linalg.norm(dot1.get_center() - center)
                    delta = dot1.get_center() - dot2.get_center()
                    angle = np.arctan2(delta[1], delta[0])
                    semi_circle_x1 = Arc(
                        radius=radius,
                        start_angle=angle,
                        angle=PI if i[0] > i[1] else -PI,
                        arc_center=center,
                        color=BLUE
                    )
                    anims.append(MoveAlongPath(dot1, semi_circle_x1))
                    anims.append(dot2.animate.move_to(dot1.get_center()))
                    new_roots[i[0]] = roots[i[1]]
                    new_roots[i[1]] = roots[i[0]]
                    
                else:
                    # 多个根直线交换
                    for j in range(len(i) - 1):
                        dot1 = roots[i[j]].dot
                        dot2 = roots[i[j + 1]].dot
                        anims.append(dot1.animate.move_to(dot2.get_center()))
                        new_roots[i[j + 1]] = roots[i[j]]
                    anims.append(roots[i[-1]].dot.animate.move_to(roots[i[0]].dot.get_center()))
                    new_roots[i[0]] = roots[i[-1]]
            
            for i in range(len(roots)):
                roots[i] = new_roots[i]
            return anims
        def update_coeff(i):
            return lambda mob: mob.move_to(plane.n2p(decide_coeffs([plane.p2n(root_dots[0].dot.get_center()), plane.p2n(root_dots[1].dot.get_center())])[i]))
        def listp2n(lst):
            return [plane.p2n(p) for p in lst]
        def update_fake_root1(mob):
            mob.move_to(plane.n2p(get_x1n(*listp2n([coeff_dot.get_center() for coeff_dot in coeff_dots]))))
        def update_fake_root2(mob):
            mob.move_to(plane.n2p(get_x2n(*listp2n([coeff_dot.get_center() for coeff_dot in coeff_dots]))))
        coeff_dots[0].dot.add_updater(update_coeff(0))
        coeff_dots[1].dot.add_updater(update_coeff(1))
        x1n.dot.add_updater(update_fake_root1)
        x2n.dot.add_updater(update_fake_root2)
        self.play(permute_anims(root_dots, [[0, 1]]), run_time=3)
        # 在两个根交换位置的过程中,方程的系数也会跟着变化,因此假根也会变化。现在,两个根已经互换了位置,但两个根的值其实并没有变,只是顺序变了,因此方程的系数也回到了原来的位置。方程的系数没有变,所以假根也不会变。但是,如果这个求根公式是真的,它求出来的根就必须紧紧跟着真正的根走,也就是说两个根必须交换。但是两个根只是在原地打转,所以,这个求根公式一定是假的!
        for _ in range(6):
            self.play(permute_anims(root_dots, [[0, 1]]), run_time=3)
        self.play(FadeOut(x1n), FadeOut(x2n), FadeOut(text1), FadeOut(text2))
        x1n.dot.clear_updaters()
        x2n.dot.clear_updaters()
        # 那么真正的求根公式是怎么避免这种情况的呢?答案就藏在公式里的根号中,它是判别式的平方根。在我们交换x1和x2的过程中,判别式绕着原点运动了一圈。
        self.wait(6)
        solved_eq.move_to(DOWN * 2 + LEFT * 4).scale(0.7)
        self.play(Write(solved_eq))
        delta_text = MathTex(r"\Delta = c_1^2 - 4c_0").next_to(solved_eq, UP)
        def get_delta(coeff_dots):
            return plane.p2n(coeff_dots[1].dot.get_center()) ** 2 - 4 * plane.p2n(coeff_dots[0].dot.get_center())
        delta_dot = create_point(get_delta(coeff_dots), MathTex(r"\Delta"), color=GREEN)
        self.play(Create(delta_dot), Write(delta_text))
        self.wait(2)
        def update_delta(mob):
            mob.move_to(plane.n2p(get_delta(coeff_dots)))
        delta_dot.dot.add_updater(update_delta)
        self.play(permute_anims(root_dots, [[0, 1]]), run_time=3)
        # 于是,判别式的平方根就会旋转半圈。也就是说,判别式的平方根并不会回到原来的位置,而是交换位置,这个求根公式就不能被我们搞掉。一个只含有加减乘除的求根公式只能求出单个值,但含有根号就可以求出多个值,求根公式自然就可以逃过一劫。
        sqrtdelta_dots = VGroup(*[create_point(pos, MathTex(r"\sqrt{\Delta}"), color=BLUE) for pos in get_roots(get_delta(coeff_dots), 2)])
        self.play(Create(sqrtdelta_dots))
        def update_sqrtdelta(mob):
            new_roots = get_roots(get_delta(coeff_dots), 2)
            for vg, pos in zip(mob, new_roots):
                vg.dot.move_to(plane.n2p(pos))
        sqrtdelta_dots.add_updater(update_sqrtdelta)
        for _ in range(2):
            self.play(permute_anims(root_dots, [[0, 1]]), run_time=3)
        self.wait(3)



        text = Text("Chapter 2: 三次方程", font_size=80)
        self.play(FadeOut(plane), FadeOut(root_dots[0]), FadeOut(root_dots[1]), FadeOut(coeff_dots[0]), FadeOut(coeff_dots[1]), FadeOut(delta_text), FadeOut(delta_dot), FadeOut(sqrtdelta_dots), FadeOut(quad_eq2), FadeOut(solved_eq), FadeIn(text))
        for mob in self.mobjects:
            mob.clear_updaters()
        self.wait(2)
        self.play(FadeOut(text))
        # 台词:研究完了二次方程,我们来研究三次方程。通过一些操作,我们可以把三次方程转化成这样。它的求根公式长这样,这就是卡尔达诺公式。三次方程的解法可以看看这个视频。可以看到,它的求根公式嵌套了两层根号。嗯,头皮发麻,赶紧隐藏。
        self.wait(2)
        cubic_eq = MathTex(r"ax^3+bx^2+cx+d=0").move_to(UP * 2)
        self.play(Write(cubic_eq))
        self.wait(3)
        cubic_eq2 = MathTex(r"x^3+px+q=0").next_to(cubic_eq, DOWN)
        self.play(Write(cubic_eq2))
        solved_eq = MathTex(r"x=\sqrt[3]{-\frac{q}{2}+\sqrt{\left(\frac{q}{2}\right)^2+\left(\frac{p}{3}\right)^3}}+\sqrt[3]{-\frac{q}{2}-\sqrt{\left(\frac{q}{2}\right)^2+\left(\frac{p}{3}\right)^3}}").next_to(cubic_eq2, DOWN)
        self.play(Write(solved_eq))
        self.wait(3)
        self.play(FadeOut(cubic_eq), FadeOut(solved_eq), FadeOut(cubic_eq2))
        cubic_eq2 = MathTex(r"x^3+c_2x^2+c_1x+c_0=0").move_to(DOWN * 3 + LEFT * 4)
        self.play(Create(cubic_eq2))
        plane.scale(2)
        plane = ComplexPlane(
            x_range=[-12, 12, 1],
            y_range=[-6, 6, 1],
            background_line_style={
                "stroke_color": BLUE,
                "stroke_width": 1,
                "stroke_opacity": 0.3
            }
        ).scale(2)
        self.play(Create(plane))
        # 根据代数基本定理,三次方程有三个根。三次方程也有韦达定理。事实上,韦达定理适用于任意次方程。先把方程的系数求出来。用类似二次方程的做法,我们可以证明,只用加减乘除是解不出来三次方程的,只需要随便选一对根交换就可以假掉它们。但是接下来我们要更进一步,我们要证明的是,即使加上单层的根号,也不能构造出三次方程的求根公式。换句话说,三次方程的求根公式必须包含嵌套的根号。
        self.wait(6)
        root_dots = [create_point(pos, MathTex(f"x_{i+1}")) for i, pos in enumerate([0.7 + 0.9j, -0.9 + 0.2j, 0.4 + 0.3j])]
        self.play(*[Create(root) for root in root_dots])
        coeff_dots = [create_point(pos, MathTex(f"c_{i}"), color=RED) for i, pos in enumerate(decide_coeffs(listp2n([vg.dot.get_center() for vg in root_dots])))]
        def update_coeff(i):
            return lambda mob: mob.move_to(plane.n2p(decide_coeffs(listp2n([vg.dot.get_center() for vg in root_dots]))[i]))
        [coeff_dots[i].dot.add_updater(update_coeff(i)) for i in range(len(coeff_dots))]
        self.play(*[Create(dot) for dot in coeff_dots])
        self.play(permute_anims(root_dots, [[0, 1]]), run_time=1)
        self.play(permute_anims(root_dots, [[0, 1]]), run_time=1)
        # 回顾刚才二次方程的证明,我们在交换x1和x2后发现假根并没有变化,从而证伪了那个求根公式。现在,我们可以用相同的思路,假设我们现在有这么一个求根公式,它只使用加减乘除和单层根号,每个根号下都有一坨东西。注意这里的根号都可以是任意次的,F和G都表示不含根号的有理函数。我们只需要找到一种操作,它交换了真正的根,但这个假公式的值不会变化。实际上,我们只需要让每一项的值都不变化就可以了。F0和G0是最好处理的,与刚才二次方程的情况相同,交换x1和x2,F0和G0都不会变化,因为它是只含四则运算的有理函数,直接假掉。
        fakesolvedeq_text = MathTex(r"x=\frac{F_0+\sqrt{F_1}+\sqrt{F_2}+...+\sqrt{F_n}}{G_0+\sqrt{G_1}+\sqrt{G_2}+...+\sqrt{G_m}}").next_to(cubic_eq2, UP).scale(0.7)
        self.play(Write(fakesolvedeq_text))
        f0_dot = create_point(0.5 * get_x1n(plane.p2n(coeff_dots[0].dot.get_center()), plane.p2n(coeff_dots[1].dot.get_center())), MathTex("F_0"), color=WHITE, fill_opacity=0.5)
        g0_dot = create_point(0.5 * get_x2n(plane.p2n(coeff_dots[0].dot.get_center()), plane.p2n(coeff_dots[1].dot.get_center())), MathTex("G_0"), color=WHITE, fill_opacity=0.5)
        self.play(Create(f0_dot), Create(g0_dot))
        def update_f0(mob):
            mob.move_to(plane.n2p(0.5 * get_x1n(*listp2n([vg.dot.get_center() for vg in coeff_dots[0:2]]))))
        def update_g0(mob):
            mob.move_to(plane.n2p(0.5 * get_x2n(*listp2n([vg.dot.get_center() for vg in coeff_dots[0:2]]))))
        f0_dot.dot.add_updater(update_f0)
        g0_dot.dot.add_updater(update_g0)
        self.play(permute_anims(root_dots, [[0, 1]]), run_time=1)
        self.play(permute_anims(root_dots, [[0, 1]]), run_time=1)
        self.play(FadeOut(f0_dot), FadeOut(g0_dot))
        f0_dot.dot.clear_updaters()
        g0_dot.dot.clear_updaters()
        # 接下来我们重点关注根号F1这一项。我们能不能用类似的方法交换方程的根,同时保持根号F1这一项不变呢?假设这个是F1,这些是根下F1的值。现在,我们交换x1和x2。然而,虽然F1在旋转了一圈后回到了原来的位置,但根下F1的值却发生了变化,它们只旋转了三分之一圈。所以,交换方程的根并不能证明这样的求根公式是假的……吗?
        note_text = Text("*由于接下来的动画有点眼花缭乱, 我们暂时隐藏方程的系数c0、c1和c2。").move_to(RIGHT * 4 + DOWN * 3).scale(0.3)
        self.play(Write(note_text), *[FadeOut(coeff_dot) for coeff_dot in coeff_dots])
        f1_dot = create_point(0.8 - 0.7j, MathTex("F_1"), color=GREEN)
        self.play(Create(f1_dot))
        self.wait(1)
        sqrtf1_dots = VGroup(create_point(pos, MathTex(r"\sqrt{F_1}"), color=BLUE) for pos in get_roots(plane.p2n(f1_dot.dot.get_center()), 3))
        self.play(Create(sqrtf1_dots), FadeOut(note_text))
        def update_sqrtf1(mob):
            new_roots = get_roots(plane.p2n(f1_dot.dot.get_center()), 3)
            for vg, pos in zip(mob, new_roots):
                vg.dot.move_to(plane.n2p(pos))
        sqrtf1_dots.add_updater(update_sqrtf1)
        self.wait(2)
        def rotate_dot_anim(dot, laps, center = 0.3 - 0.3j):
            center = plane.n2p(center)
            radius = np.linalg.norm(dot.get_center() - center)
            delta = dot.get_center() - center
            angle = np.arctan2(delta[1], delta[0])
            circle = Arc(
                radius=radius,
                start_angle=angle,
                angle= 2 * PI * laps,
                arc_center=center
            )
            return MoveAlongPath(dot, circle)
        for _ in range(2):
            self.play(permute_anims(root_dots, [[0, 1]]), rotate_dot_anim(f1_dot.dot, 1), run_time=3)
        # 刚才我们的操作全部都是在交换x1和x2,但x3被遗忘在了角落里。那如果我们交换一下x1和x3呢?看起来这一次F1旋转了4圈。但是这样也不能达成我们的目的,因为根下F1的值还是变化了。也许我们可以考虑把两种操作结合起来?我们把刚才两种操作分别记作操作1和操作2。我们先进行操作1,然后进行操作2。F1会先绕原点旋转一圈,然后再绕原点旋转4圈。看起来根下F1的值还是变化了。但是接下来的操作才是神来之笔,我们分别进行操作1的逆操作和操作2的逆操作。这会使F1反着转1圈,然后再反着转4圈。这样一来,F1就回到了原来的位置,更关键的是,正正反反四次操作对F1的辐角的影响全部抵消,所以根号下F1的值也回到了原来的位置。而比较前后根的顺序,发现三个根的顺序确实变化了,做了一个轮换。我们成功了!我们找到了一个操作,它交换了真正的根,但根号下F1的值不会变化。对于假求根公式中的各项也是如此,它们的值都不会变化。于是,整个公式的值都不会变化。所以,这个公式一定是假的!三次方程的求根公式一定含有嵌套的根号!
        self.wait(7)
        self.play(permute_anims(root_dots, [[0, 2]]), rotate_dot_anim(f1_dot.dot, 4), run_time=4)
        self.wait(1)
        self.play(permute_anims(root_dots, [[2, 0]]), rotate_dot_anim(f1_dot.dot, -4))
        self.wait(7)
        op1_text = Text("操作1: 1 ↔ 2").move_to(UP * 2 + LEFT * 4).scale(0.7)
        self.play(Write(op1_text))
        op2_text = Text("操作2: 1 ↔ 3").next_to(op1_text, DOWN).scale(0.7)
        self.play(Write(op2_text))
        self.wait(1)
        self.play(permute_anims(root_dots, [[0, 1]]), rotate_dot_anim(f1_dot.dot, 1), run_time=3)
        note_text = Text("*注意: 这里在进行完操作1后, 由于x1和x2已交换, 操作2实际上交换的是x2和x3。\n当多个操作连续进行时, 前面的操作不影响后面的操作, 我们以最开始时各位置上根的\n编号为基准。").move_to(RIGHT * 4 + DOWN * 3).scale(0.3)
        self.play(Write(note_text))
        self.play(permute_anims(root_dots, [[0, 2]]), rotate_dot_anim(f1_dot.dot, 4), run_time=3)
        self.wait(3)
        self.play(permute_anims(root_dots, [[1, 0]]), rotate_dot_anim(f1_dot.dot, -1), run_time=3)
        self.play(permute_anims(root_dots, [[2, 0]]), rotate_dot_anim(f1_dot.dot, -4), run_time=3)
        self.play(FadeOut(note_text))
        self.wait(12)
        self.play(permute_anims(root_dots, [[0, 1, 2]]))
        self.wait(1)
        self.play(permute_anims(root_dots, [[0, 2, 1]]))


        
        text = Text("Chapter 3: 四次方程", font_size=80)
        self.play(FadeOut(plane), *[FadeOut(root_dot) for root_dot in root_dots], FadeOut(f1_dot), *[FadeOut(sqrtf1_dot) for sqrtf1_dot in sqrtf1_dots], FadeOut(op1_text), FadeOut(op2_text), FadeOut(cubic_eq2), FadeOut(fakesolvedeq_text), FadeIn(text))
        for mob in self.mobjects:
            mob.clear_updaters()
        self.wait(2)
        # 接下来就是令人闻风丧胆的四次方程了。先上求根公式看看。很明显,这个公式嵌套了三层根号。也许你已经猜到我们要证什么了。没错,我们要证明四次方程的求根公式必须嵌套至少三层根号,只嵌套两层根号的解不存在。
        self.play(FadeOut(text))
        quar_eq = MathTex(r"ax^4+bx^3+cx^2+dx+e=0").move_to(UP)
        quar_eq2 = MathTex(r"x^4+c_3x^3+c_2x^2+c_1x+c_0=0").next_to(quar_eq, DOWN).scale(0.7)
        self.play(Write(quar_eq), Write(quar_eq2))
        self.wait(3)
        self.play(FadeOut(quar_eq), quar_eq2.animate.move_to(DOWN * 3 + LEFT * 4))
        quar_eq2.move_to(DOWN * 3 + LEFT * 4)
        # 用四次方程的韦达定理,可以解出方程的系数。让我们直奔主题,假设我们存在这样一个求根公式,它只使用加减乘除和最多两层根号。这一次,F和G是含有加减乘除和至多一层根号的函数。我们要证明它不存在。
        self.play(Create(plane))
        root_dots = [create_point(pos, MathTex(f"x_{i+1}")) for i, pos in enumerate([-0.12 + 1.03j, -0.53 + 0.56j, -0.28 - 0.47j, 0.19 + 0.1j])]
        self.play(*[Create(root) for root in root_dots])
        coeff_dots = [create_point(pos, MathTex(f"c_{i}"), color=RED) for i, pos in enumerate(decide_coeffs(listp2n([vg.dot.get_center() for vg in root_dots])))]
        self.play(*[Create(dot) for dot in coeff_dots])
        fakesolvedeq_text = MathTex(r"x=\frac{F_0+\sqrt{F_1}+\sqrt{F_2}+...+\sqrt{F_n}}{G_0+\sqrt{G_1}+\sqrt{G_2}+...+\sqrt{G_m}}").next_to(quar_eq2, UP).scale(0.7)
        self.play(Write(fakesolvedeq_text))
        fn_text = MathTex(r"F_n=F_0'+\sqrt{F_1'}+\sqrt{F_2'}+...+\sqrt{F_m'}").next_to(fakesolvedeq_text, UP).scale(0.7)
        fn_text.color = ManimColor(WHITE, 0.5)
        self.play(Write(fn_text))
        self.wait(3)
        self.play(FadeOut(fn_text))
        # 先证明F0和G0可以被假掉,方法和三次方程一模一样,因为F0和G0都只有单层根号。
        note_text = Text("*接下来的动画更离谱, 暂时隐藏系数c0、c1、c2和c3。").move_to(RIGHT * 4 + DOWN * 3).scale(0.3)
        self.play(Write(note_text), *[FadeOut(coeff_dot) for coeff_dot in coeff_dots])
        f0_dot = create_point(0.7 - 0.6j, MathTex("F_0"), color=GREEN)
        sqrtf0_dots = VGroup(create_point(pos, MathTex(r"\sqrt{F_0}"), color=BLUE) for pos in get_roots(plane.p2n(f0_dot.dot.get_center()), 3))
        self.play(Create(f0_dot), Create(sqrtf0_dots))
        def update_sqrtf0(mob):
            new_roots = get_roots(plane.p2n(f0_dot.dot.get_center()), 3)
            for vg, pos in zip(mob, new_roots):
                vg.dot.move_to(plane.n2p(pos))
        sqrtf0_dots.add_updater(update_sqrtf0)
        text1 = Text("操作1: 1 ↔ 2").move_to(UP * 2 + LEFT * 4).scale(0.7)
        self.play(permute_anims(root_dots, [[0, 1]]), rotate_dot_anim(f0_dot.dot, 1), Write(text1))
        text2 = Text("操作2: 1 ↔ 3").next_to(text1, DOWN).scale(0.7)
        self.play(permute_anims(root_dots, [[0, 2]]), rotate_dot_anim(f0_dot.dot, 2), Write(text2))
        text3 = Text("操作3: 2 ↔ 1").next_to(text2, DOWN).scale(0.7)
        self.play(permute_anims(root_dots, [[1, 0]]), rotate_dot_anim(f0_dot.dot, -1), Write(text3))
        text4 = Text("操作4: 3 ↔ 1").next_to(text3, DOWN).scale(0.7)
        self.play(permute_anims(root_dots, [[2, 0]]), rotate_dot_anim(f0_dot.dot, -2), Write(text4))
        self.wait(1)
        self.play(FadeOut(text1), FadeOut(text2), FadeOut(text3), FadeOut(text4), FadeOut(note_text), FadeOut(sqrtf0_dots), FadeOut(f0_dot))
        # 现在F0和G0都被处理掉了。然后就是重头戏,我们来处理根号F1这一项。和之前一样,我们要找到一个操作,它交换了真正的根,但根号F1的值不会变化。我们展开根号F1这一项。目前场上的形势是,F撇是不含根号的有理函数,F1是单层根号公式,整个公式是双层嵌套根式。现在,用刚才正正反反的方法,我们能证明的是构成F1的各项的值不会变化,即F1本身不会变化,但根下F1的值会变化。问题出在我们虽然保证了F1的值不变,但由于F1是由几个根式组合而成的,所以它的运动路径会比较复杂,可能绕原点运动了几圈才会回去,而这会影响根下F1的值。举个例子,假设这些是F1的值。我们关注其中一个F1的辐角变化。现在,交换x1和x2,F1可能会这么运动
        self.wait(10)
        expanded_f1_text = MathTex(r"F_1=F_0'+\sqrt{F_1'}+\sqrt{F_2'}+...+\sqrt{F_n'}").next_to(quar_eq2, UP).scale(0.7)
        self.play(FadeOut(quar_eq2), fakesolvedeq_text.animate.move_to(DOWN * 3 + LEFT * 4), Write(expanded_f1_text))
        self.wait(18)
        f1_dots = [create_point(pos, MathTex("F_1"), color=GREEN) for pos in [1.5 + 0.1j, 2.5 + 0.2j]]
        for vg in f1_dots:
            vg.dot.arg = np.arctan2(vg.dot.get_center()[1], vg.dot.get_center()[0])
        self.play(*[Create(dot) for dot in f1_dots])
        note_text = Text("*由于F1中含有根号, 所以会有多个值。").move_to(RIGHT * 4 + DOWN * 3).scale(0.3)
        self.play(Write(note_text))
        
        self.wait(1)
        self.play(f1_dots[1].animate.set_color(WHITE), f1_dots[1].animate.set_opacity(0.2))
        f1_dots[1].set_color(WHITE)
        f1_dots[1].set_opacity(0.2)
        arg_displayers = [Sector(radius=2, angle=f1_dots[0].dot.arg, color=GREEN, fill_opacity=0.5), *[Sector() for _ in range(5)]]
        arg_displayers[0].set_fill(GREEN, 0.5)
        for i in range(len(arg_displayers)):
            arg_displayers[i].index = i
        [displayer.set_opacity(0) for displayer in arg_displayers[1:]]
        def update_arg(mob):
            targetangle = (np.arctan2(mob.get_center()[1], mob.get_center()[0]) + 2 * PI) % (2 * PI)
            candidates = [targetangle + 2 * k * PI for k in range(-10, 10)]
            newarg = candidates[np.argmin([abs(angle - mob.arg) for angle in candidates])]
            mob.arg = newarg
        def update_arg_displayer0(mob):
            arg = f1_dots[0].dot.arg
            if(arg // (2 * PI) > mob.index + 1):
                mob.set_opacity(0)
            elif arg // (2 * PI) == mob.index + 1:
                mob.become(Circle(radius=2, color=GREEN, fill_opacity=0.5*(1 - arg % (2 * PI) / (2 * PI))))
            elif arg // (2 * PI) == mob.index:
                mob.become(Sector(radius=2, angle=(arg + 2000 * PI) % (2 * PI), color=GREEN, fill_opacity=0.5))
            else:
                mob.set_opacity(0)
        def update_arg_displayer1(mob):
            arg = f1_dots[1].dot.arg
            if(arg // (2 * PI) > mob.index + 1):
                mob.set_opacity(0)
            elif arg // (2 * PI) == mob.index + 1:
                mob.become(Circle(radius=2, color=GREEN, fill_opacity=0.5*(1 - arg % (2 * PI) / (2 * PI))))
                mob.stroke_opacity = 0
            elif arg // (2 * PI) == mob.index:
                mob.become(Sector(radius=2, angle=(arg + 2000 * PI) % (2 * PI), color=GREEN, fill_opacity=0.5))
            else:
                mob.set_opacity(0)
        f1_dots[0].dot.add_updater(update_arg)
        f1_dots[1].dot.add_updater(update_arg)
        [displayer.add_updater(update_arg_displayer0) for displayer in arg_displayers]
        self.play(*[Create(arg_displayer) for arg_displayer in arg_displayers])
        text1 = Text("操作1: 1 ↔ 2").move_to(UP * 2 + LEFT * 4).scale(0.7)
        self.play(Write(text1))
        self.play(permute_anims(root_dots, [[0, 1]]), rotate_dot_anim(f1_dots[0], 1, 0.7), rotate_dot_anim(f1_dots[1], -1, 1.3), run_time=3)
        text2 = Text("操作2: 1 ↔ 3").next_to(text1, DOWN).scale(0.7)
        self.play(Write(text2))
        [displayer.clear_updaters() for displayer in arg_displayers]
        [displayer.add_updater(update_arg_displayer1) for displayer in arg_displayers]
        self.play(permute_anims(root_dots, [[0, 2]]), permute_anims(f1_dots, [[0, 1]]), run_time = 1)
        text3 = Text("操作3: 2 ↔ 1").next_to(text2, DOWN).scale(0.7)
        self.play(Write(text3))
        self.play(permute_anims(root_dots, [[1, 0]]), rotate_dot_anim(f1_dots[0], -1, 0.7), rotate_dot_anim(f1_dots[1], 1, 1.3), run_time=3)
        text4 = Text("操作4: 3 ↔ 1").next_to(text3, DOWN).scale(0.7)
        self.play(Write(text4))
        [displayer.clear_updaters() for displayer in arg_displayers]
        [displayer.add_updater(update_arg_displayer0) for displayer in arg_displayers]
        self.play(permute_anims(root_dots, [[2, 0]]), permute_anims(f1_dots, [[1, 0]]), run_time = 1)
        self.play(FadeOut(text1), FadeOut(text2), FadeOut(text3), FadeOut(text4), FadeOut(note_text), *[FadeOut(displayer) for displayer in arg_displayers], *[FadeOut(dot) for dot in f1_dots])
        [displayer.clear_updaters() for displayer in arg_displayers]
        # 可以看到,F1绕原点运行了2圈,辐角增加了4派,这会使根下F1的值变化。
        # 那么有什么办法可以让F1保持辐角不变呢?我们再回头看看刚才处理三次方程的方法。在证伪单层根号的三次方程求根公式时,交换两个根会使F1绕原点旋转,但我们通过正正反反的方法抵消了它的旋转。而现在,由于F1含有了根号,正正反反的操作只会使F1绕原点旋转,不会抵消。我们对比一下,三次方程时,交换操作会使F1绕原点旋转,而正正反反的操作会使F1的旋转被抵消。四次方程的情况中,正正反反的操作会使F1绕原点旋转,那什么操作可以抵消这个旋转呢?没错,我们只需要把正正反反的操作正正反反地做就可以了!
        hint = Text("视频后期处理: \n回放三次方程的操作", font_size=80)
        self.play(Write(hint))
        self.play(FadeOut(hint))
        self.play(*[FadeOut(mob) for mob in self.mobjects])
        title_text1 = Text("三次方程").move_to(UP * 3 + LEFT * 4)
        title_text2 = Text("四次方程").move_to(UP * 3 + RIGHT * 4)
        self.play(Write(title_text1), Write(title_text2))
        text1 = Text("1 ↔ 2").move_to(LEFT * 5.5).scale(0.7)
        self.play(Write(text1))
        text2 = Text(" → F1绕原点旋转").next_to(text1, RIGHT).scale(0.7)
        self.play(Write(text2))
        text3 = Text("正正反反").move_to(DOWN * 3 + LEFT * 6).scale(0.7)
        self.play(Write(text3))
        text4 = Text(" → F1的旋转被抵消").next_to(text3, RIGHT).scale(0.7)
        self.play(Write(text4))
        text5 = Text("正正反反").move_to(RIGHT * 2).scale(0.7)
        self.play(Write(text5))
        text6 = Text(" → F1绕原点旋转").next_to(text5, RIGHT).scale(0.7)
        self.play(Write(text6))
        text7 = Text("?").move_to(DOWN * 3 + RIGHT * 2).scale(0.7)
        text7.set_color(RED)
        self.play(Write(text7))
        text8 = Text(" → F1的旋转被抵消").next_to(text7, RIGHT).scale(0.7)
        self.play(Write(text8))
        self.wait(3)
        self.play(FadeOut(title_text1), FadeOut(title_text2), FadeOut(text1), FadeOut(text2), FadeOut(text3), FadeOut(text4), FadeOut(text5), FadeOut(text6), FadeOut(text7), FadeOut(text8))
        # 这么说比较抽象,举个例子。比如说,我们正正反反的操作是这个。我们再找一个正正反反的操作,比如这个。咱也甭管操作具体是什么,我们只需要知道,正正反反的操作分别会使F1绕原点旋转l1和l2圈。
        self.play(FadeIn(plane), *[FadeIn(root_dot) for root_dot in root_dots])
        text1 = Text("正正反反(1): 1 ↔ 2, 1 ↔ 3, 2 ↔ 1, 3 ↔ 1").move_to(UP * 2 + LEFT * 4).scale(0.5)
        self.play(Write(text1))
        self.wait(2)
        text2 = Text("正正反反(2): 1 ↔ 4, 1 ↔ 2, 4 ↔ 1, 2 ↔ 1").next_to(text1, DOWN).scale(0.5)
        self.play(Write(text2))
        self.wait(6)

        self.play(Transform(text1, Text("正正反反(1): 🔄l1圈").move_to(UP * 2 + LEFT * 4).scale(0.7)))
        self.play(Transform(text2, Text("正正反反(2): 🔄l2圈").next_to(text1, DOWN).scale(0.7)))
        # 接下来,我们先做正正反反1,这会使F1绕原点旋转l1圈。然后,我们再做正正反反2,这会使F1绕原点旋转l2圈。现在F1一共旋转了l1+l2圈。接下来,我们做正正反反1的逆操作,让F1往回转l1圈。最后,我们再做正正反反2的逆操作,让F1往回转l2圈。这样一来,F1的旋转成功被抵消了,说明根下F1也不会变化!而且我们发现,四个根的顺序发生了变化。这符合我们的要求。
        f1_dots[0].dot.arg = np.arctan2(f1_dots[0].dot.get_center()[1], f1_dots[0].dot.get_center()[0])
        [displayer.add_updater(update_arg_displayer0) for displayer in arg_displayers]
        self.play(FadeIn(f1_dots[0]), *[FadeIn(displayer) for displayer in arg_displayers])

        text3 = Text("操作1: 正正反反(1)").move_to(DOWN * 3 + LEFT * 4).scale(0.7)
        self.play(*[root_dots[i].dot.animate.set_color(RED) for i in [0, 1, 2]], FadeIn(text3))
        self.play(permute_anims(root_dots, [[0, 1]]))
        self.play(permute_anims(root_dots, [[0, 2]]))
        self.play(permute_anims(root_dots, [[1, 0]]))
        self.play(permute_anims(root_dots, [[2, 0]]))
        self.play(rotate_dot_anim(f1_dots[0], 2))
        self.play(*[root_dots[i].dot.animate.set_color(YELLOW) for i in [0, 1, 2]])
        self.wait(1)

        text4 = Text("操作2: 正正反反(2)").next_to(text3, UP).scale(0.7)
        self.play(*[root_dots[i].dot.animate.set_color(RED) for i in [0, 1, 3]], FadeIn(text4))
        self.play(permute_anims(root_dots, [[0, 3]]))
        self.play(permute_anims(root_dots, [[0, 1]]))
        self.play(permute_anims(root_dots, [[3, 1]]))
        self.play(permute_anims(root_dots, [[1, 0]]))
        self.play(rotate_dot_anim(f1_dots[0], 1))
        self.play(*[root_dots[i].dot.animate.set_color(YELLOW) for i in [0, 1, 3]])
        self.wait(1)

        text5 = Text("操作3: 正正反反(1)逆").next_to(text4, UP).scale(0.7)
        self.play(*[root_dots[i].dot.animate.set_color(RED) for i in [0, 1, 2]], FadeIn(text5))
        self.play(permute_anims(root_dots, [[0, 2]]))
        self.play(permute_anims(root_dots, [[0, 1]]))
        self.play(permute_anims(root_dots, [[2, 0]]))
        self.play(permute_anims(root_dots, [[1, 0]]))
        self.play(rotate_dot_anim(f1_dots[0], -2))
        self.play(*[root_dots[i].dot.animate.set_color(YELLOW) for i in [0, 1, 2]])
        self.wait(1)

        text6 = Text("操作3: 正正反反(2)逆").next_to(text5, UP).scale(0.7)
        self.play(*[root_dots[i].dot.animate.set_color(RED) for i in [0, 1, 3]], FadeIn(text6))
        self.play(permute_anims(root_dots, [[0, 1]]))
        self.play(permute_anims(root_dots, [[1, 3]]))
        self.play(permute_anims(root_dots, [[1, 0]]))
        self.play(permute_anims(root_dots, [[3, 0]]))
        self.play(rotate_dot_anim(f1_dots[0], -1))
        self.play(*[root_dots[i].dot.animate.set_color(YELLOW) for i in [0, 1, 3]])
        self.wait(1)

        self.play(permute_anims(root_dots, [[2, 3, 0, 1]]))
        self.play(permute_anims(root_dots, [[1, 0, 3, 2]]))

        # 回到最开始我们假设的求根公式,和之前一样,其中的每一项在这么操作后都会回到原位,于是整个公式的值都不会变化。但根的顺序变了,说明这不可能是求根公式。我们成功证明了四次方程的求根公式必须嵌套至少三层根号。
        self.play(FadeOut(text1), FadeOut(text2), FadeOut(text3), FadeOut(text4), FadeOut(text5), FadeOut(text6), FadeIn(fakesolvedeq_text), FadeIn(expanded_f1_text))
        self.wait(13)
        for mob in self.mobjects:
            mob.clear_updaters()
        self.play(*[FadeOut(mob) for mob in self.mobjects])



        text = Text("Chapter 4: 五次方程", font_size=80)
        self.play(FadeIn(text))
        self.wait(2)
        self.play(FadeOut(text))
        # 现在,我们来到了五次方程。一切变得清晰起来了。我们的手段,就是通过找到一个操作,它交换了真正的根,但假设的求根公式的值不会变化。通过重复嵌套正正反反的组合操作,我们可以证明嵌套根号的层数存在下限。我们已经证明了,二次方程的求根公式必须嵌套至少一层根号,三次方程的求根公式必须嵌套至少两层根号,四次方程的求根公式必须嵌套至少三层根号。那么五次方程呢?也许你会觉得这样只能五次方程的求根公式必须嵌套至少四层根号。
        self.wait(17)
        text1 = Text("二次方程 → 至少1层根号").move_to(UP * 3)
        self.play(Write(text1))
        text2 = Text("三次方程 → 至少2层根号").move_to(UP * 1)
        self.play(Write(text2))
        text3 = Text("四次方程 → 至少3层根号").move_to(DOWN * 1)
        self.play(Write(text3))
        self.wait(3)
        text4 = Text("五次方程 → 至少4层根号?").move_to(DOWN * 3)
        self.play(Write(text4))
        self.play(*[FadeOut(mob) for mob in [text1, text2, text3, text4]])
        # 事实并非如此,五次方程没有求根公式。我们来考察一下五次方程的正正反反操作。这是一个嵌套了两层的正正反反操作,它的效果是这样的。
        root_dots = [create_point(pos, MathTex(f"x_{i+1}")) for i, pos in enumerate([-0.12 + 1.03j, -0.53 + 0.56j, -0.28 - 0.47j, 0.19 + 0.1j, 0.5 + 0.5j])]
        self.play(FadeIn(plane), *[Create(root) for root in root_dots])
        text1 = Text("(1): 1 ↔ 2, 1 ↔ 3, 2 ↔ 1, 3 ↔ 1").move_to(UP * 2 + LEFT * 4).scale(0.5)
        text2 = Text("(2): 3 ↔ 4, 3 ↔ 5, 4 ↔ 3, 5 ↔ 3").next_to(text1, DOWN).scale(0.5)
        self.play(Write(text1), Write(text2))
        text3 = Text("操作: (1) → (2) → (1)逆 → (2)逆").next_to(text2, DOWN).scale(0.5)
        self.play(Write(text3))
        self.wait(5)
        self.play(permute_anims(root_dots, [[0, 1]]))
        self.play(permute_anims(root_dots, [[0, 2]]))
        self.play(permute_anims(root_dots, [[1, 0]]))
        self.play(permute_anims(root_dots, [[2, 0]]))

        self.play(permute_anims(root_dots, [[2, 3]]))
        self.play(permute_anims(root_dots, [[2, 4]]))
        self.play(permute_anims(root_dots, [[3, 2]]))
        self.play(permute_anims(root_dots, [[4, 2]]))

        self.play(permute_anims(root_dots, [[0, 2]]))
        self.play(permute_anims(root_dots, [[0, 1]]))
        self.play(permute_anims(root_dots, [[2, 0]]))
        self.play(permute_anims(root_dots, [[1, 0]]))

        self.play(permute_anims(root_dots, [[2, 4]]))
        self.play(permute_anims(root_dots, [[2, 3]]))
        self.play(permute_anims(root_dots, [[4, 2]]))
        self.play(permute_anims(root_dots, [[3, 2]]))
        # 如果你的注意力足够集中,你会发现,这个操作实际上等效于这个操作。我们把正正反反这种操作更简洁的记号表示出来。注意到这个操作实际上可以被表示为双层正正反反嵌套。现在,这两个操作是等价的,我们得到了一个等式。我们可以把等式中的1全部替换成a,2全部替换成b,等等。这时候如果我们从右往左观察这个式子,就能看出端倪:它允许我们把一个正正反反的操作展开成双层嵌套正正反反操作。更重要的是,这个过程可以无限进行下去,从而把一层的正正反反变成任意层数的正正反反操作。
        self.wait(4)
        text4 = Text("等效: 1 ↔ 4, 1 ↔ 3, 4 ↔ 1, 3 ↔ 1").next_to(text3, DOWN).scale(0.5)
        self.play(Write(text4))
        self.wait(1)
        self.play(*[FadeOut(root_dot) for root_dot in root_dots], FadeOut(plane))
        text5 = Text("[a, b] = a → b → a逆 → b逆").move_to(UP * 3)
        self.play(Write(text5), FadeOut(text1), FadeOut(text2), text3.animate.move_to(ORIGIN), text4.animate.move_to(DOWN * 1.5))
        self.play(Transform(text3, Text("操作: [1 ↔ 2, 1 ↔ 3] → [3 ↔ 4, 3 ↔ 5] → [1 ↔ 2, 1 ↔ 3]逆 → [3 ↔ 4, 3 ↔ 5]逆").scale(0.7)), Transform(text4, Text("等效: [1 ↔ 4, 1 ↔ 3]").move_to(text4.get_center()).scale(0.5)))
        self.wait(2)
        self.play(Transform(text3, Text("操作: [[1 ↔ 2, 1 ↔ 3], [3 ↔ 4, 3 ↔ 5]]")))
        self.wait(1)
        text1 = Text("[[1 ↔ 2, 1 ↔ 3], [3 ↔ 4, 3 ↔ 5]] = [1 ↔ 4, 1 ↔ 3]")
        self.play(FadeOut(text3), FadeOut(text4), FadeIn(text1))
        self.wait(5)
        self.play(Transform(text1, Text("[[a ↔ b, a ↔ c], [c ↔ d, c ↔ e]] = [a ↔ d, a ↔ c]")))
        self.wait(10)
        self.play(Transform(text1, Text("[[[a ↔ d, a ↔ c], [c ↔ b, c ↔ e]], [[c ↔ b, c ↔ a], [e ↔ d, e ↔ a]]] = [a ↔ d, a ↔ c]").scale(0.5)))
        self.wait(1)
        self.play(Transform(text1, Text("[[[[a ↔ b, a ↔ c], [c ↔ d, c ↔ e]], [[a ↔ b, a ↔ c], [c ↔ d, c ↔ e]]], [[[a ↔ b, a ↔ c], [c ↔ d, c ↔ e]], [[a ↔ b, a ↔ c], [c ↔ d, c ↔ e]]]] = [a ↔ d, a ↔ c]").scale(0.3)))
        # 现在我们回到五次方程。假设我们现在找到了一个嵌套10层根号的求根公式。于是,F九个撇就是有理函数。我们从里往外破解它,第一层是有理函数,交换1和2就能破解。第二层是单层根号的函数,一个正正反反就能破解。接下来,我们用刚才的等式把操作展开成双层嵌套的正正反反操作,然后来处理第三层,也就是双层根号函数。不管你上一层的辐角怎么变,你最终都会被双层正正反反嵌套抵消掉,所以这一层的值也不会变。然后再展开,再处理第四层,依次类推。直到第十层,所有的根号都被抵消掉了。
        self.play(FadeOut(text1), FadeOut(text5))
        root_dots = [create_point(pos, MathTex(f"x_{i+1}")) for i, pos in enumerate([-0.12 + 1.03j, -0.53 + 0.56j, -0.28 - 0.47j, 0.19 + 0.1j, 0.5 + 0.5j])]
        self.play(FadeIn(plane), *[FadeIn(root_dot) for root_dot in root_dots])
        fakesolvedeq_text = MathTex(r"x=\frac{F_0+\sqrt{F_1}+\sqrt{F_2}+...+\sqrt{F_n}}{G_0+\sqrt{G_1}+\sqrt{G_2}+...+\sqrt{G_m}}").move_to(DOWN * 3 + LEFT * 4).scale(0.7)
        fakesolvedeq_text1 = MathTex(r"F=F_0'+\sqrt{F_1'}+\sqrt{F_2'}+...+\sqrt{F_n'}").next_to(fakesolvedeq_text, UP).scale(0.7)
        fakesolvedeq_text2 = MathTex(r"F'=F_0''+\sqrt{F_1''}+\sqrt{F_2''}+...+\sqrt{F_n''}").next_to(fakesolvedeq_text1, UP).scale(0.7)
        fakesolvedeq_text3 = Text("...").next_to(fakesolvedeq_text2, UP).scale(0.7)
        self.play(Write(fakesolvedeq_text), Write(fakesolvedeq_text1), Write(fakesolvedeq_text2), Write(fakesolvedeq_text3))
        f10_dot = create_point(0.7 - 0.6j, MathTex("F^{(9)}"), color=GREEN)
        self.play(Create(f10_dot))
        self.wait(3)
        text = Text("操作1: 1 ↔ 2").move_to(UP * 3 + LEFT * 4).scale(0.7)
        self.play(Write(text))
        self.play(permute_anims(root_dots, [[0, 1]]), rotate_dot_anim(f10_dot.dot, 1, 0.7 - 0.9j), run_time=3)
        self.wait(1)
        self.play(Transform(text, Text("操作2: [1 ↔ 2, 1 ↔ 3]").move_to(text.get_center()).scale(0.7)), FadeOut(f10_dot))
        f9_dot = create_point(0.7 + 0.6j, MathTex("F^{(8)}"), color=GREEN)
        self.play(Create(f9_dot))
        self.play(permute_anims(root_dots, [[0, 1]]), rotate_dot_anim(f9_dot.dot, 1))
        self.play(permute_anims(root_dots, [[0, 2]]), rotate_dot_anim(f9_dot.dot, 2, 1 + 1j))
        self.play(permute_anims(root_dots, [[1, 0]]), rotate_dot_anim(f9_dot.dot, -1))
        self.play(permute_anims(root_dots, [[2, 0]]), rotate_dot_anim(f9_dot.dot, -2, 1 + 1j))
        self.play(FadeOut(f9_dot))
        self.wait(2)
        text1 = Text("=[[1 ↔ 4, 1 ↔ 3], [3 ↔ 2, 3 ↔ 5]]").move_to(UP * 3 + RIGHT).scale(0.7)
        self.play(Write(text1))
        f8_dot = create_point(1.1 + 0.6j, MathTex("F^{(7)}"), color=GREEN)
        self.play(Create(f8_dot))
        self.wait(9)
        self.play(FadeOut(f8_dot))
        text2 = Text("=...=...").next_to(text1, RIGHT).scale(0.7)
        self.play(Write(text2))
        self.wait(5)
        # 无论我们把上面的10层换成100层还是10000层,它最终都会被我们的无限嵌套正正反反操作抵消掉。于是,F的值不会变化。但是,这些正正反反最终都等价于这个操作2,根的顺序发生了变化。我们期待已久的最终矛盾终于显现。
        self.wait(15)
        # 终于,我们证明了,五次方程是没有根式解的。
        for mob in self.mobjects:
            mob.clear_updaters()
        self.play(*[FadeOut(mob) for mob in self.mobjects])
        theorem_title = Text("Abel-Ruffini定理", font_size=80).move_to(UP * 0.5)
        theorem_text = Text("五次及更高次的一般多项式方程没有根式解", font_size=50).next_to(theorem_title, DOWN)
        self.play(Write(theorem_title))
        self.play(Write(theorem_text))
        self.wait(3)
        self.play(FadeOut(theorem_title), FadeOut(theorem_text))
        

        # self.next_section(skip_animations=True)
        # text = Text("Chapter 5: 最后", font_size=80)
        # self.play(FadeIn(text))
        # self.wait(2)
        # self.play(FadeOut(text))
        # # 为什么正正好好是五次方程呢?我们回顾整个证明,其实都是在做一件事,那就是找一个操作,它交换了真正的根,但假设的求根公式的值不会变化。通过正正反反的操作,我们可以抵消辐角的变化,从而将求根公式的根号一层层抽离开。而如果我们想要证明方程无根式解,就要保证无论求根公式有多少层根号,我们都能找到对应层数的正正反反嵌套操作,同时保证这个操作能交换真正的根。也就是说,正正反反嵌套必须达到无穷层。然而,这样的操作只有当我们能交换的元素个数达到5个或以上时才存在。
        # self.play(Write(hint))
        # self.wait(2)
        # self.play(FadeOut(hint))
        # self.wait(29)
        # # 比如四次方程。我们把4个元素所有可能的交换操作都列出来,共有6种。这6种操作中,任意一种都可以证伪仅含四则运算的求根公式。然后我们任选其中两种,组成一个正正反反的操作。这样的操作共有种。熟悉群论的朋友可能已经发现,正正反反的操作实际上就是交换子。
        # ops1 = VGroup(*[Text("1 ↔ 2"), Text("1 ↔ 3"), Text("1 ↔ 4"), Text("2 ↔ 3"), Text("2 ↔ 4"), Text("3 ↔ 4")])
        # for i, op in enumerate(ops1):
        #     op.move_to(UP * (3 - i * 0.5) + LEFT * 4).scale(0.7)
        # self.play(Create(ops1))
        # ops2 = Text("[1 ↔ 3, 1 ↔ 4]")

command line:

manim file.py --fps=30

Wrong display or Error traceback:

╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ C:\Users\Username\AppData\Roaming\Python\Python310\site-packages\manim\cli\render\commands.py:125   │
│ in render                                                                                        │
│                                                                                                  │
│   122 │   │   │   try:                                                                           │
│   123 │   │   │   │   with tempconfig({}):                                                       │
│   124 │   │   │   │   │   scene = SceneClass()                                                   │
│ ❱ 125 │   │   │   │   │   scene.render()                                                         │
│   126 │   │   │   except Exception:                                                              │
│   127 │   │   │   │   error_console.print_exception()                                            │
│   128 │   │   │   │   sys.exit(1)                                                                │
│                                                                                                  │
│ C:\Users\Username\AppData\Roaming\Python\Python310\site-packages\manim\scene\scene.py:247 in render │
│                                                                                                  │
│    244 │   │   │   return True                                                                   │
│    245 │   │   self.tear_down()                                                                  │
│    246 │   │   # We have to reset these settings in case of multiple renders.                    │
│ ❱  247 │   │   self.renderer.scene_finished(self)                                                │
│    248 │   │                                                                                     │
│    249 │   │   # Show info only if animations are rendered or to get image                       │
│    250 │   │   if (                                                                              │
│                                                                                                  │
│ C:\Users\Username\AppData\Roaming\Python\Python310\site-packages\manim\renderer\cairo_renderer.py:2 │
│ 69 in scene_finished                                                                             │
│                                                                                                  │
│   266 │   def scene_finished(self, scene):                                                       │
│   267 │   │   # If no animations in scene, render an image instead                               │
│   268 │   │   if self.num_plays:                                                                 │
│ ❱ 269 │   │   │   self.file_writer.finish()                                                      │
│   270 │   │   elif config.write_to_movie:                                                        │
│   271 │   │   │   config.save_last_frame = True                                                  │
│   272 │   │   │   config.write_to_movie = False                                                  │
│                                                                                                  │
│ C:\Users\Username\AppData\Roaming\Python\Python310\site-packages\manim\scene\scene_file_writer.py:5 │
│ 14 in finish                                                                                     │
│                                                                                                  │
│   511 │   │   frame in the default image directory.                                              │
│   512 │   │   """                                                                                │
│   513 │   │   if write_to_movie():                                                               │
│ ❱ 514 │   │   │   self.combine_to_movie()                                                        │
│   515 │   │   │   if config.save_sections:                                                       │
│   516 │   │   │   │   self.combine_to_section_videos()                                           │
│   517 │   │   │   if config["flush_cache"]:                                                      │
│                                                                                                  │
│ C:\Users\Username\AppData\Roaming\Python\Python310\site-packages\manim\scene\scene_file_writer.py:7 │
│ 40 in combine_to_movie                                                                           │
│                                                                                                  │
│   737 │   │   │   return                                                                         │
│   738 │   │                                                                                      │
│   739 │   │   logger.info("Combining to Movie file.")                                            │
│ ❱ 740 │   │   self.combine_files(                                                                │
│   741 │   │   │   partial_movie_files,                                                           │
│   742 │   │   │   movie_file_path,                                                               │
│   743 │   │   │   is_gif_format(),                                                               │
│                                                                                                  │
│ C:\Users\Username\AppData\Roaming\Python\Python310\site-packages\manim\scene\scene_file_writer.py:7 │
│ 14 in combine_files                                                                              │
│                                                                                                  │
│   711 │   │   │   │                                                                              │
│   712 │   │   │   │   # We need to assign the packet to the new stream.                          │
│   713 │   │   │   │   packet.stream = output_stream                                              │
│ ❱ 714 │   │   │   │   output_container.mux(packet)                                               │
│   715 │   │                                                                                      │
│   716 │   │   partial_movies_input.close()                                                       │
│   717 │   │   output_container.close()                                                           │
│                                                                                                  │
│ in av.container.output.OutputContainer.mux:257                                                   │
│                                                                                                  │
│ in av.container.output.OutputContainer.mux_one:278                                               │
│                                                                                                  │
│ in av.container.core.Container.err_check:286                                                     │
│                                                                                                  │
│ in av.error.err_check:326                                                                        │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: [Errno 22] Invalid argument: 'C:\\Users\\Username\\Documents\\Tiger\\media\\videos\\quintic unsolvability\\1080p30\\QuinticUnsolvability.mp4'

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions