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() 讲解
这个视频用一个经典反例提醒:只验证
还不能直接推出曲线积分路径无关。还需要确认定义域满足单连通等条件。
视频考察的向量场是
开头先展示一个“不完整定理”:若偏导相等,则曲线积分与路径无关。向量场画面随后给出绕原点旋转的场,为后面的反例做准备。
偏导验证部分计算
并验证
这一步看起来满足路径无关的局部条件。
路径比较部分考察两条曲线:从 到 ,上半圆路径积分为 ,下半圆路径积分为 。同起点同终点却得到不同结果,路径无关不成立。
闭合环路检验进一步看完整单位圆上的积分:
闭合环路积分不为零,直接坐实了反例。
反例原因在最后揭示:原点是奇点,向量场在原点无定义,所以定义域实际是 。这个区域有洞,是多连通区域,不满足“单连通”前提。
正确表述应强调:在合适的单连通区域内,且 时,才能推出曲线积分与路径无关。