多元微积分可视化

路径无关的陷阱

用绕原点旋转的向量场说明:偏导相等并不自动推出路径无关,单连通条件不能省略。

打开原视频

path_independence_counterexample.py
1from manim import *2import numpy as np3 4config.tex_template = TexTemplateLibrary.ctex5config.tex_template.add_to_preamble(r"\setCJKmainfont{STSong}")6 7 8class PathIndependenceCounterexample(Scene):9    def construct(self):10        # ========== 场景1:抛出"定理"(设疑)==========11        title = Text("满足条件却失败?——路径无关的陷阱", font="SimSun", color=YELLOW).scale(0.65)12        title.to_edge(UP, buff=0.3)13        self.play(Write(title), run_time=1.5)14        self.wait(0.5)15 16        # 展示"定理"(故意省略单连通条件)17        theorem_text = Text("定理:", font="SimSun", color=WHITE).scale(0.5)18        theorem_text.next_to(title, DOWN, buff=0.4).to_edge(LEFT, buff=1.0)19 20        theorem_content = MathTex(21            r"\text{若}\ \frac{\partial Q}{\partial x} = \frac{\partial P}{\partial y}",22            r"\text{,则曲线积分与路径无关}",23            color=WHITE,24        ).scale(0.55)25        theorem_content.next_to(theorem_text, RIGHT, buff=0.1)26 27        theorem_group = VGroup(theorem_text, theorem_content)28        theorem_box = SurroundingRectangle(theorem_group, color=WHITE, buff=0.15, corner_radius=0.08)29 30        self.play(Write(theorem_text), Write(theorem_content), run_time=2)31        self.play(Create(theorem_box), run_time=0.8)32        self.wait(1)33 34        # 引出向量场35        intro = Text("考察向量场:", font="SimSun", color=WHITE).scale(0.4)36        intro.next_to(theorem_box, DOWN, buff=0.4).to_edge(LEFT, buff=1.0)37        field_formula = MathTex(38            r"\vec{F} = \left(\frac{-y}{x^2+y^2},\ \frac{x}{x^2+y^2}\right)",39            color=BLUE,40        ).scale(0.55)41        field_formula.next_to(intro, DOWN, buff=0.15).align_to(intro, LEFT)42 43        self.play(Write(intro), run_time=0.8)44        self.play(Write(field_formula), run_time=1.5)45        self.wait(1)46 47        # 画向量场48        def vec_field_func(pos):49            x, y = pos[0], pos[1]50            r2 = x**2 + y**251            if r2 < 0.15:52                return np.array([0, 0, 0])53            return np.array([-y / r2, x / r2, 0])54 55        vector_field = ArrowVectorField(56            vec_field_func,57            x_range=[-3.5, 3.5, 0.7],58            y_range=[-2.8, 2.8, 0.7],59            length_func=lambda norm: 0.4 * sigmoid(norm),60            color_scheme=None,61        )62        vector_field.set_color(BLUE_C).set_opacity(0.6)63 64        self.play(65            FadeOut(intro),66            theorem_group.animate.scale(0.8).to_edge(UP, buff=0.35).to_edge(LEFT, buff=0.3),67            theorem_box.animate.scale(0.8).to_edge(UP, buff=0.35).to_edge(LEFT, buff=0.3),68            field_formula.animate.scale(0.7).next_to(title, DOWN, buff=1.6).to_edge(LEFT, buff=0.3),69            FadeOut(title),70            run_time=1.2,71        )72        # 重新包围框73        new_theorem_box = SurroundingRectangle(theorem_group, color=WHITE, buff=0.12, corner_radius=0.08)74        self.play(Transform(theorem_box, new_theorem_box), run_time=0.3)75 76        self.play(Create(vector_field), run_time=2.5)77        self.wait(0.5)78 79        # ========== 场景2:验证条件 —— "完美符合!" ==========80        step2_label = Text("验证条件:", font="SimSun", color=WHITE).scale(0.4)81        step2_label.to_edge(RIGHT, buff=1.5).shift(UP * 2.5)82 83        pq_def = MathTex(84            r"P = \frac{-y}{x^2+y^2},\ Q = \frac{x}{x^2+y^2}",85            color=WHITE,86        ).scale(0.42)87        pq_def.next_to(step2_label, DOWN, buff=0.12).align_to(step2_label, LEFT)88 89        dq_dx = MathTex(90            r"\frac{\partial Q}{\partial x} = \frac{y^2 - x^2}{(x^2+y^2)^2}",91            color=TEAL,92        ).scale(0.4)93        dq_dx.next_to(pq_def, DOWN, buff=0.12).align_to(step2_label, LEFT)94 95        dp_dy = MathTex(96            r"\frac{\partial P}{\partial y} = \frac{y^2 - x^2}{(x^2+y^2)^2}",97            color=TEAL,98        ).scale(0.4)99        dp_dy.next_to(dq_dx, DOWN, buff=0.1).align_to(step2_label, LEFT)100 101        # 相等结论102        check = MathTex(103            r"\frac{\partial Q}{\partial x} = \frac{\partial P}{\partial y}\ \checkmark",104            color=GREEN,105        ).scale(0.5)106        check.next_to(dp_dy, DOWN, buff=0.15).align_to(step2_label, LEFT)107 108        self.play(Write(step2_label), run_time=0.6)109        self.play(Write(pq_def), run_time=1)110        self.play(Write(dq_dx), run_time=1)111        self.play(Write(dp_dy), run_time=1)112        self.play(Write(check), run_time=1.2)113        self.wait(0.5)114 115        # "自信结论"116        confident = Text('结论:路径无关!', font="SimSun", color=GREEN).scale(0.45)117        confident.next_to(check, DOWN, buff=0.2).align_to(step2_label, LEFT)118        confident_box = SurroundingRectangle(confident, color=GREEN, buff=0.08, corner_radius=0.05)119        self.play(Write(confident), Create(confident_box), run_time=1.2)120        self.wait(1.5)121 122        # 清理验证部分123        verify_objs = VGroup(step2_label, pq_def, dq_dx, dp_dy, check, confident, confident_box)124        self.play(FadeOut(verify_objs), run_time=0.8)125 126        # ========== 场景3:实际计算 —— 出现矛盾!==========127        challenge = Text('让我们实际算一算...', font="SimSun", color=WHITE).scale(0.45)128        challenge.to_edge(RIGHT, buff=1.5).shift(UP * 2.5)129        self.play(Write(challenge), run_time=1)130 131        # 画单位圆和起终点132        center_offset = ORIGIN133        unit_circle = Circle(radius=1.5, color=GREY, stroke_width=1.5, stroke_opacity=0.4)134 135        start_dot = Dot(np.array([1.5, 0, 0]), color=WHITE, radius=0.08)136        end_dot = Dot(np.array([-1.5, 0, 0]), color=WHITE, radius=0.08)137        start_label = MathTex(r"(1,0)", color=WHITE).scale(0.35)138        start_label.next_to(start_dot, DR, buff=0.05)139        end_label = MathTex(r"(-1,0)", color=WHITE).scale(0.35)140        end_label.next_to(end_dot, DL, buff=0.05)141 142        self.play(143            Create(unit_circle),144            FadeIn(start_dot), FadeIn(end_dot),145            Write(start_label), Write(end_label),146            run_time=1.2,147        )148 149        # 上半圆路径150        upper_arc = Arc(radius=1.5, start_angle=0, angle=PI, color=GREEN, stroke_width=4)151        upper_arrow = Arrow(152            upper_arc.point_from_proportion(0.45),153            upper_arc.point_from_proportion(0.55),154            buff=0, stroke_width=3, color=GREEN, tip_length=0.15,155        )156        path1_label = MathTex(r"C_1", color=GREEN).scale(0.45)157        path1_label.next_to(upper_arc, UP, buff=0.1)158 159        self.play(Create(upper_arc), Create(upper_arrow), Write(path1_label), run_time=1.2)160 161        # 上半圆计算162        calc_upper = MathTex(163            r"\int_{C_1} \vec{F}\cdot d\vec{r} = \pi",164            color=GREEN,165        ).scale(0.5)166        calc_upper.next_to(challenge, DOWN, buff=0.3).align_to(challenge, LEFT)167        self.play(Write(calc_upper), run_time=1.2)168        self.wait(0.8)169 170        # 下半圆路径171        lower_arc = Arc(radius=1.5, start_angle=0, angle=-PI, color=ORANGE, stroke_width=4)172        lower_arrow = Arrow(173            lower_arc.point_from_proportion(0.45),174            lower_arc.point_from_proportion(0.55),175            buff=0, stroke_width=3, color=ORANGE, tip_length=0.15,176        )177        path2_label = MathTex(r"C_2", color=ORANGE).scale(0.45)178        path2_label.next_to(lower_arc, DOWN, buff=0.1)179 180        self.play(Create(lower_arc), Create(lower_arrow), Write(path2_label), run_time=1.2)181 182        # 下半圆计算183        calc_lower = MathTex(184            r"\int_{C_2} \vec{F}\cdot d\vec{r} = -\pi",185            color=ORANGE,186        ).scale(0.5)187        calc_lower.next_to(calc_upper, DOWN, buff=0.15).align_to(challenge, LEFT)188        self.play(Write(calc_lower), run_time=1.2)189        self.wait(0.8)190 191        # 矛盾!192        contradiction = MathTex(193            r"\pi \neq -\pi\ ???",194            color=RED,195        ).scale(0.7)196        contradiction.next_to(calc_lower, DOWN, buff=0.3).align_to(challenge, LEFT)197        self.play(Write(contradiction), run_time=1)198 199        # 震动效果200        self.play(201            contradiction.animate.shift(LEFT * 0.1),202            run_time=0.05,203        )204        self.play(205            contradiction.animate.shift(RIGHT * 0.2),206            run_time=0.05,207        )208        self.play(209            contradiction.animate.shift(LEFT * 0.1),210            run_time=0.05,211        )212 213        shock_text = Text('等等...说好的路径无关呢?!', font="SimSun", color=RED).scale(0.4)214        shock_text.next_to(contradiction, DOWN, buff=0.15)215        self.play(Write(shock_text), run_time=1.2)216        self.wait(2)217 218        # ========== 场景4:环路积分 —— 坐实矛盾 ==========219        self.play(220            FadeOut(challenge), FadeOut(calc_upper), FadeOut(calc_lower),221            FadeOut(contradiction), FadeOut(shock_text),222            FadeOut(upper_arc), FadeOut(upper_arrow), FadeOut(path1_label),223            FadeOut(lower_arc), FadeOut(lower_arrow), FadeOut(path2_label),224            FadeOut(start_dot), FadeOut(end_dot),225            FadeOut(start_label), FadeOut(end_label),226        )227 228        step4_label = Text('再看闭合环路积分:', font="SimSun", color=WHITE).scale(0.42)229        step4_label.to_edge(RIGHT, buff=1.5).shift(UP * 2.0)230        self.play(Write(step4_label), run_time=0.8)231 232        # 完整单位圆 + 逆时针箭头233        full_circle = Circle(radius=1.5, color=YELLOW, stroke_width=4)234        circle_arrows = VGroup()235        for prop in [0.1, 0.35, 0.6, 0.85]:236            pos = full_circle.point_from_proportion(prop)237            next_pos = full_circle.point_from_proportion(prop + 0.03)238            tangent = next_pos - pos239            tangent = tangent / np.linalg.norm(tangent) * 0.25240            arr = Arrow(241                pos - tangent * 0.5, pos + tangent * 0.5,242                buff=0, stroke_width=2.5, color=YELLOW, tip_length=0.12,243            )244            circle_arrows.add(arr)245 246        self.play(247            FadeOut(unit_circle),248            Create(full_circle),249            LaggedStartMap(Create, circle_arrows, lag_ratio=0.15),250            run_time=1.5,251        )252 253        # 环路计算254        loop_calc = MathTex(255            r"\oint_L \vec{F}\cdot d\vec{r} = \int_0^{2\pi} 1\,dt = 2\pi",256            color=WHITE,257        ).scale(0.5)258        loop_calc.next_to(step4_label, DOWN, buff=0.25).align_to(step4_label, LEFT)259        self.play(Write(loop_calc), run_time=1.5)260 261        # 红色强调262        loop_result = MathTex(r"= 2\pi \neq 0\ !", color=RED).scale(0.65)263        loop_result.next_to(loop_calc, DOWN, buff=0.15).align_to(step4_label, LEFT)264        result_box = SurroundingRectangle(loop_result, color=RED, buff=0.08, corner_radius=0.05)265        self.play(Write(loop_result), Create(result_box), run_time=1.2)266 267        loop_note = Text('闭合环路积分不为零!路径无关不成立!', font="SimSun", color=RED).scale(0.35)268        loop_note.next_to(result_box, DOWN, buff=0.12)269        self.play(Write(loop_note), run_time=1.2)270        self.wait(2)271 272        # ========== 场景5:揭秘 —— 问题出在哪里?==========273        self.play(274            FadeOut(step4_label), FadeOut(loop_calc),275            FadeOut(loop_result), FadeOut(result_box), FadeOut(loop_note),276            FadeOut(full_circle), FadeOut(circle_arrows),277            run_time=1,278        )279 280        reveal_title = Text('问题出在哪里?', font="SimSun", color=YELLOW).scale(0.55)281        reveal_title.to_edge(UP, buff=0.35).to_edge(RIGHT, buff=1.5)282        self.play(Write(reveal_title), run_time=1)283 284        # 划掉原来的定理285        cross_line = Line(286            theorem_group.get_left() + LEFT * 0.1,287            theorem_group.get_right() + RIGHT * 0.1,288            color=RED, stroke_width=4,289        )290        self.play(Create(cross_line), run_time=1)291        self.wait(0.5)292 293        wrong_label = Text('不完整!', font="SimSun", color=RED).scale(0.4)294        wrong_label.next_to(theorem_box, RIGHT, buff=0.15)295        self.play(Write(wrong_label), run_time=0.8)296        self.wait(1)297 298        # 标注原点299        origin_dot = Dot(ORIGIN, color=RED, radius=0.12)300        origin_cross = VGroup(301            Line(UL * 0.15, DR * 0.15, color=RED, stroke_width=4),302            Line(UR * 0.15, DL * 0.15, color=RED, stroke_width=4),303        )304        origin_label = Text('奇点(无定义)', font="SimSun", color=RED).scale(0.3)305        origin_label.next_to(origin_dot, DR, buff=0.1)306 307        self.play(FadeIn(origin_dot), Create(origin_cross), Write(origin_label), run_time=1)308        self.wait(1)309 310        # 正确版本的定理311        correct_theorem = VGroup(312            Text("正确表述:", font="SimSun", color=YELLOW).scale(0.4),313            MathTex(314                r"\text{若}\ D\ \text{为}\,",315                r"\text{单连通区域}",316                r"\text{,且}\ \frac{\partial Q}{\partial x} = \frac{\partial P}{\partial y}",317                color=WHITE,318            ).scale(0.45),319            MathTex(320                r"\text{则曲线积分与路径无关}",321                color=WHITE,322            ).scale(0.45),323        )324        correct_theorem[1][1].set_color(YELLOW)325        correct_theorem.arrange(DOWN, buff=0.1, aligned_edge=LEFT)326        correct_theorem.move_to(RIGHT * 3.8 + DOWN * 0.5)327 328        correct_box = SurroundingRectangle(correct_theorem, color=YELLOW, buff=0.12, corner_radius=0.08)329        self.play(Write(correct_theorem), Create(correct_box), run_time=2)330        self.wait(1)331 332        # 单连通 vs 多连通对比333        self.play(334            FadeOut(vector_field), FadeOut(origin_dot),335            FadeOut(origin_cross), FadeOut(origin_label),336            run_time=0.8,337        )338 339        # 单连通示意340        simple_region = Circle(radius=0.8, color=GREEN, fill_color=GREEN, fill_opacity=0.2, stroke_width=3)341        simple_region.move_to(LEFT * 3.5 + DOWN * 2.0)342        simple_label = Text('单连通(无洞)', font="SimSun", color=GREEN).scale(0.3)343        simple_label.next_to(simple_region, DOWN, buff=0.1)344        simple_check = MathTex(r"\checkmark", color=GREEN).scale(0.8)345        simple_check.next_to(simple_region, RIGHT, buff=0.15)346 347        # 多连通示意(带洞)348        outer_ring = Circle(radius=0.8, color=RED, fill_color=RED, fill_opacity=0.1, stroke_width=3)349        inner_hole = Circle(radius=0.2, color=RED, fill_color=BLACK, fill_opacity=1.0, stroke_width=2)350        outer_ring.move_to(LEFT * 0.5 + DOWN * 2.0)351        inner_hole.move_to(outer_ring.get_center())352        multi_region = VGroup(outer_ring, inner_hole)353        multi_label = Text('多连通(有洞)', font="SimSun", color=RED).scale(0.3)354        multi_label.next_to(outer_ring, DOWN, buff=0.1)355        multi_cross = MathTex(r"\times", color=RED).scale(0.8)356        multi_cross.next_to(outer_ring, RIGHT, buff=0.15)357 358        self.play(359            Create(simple_region), Write(simple_label), Write(simple_check),360            run_time=1.2,361        )362        self.play(363            Create(outer_ring), Create(inner_hole),364            Write(multi_label), Write(multi_cross),365            run_time=1.2,366        )367        self.wait(1)368 369        # 最终结论370        final_note = Text(371            'R² \\ {O} 有洞 → 多连通 → 定理不适用!',372            font="SimSun", color=YELLOW,373        ).scale(0.4)374        final_note.to_edge(DOWN, buff=0.3)375        self.play(Write(final_note), run_time=1.5)376        self.wait(3)377 378 379def main():380    import os381    os.system("manim -pqh path_independence_counterexample.py PathIndependenceCounterexample")382 383 384if __name__ == "__main__":385    main()

讲解

这个视频用一个经典反例提醒:只验证

Qx=Py\frac{\partial Q}{\partial x}=\frac{\partial P}{\partial y}

还不能直接推出曲线积分路径无关。还需要确认定义域满足单连通等条件。

视频考察的向量场是

F=(yx2+y2,xx2+y2).\vec F=\left(\frac{-y}{x^2+y^2},\frac{x}{x^2+y^2}\right).

开头先展示一个“不完整定理”:若偏导相等,则曲线积分与路径无关。向量场画面随后给出绕原点旋转的场,为后面的反例做准备。

偏导验证部分计算

P=yx2+y2,Q=xx2+y2,P=\frac{-y}{x^2+y^2},\qquad Q=\frac{x}{x^2+y^2},

并验证

Qx=Py=y2x2(x2+y2)2.\frac{\partial Q}{\partial x} = \frac{\partial P}{\partial y} = \frac{y^2-x^2}{(x^2+y^2)^2}.

这一步看起来满足路径无关的局部条件。

路径比较部分考察两条曲线:从 (1,0)(1,0)(1,0)(-1,0),上半圆路径积分为 π\pi,下半圆路径积分为 π-\pi。同起点同终点却得到不同结果,路径无关不成立。

闭合环路检验进一步看完整单位圆上的积分:

LFdr=02π1dt=2π0.\oint_L\vec F\cdot d\vec r = \int_0^{2\pi}1\,dt = 2\pi \neq0.

闭合环路积分不为零,直接坐实了反例。

反例原因在最后揭示:原点是奇点,向量场在原点无定义,所以定义域实际是 R2{O}\mathbb R^2\setminus\{O\}。这个区域有洞,是多连通区域,不满足“单连通”前提。

正确表述应强调:在合适的单连通区域内,且 Qx=Py\frac{\partial Q}{\partial x}=\frac{\partial P}{\partial y} 时,才能推出曲线积分与路径无关。