多元微积分可视化

格林公式的“洞”处理

用挖洞法绕开奇点,把含奇点区域上的边界积分转化为小圆积分,解释为什么直接套格林公式会出错。

打开原视频

greens_theorem_hole.py
1from manim import *2import numpy as np3 4config.tex_template = TexTemplateLibrary.ctex5config.tex_template.add_to_preamble(r"\setCJKmainfont{STSong}")6 7 8class GreensTheoremHole(Scene):9    def _inward_normal(self, circle, point, center):10        """Get unit vector pointing inward (toward center) from point on circle."""11        direction = center - point12        norm = np.linalg.norm(direction)13        if norm < 0.01:14            return np.array([0, 1, 0])15        return direction / norm16 17    def _outward_normal(self, circle, point, center):18        """Get unit vector pointing outward (away from center) from point on circle."""19        direction = point - center20        norm = np.linalg.norm(direction)21        if norm < 0.01:22            return np.array([0, 1, 0])23        return direction / norm24 25    def construct(self):26        # ========== 标题 ==========27        title = Text('格林公式的"洞"处理——挖洞法', font="SimSun", color=YELLOW).scale(0.6)28        title.to_edge(UP, buff=0.3)29        self.play(Write(title), run_time=1.5)30        self.wait(0.5)31 32        # ========== 场景1:展示向量场和奇点 ==========33        # 向量场函数34        center = DOWN * 0.3  # 图形中心偏下35 36        def vec_field_func(pos):37            x, y = pos[0] - center[0], pos[1] - center[1]38            r2 = x**2 + y**239            if r2 < 0.12:40                return np.array([0, 0, 0])41            return np.array([-y / r2, x / r2, 0])42 43        # 画向量场44        vector_field = ArrowVectorField(45            vec_field_func,46            x_range=[-3.5, 3.5, 0.7],47            y_range=[-3.0, 2.5, 0.7],48            length_func=lambda norm: 0.35 * sigmoid(norm),49            color_scheme=None,50        )51        vector_field.set_color(BLUE_C).set_opacity(0.5)52 53        self.play(Create(vector_field), run_time=2.5)54 55        # 原点奇点闪烁56        sing_dot = Dot(center, color=RED, radius=0.12)57        sing_ring = Circle(radius=0.2, color=RED, stroke_width=2).move_to(center)58        sing_label = MathTex("O", color=RED).scale(0.5).next_to(sing_dot, DR, buff=0.05)59 60        self.play(FadeIn(sing_dot), Create(sing_ring), Write(sing_label), run_time=1)61        self.play(62            sing_ring.animate.scale(2).set_opacity(0),63            rate_func=there_and_back,64            run_time=1,65        )66 67        # 向量场公式(缩小保留在左上)68        field_formula = MathTex(69            r"\vec{F} = \left(\frac{-y}{x^2+y^2},\ \frac{x}{x^2+y^2}\right)",70            color=WHITE,71        ).scale(0.4)72        field_formula.to_edge(UL, buff=0.5).shift(DOWN * 0.5)73 74        note_sing = Text("原点无定义(奇点)", font="SimSun", color=RED).scale(0.3)75        note_sing.next_to(field_formula, DOWN, buff=0.1)76 77        self.play(Write(field_formula), Write(note_sing), run_time=1.2)78        self.wait(1)79 80        # 要计算的问题(保留全程)81        problem_desc = Text('L 为包围原点的闭曲线(逆时针)', font="SimSun", color=WHITE).scale(0.26)82        problem_desc.next_to(field_formula, DOWN, buff=0.25)83        problem_eq = MathTex(84            r"\text{求:}\ \oint_L Pdx + Qdy = \ ?",85            color=YELLOW,86        ).scale(0.42)87        problem_eq.next_to(problem_desc, DOWN, buff=0.08)88        problem_group = VGroup(problem_desc, problem_eq)89        problem_box = SurroundingRectangle(problem_group, color=YELLOW, buff=0.08, corner_radius=0.05)90        self.play(Write(problem_desc), run_time=0.8)91        self.play(Write(problem_eq), Create(problem_box), run_time=1.2)92        self.wait(1)93 94        # ========== 场景2:外边界L + 直接用格林 → 错误 ==========95        self.play(FadeOut(note_sing), vector_field.animate.set_opacity(0.2), run_time=0.8)96 97        # 画外边界L98        outer_curve = Circle(radius=2.2, color=BLUE, stroke_width=3.5).move_to(center)99        outer_fill = Circle(radius=2.2, color=BLUE, fill_opacity=0.08, stroke_width=0).move_to(center)100 101        l_label = MathTex("L", color=BLUE).scale(0.6).next_to(outer_curve, UR, buff=-0.15)102        d_label = MathTex("D", color=BLUE).scale(0.7).move_to(center + UP * 1.0 + RIGHT * 1.0)103 104        self.play(Create(outer_curve), FadeIn(outer_fill), run_time=1.5)105        self.play(Write(l_label), Write(d_label), run_time=0.6)106 107        # 动画:点沿L逆时针运动展示方向108        tracer_L = Dot(color=BLUE, radius=0.08)109        tracer_L.move_to(outer_curve.point_from_proportion(0))110        trail_L = TracedPath(tracer_L.get_center, stroke_color=BLUE, stroke_width=4)111        self.add(trail_L)112        self.play(113            MoveAlongPath(tracer_L, outer_curve),114            run_time=2.5, rate_func=linear,115        )116        self.play(FadeOut(tracer_L), FadeOut(trail_L), run_time=0.3)117 118        # 逆时针箭头标记119        l_arrows = VGroup()120        for prop in [0.1, 0.35, 0.6, 0.85]:121            pos = outer_curve.point_from_proportion(prop)122            next_pos = outer_curve.point_from_proportion(prop + 0.02)123            tangent = next_pos - pos124            tangent = tangent / np.linalg.norm(tangent) * 0.22125            arr = Arrow(pos - tangent * 0.5, pos + tangent * 0.5,126                        buff=0, stroke_width=2.5, color=BLUE, tip_length=0.1)127            l_arrows.add(arr)128        self.play(LaggedStartMap(Create, l_arrows, lag_ratio=0.1), run_time=0.8)129 130        # 右侧:尝试格林公式131        naive_group = VGroup()132        naive_title = Text('直接用格林公式?', font="SimSun", color=WHITE).scale(0.35)133        naive_title.to_edge(RIGHT, buff=0.8).shift(UP * 1.5)134        naive_group.add(naive_title)135 136        naive_eq = MathTex(137            r"\oint_L = \iint_D 0\,d\sigma = 0\ ?",138            color=WHITE,139        ).scale(0.4)140        naive_eq.next_to(naive_title, DOWN, buff=0.12)141        naive_group.add(naive_eq)142 143        self.play(Write(naive_title), run_time=0.6)144        self.play(Write(naive_eq), run_time=1)145 146        # 闪烁D区域表示"有问题"147        flash_fill = outer_fill.copy().set_fill(RED, opacity=0.3)148        self.play(FadeIn(flash_fill), run_time=0.3)149        self.play(FadeOut(flash_fill), run_time=0.3)150        self.play(FadeIn(flash_fill), run_time=0.3)151        self.play(FadeOut(flash_fill), run_time=0.3)152 153        # 错误提示154        error_text = Text('错误!D内有奇点!', font="SimSun", color=RED).scale(0.32)155        error_text.next_to(naive_eq, DOWN, buff=0.15)156        cross_line = Line(157            naive_eq.get_left() + LEFT * 0.05,158            naive_eq.get_right() + RIGHT * 0.05,159            color=RED, stroke_width=3,160        )161        self.play(Write(error_text), Create(cross_line), run_time=1)162        self.wait(1.5)163 164        # 清理右侧165        self.play(FadeOut(naive_group), FadeOut(error_text), FadeOut(cross_line), run_time=0.6)166 167        # ========== 场景3:挖洞法 —— 动画展示 ==========168        step3_label = Text('挖洞法:绕开奇点', font="SimSun", color=GREEN).scale(0.35)169        step3_label.to_edge(RIGHT, buff=0.8).shift(UP * 1.8)170        self.play(Write(step3_label), run_time=0.8)171 172        # 动画:小圆从0逐渐长大到 epsilon173        eps_circle = Circle(radius=0.01, color=YELLOW, stroke_width=3).move_to(center)174        self.play(Create(eps_circle), run_time=0.3)175        self.play(eps_circle.animate.scale(50), run_time=1.5)  # 0.01 * 50 = 0.5176 177        # 填充黑色(挖掉)178        hole_fill = Circle(radius=0.5, color=BLACK, fill_opacity=0.85, stroke_width=0).move_to(center)179        self.play(FadeIn(hole_fill), FadeOut(sing_dot), FadeOut(sing_ring), FadeOut(sing_label), run_time=1)180 181        # 小圆标签182        eps_label = MathTex(r"C_\varepsilon", color=YELLOW).scale(0.45)183        eps_label.next_to(eps_circle, DOWN, buff=0.08)184        self.play(Write(eps_label), run_time=0.5)185 186        # 描述小圆的特征/方程187        eps_desc = MathTex(188            r"C_\varepsilon: x^2 + y^2 = \varepsilon^2",189            color=YELLOW,190        ).scale(0.38)191        eps_desc.next_to(step3_label, DOWN, buff=0.15)192        eps_note = Text('以奇点为圆心,半径为 ε 的小圆', font="SimSun", color=WHITE).scale(0.26)193        eps_note.next_to(eps_desc, DOWN, buff=0.08)194        eps_note2 = Text('(ε 足够小,使 C_ε 完全在 L 内部)', font="SimSun", color=GREY_B).scale(0.24)195        eps_note2.next_to(eps_note, DOWN, buff=0.05)196        self.play(Write(eps_desc), run_time=0.8)197        self.play(Write(eps_note), Write(eps_note2), run_time=1)198        self.wait(1)199 200        # 点沿 C_eps 顺时针运动展示方向201        # 顺时针 = 反向参数化202        tracer_eps = Dot(color=YELLOW, radius=0.07)203        eps_path_cw = eps_circle.copy().reverse_direction()204        tracer_eps.move_to(eps_path_cw.point_from_proportion(0))205        trail_eps = TracedPath(tracer_eps.get_center, stroke_color=YELLOW, stroke_width=3)206        self.add(trail_eps)207        self.play(208            MoveAlongPath(tracer_eps, eps_path_cw),209            run_time=2, rate_func=linear,210        )211        self.play(FadeOut(tracer_eps), FadeOut(trail_eps), run_time=0.3)212 213        # 顺时针箭头214        eps_arrows = VGroup()215        for prop in [0.1, 0.4, 0.7]:216            pos = eps_circle.point_from_proportion(prop)217            prev_pos = eps_circle.point_from_proportion(prop - 0.025)218            tangent = prev_pos - pos219            tangent = tangent / np.linalg.norm(tangent) * 0.18220            arr = Arrow(pos - tangent * 0.5, pos + tangent * 0.5,221                        buff=0, stroke_width=2.5, color=YELLOW, tip_length=0.08)222            eps_arrows.add(arr)223        self.play(LaggedStartMap(Create, eps_arrows, lag_ratio=0.15), run_time=0.8)224 225        # 标注新区域 D'226        d_prime_label = MathTex(r"D'", color=GREEN).scale(0.65)227        d_prime_label.move_to(center + UP * 1.2 + LEFT * 0.7)228        self.play(Transform(d_label, d_prime_label), run_time=0.8)229 230        # 右侧注释231        note_dprime = Text("D' 无奇点 → 可用格林公式", font="SimSun", color=GREEN).scale(0.3)232        note_dprime.next_to(eps_note2, DOWN, buff=0.15)233        self.play(Write(note_dprime), run_time=1)234        self.wait(1.5)235 236        # ========== 场景3.5:强调复连通区域边界正方向 ==========237        self.play(238            FadeOut(step3_label), FadeOut(note_dprime),239            FadeOut(eps_desc), FadeOut(eps_note), FadeOut(eps_note2),240            run_time=0.5,241        )242 243        dir_title = Text('复连通区域的边界正方向', font="SimSun", color=TEAL).scale(0.38)244        dir_title.to_edge(RIGHT, buff=0.6).shift(UP * 2.2)245        self.play(Write(dir_title), run_time=0.8)246 247        # 规则文字248        rule_text = Text('规则:沿正方向行走时', font="SimSun", color=WHITE).scale(0.3)249        rule_text.next_to(dir_title, DOWN, buff=0.15)250        rule_text2 = Text('区域 D\' 始终在左手边', font="SimSun", color=YELLOW).scale(0.3)251        rule_text2.next_to(rule_text, DOWN, buff=0.05)252        self.play(Write(rule_text), run_time=0.8)253        self.play(Write(rule_text2), run_time=0.8)254 255        # 高亮外边界 —— 逆时针256        outer_dir_label = Text('外边界 L:逆时针', font="SimSun", color=BLUE).scale(0.28)257        outer_dir_label.next_to(rule_text2, DOWN, buff=0.25)258        self.play(Write(outer_dir_label), run_time=0.6)259 260        # 动画:小人沿外边界逆时针走 + 左手指向内侧261        walker_L = Dot(color=BLUE, radius=0.1)262        walker_L.move_to(outer_curve.point_from_proportion(0))263        # 左手指示箭头(指向区域内侧)264        left_hand_L = always_redraw(265            lambda: Arrow(266                walker_L.get_center(),267                walker_L.get_center() + self._inward_normal(outer_curve, walker_L.get_center(), center) * 0.5,268                buff=0, color=GREEN, stroke_width=3, tip_length=0.1,269            )270        )271        left_label_L = always_redraw(272            lambda: Text('D\'', font="SimSun", color=GREEN).scale(0.22).move_to(273                walker_L.get_center() + self._inward_normal(outer_curve, walker_L.get_center(), center) * 0.7274            )275        )276 277        self.add(left_hand_L, left_label_L)278        self.play(279            MoveAlongPath(walker_L, outer_curve),280            run_time=4, rate_func=linear,281        )282        self.play(FadeOut(walker_L), FadeOut(left_hand_L), FadeOut(left_label_L), run_time=0.3)283 284        # 高亮内边界 —— 顺时针285        inner_dir_label = Text('内边界 C_ε:顺时针', font="SimSun", color=YELLOW).scale(0.28)286        inner_dir_label.next_to(outer_dir_label, DOWN, buff=0.12)287        self.play(Write(inner_dir_label), run_time=0.6)288 289        # 动画:小人沿内边界顺时针走 + 左手指向外侧(即区域D')290        walker_C = Dot(color=YELLOW, radius=0.1)291        eps_path_cw2 = eps_circle.copy().reverse_direction()292        walker_C.move_to(eps_path_cw2.point_from_proportion(0))293        left_hand_C = always_redraw(294            lambda: Arrow(295                walker_C.get_center(),296                walker_C.get_center() + self._outward_normal(eps_circle, walker_C.get_center(), center) * 0.5,297                buff=0, color=GREEN, stroke_width=3, tip_length=0.1,298            )299        )300        left_label_C = always_redraw(301            lambda: Text('D\'', font="SimSun", color=GREEN).scale(0.22).move_to(302                walker_C.get_center() + self._outward_normal(eps_circle, walker_C.get_center(), center) * 0.7303            )304        )305 306        self.add(left_hand_C, left_label_C)307        self.play(308            MoveAlongPath(walker_C, eps_path_cw2),309            run_time=3, rate_func=linear,310        )311        self.play(FadeOut(walker_C), FadeOut(left_hand_C), FadeOut(left_label_C), run_time=0.3)312 313        # 总结图示314        summary_dir = VGroup(315            MathTex(r"L:", color=BLUE).scale(0.4),316            Text('逆时针(区域在左)', font="SimSun", color=BLUE).scale(0.26),317        ).arrange(RIGHT, buff=0.1)318        summary_dir2 = VGroup(319            MathTex(r"C_\varepsilon:", color=YELLOW).scale(0.4),320            Text('顺时针(区域在左)', font="SimSun", color=YELLOW).scale(0.26),321        ).arrange(RIGHT, buff=0.1)322        summary_group = VGroup(summary_dir, summary_dir2).arrange(DOWN, buff=0.1, aligned_edge=LEFT)323        summary_group.next_to(inner_dir_label, DOWN, buff=0.2)324        self.play(FadeIn(summary_group), run_time=0.8)325        self.wait(2)326 327        # 清理328        dir_objs = VGroup(dir_title, rule_text, rule_text2, outer_dir_label, inner_dir_label, summary_group)329        self.play(FadeOut(dir_objs), run_time=0.6)330 331        # ========== 场景4:在D'上格林公式(图+公式配合) ==========332 333        # 高亮两条边界334        self.play(335            outer_curve.animate.set_color(BLUE).set_stroke(width=5),336            eps_circle.animate.set_color(YELLOW).set_stroke(width=5),337            run_time=0.8,338        )339        self.play(340            outer_curve.animate.set_stroke(width=3.5),341            eps_circle.animate.set_stroke(width=3),342            run_time=0.5,343        )344 345        # 右侧公式346        step4_eq1 = MathTex(347            r"\partial D' = ", r"L", r"\ +\ ", r"C_\varepsilon^-",348            color=WHITE,349        ).scale(0.4)350        step4_eq1[1].set_color(BLUE)351        step4_eq1[3].set_color(YELLOW)352        step4_eq1.to_edge(RIGHT, buff=0.8).shift(UP * 1.5)353        self.play(Write(step4_eq1), run_time=1)354 355        # 闪烁对应边界356        self.play(outer_curve.animate.set_color(WHITE), run_time=0.3)357        self.play(outer_curve.animate.set_color(BLUE), run_time=0.3)358        self.play(eps_circle.animate.set_color(WHITE), run_time=0.3)359        self.play(eps_circle.animate.set_color(YELLOW), run_time=0.3)360 361        # 格林公式362        step4_eq2 = MathTex(363            r"\oint_L + \oint_{C_\varepsilon^-} = \iint_{D'} 0\,d\sigma",364            color=WHITE,365        ).scale(0.38)366        step4_eq2.next_to(step4_eq1, DOWN, buff=0.2)367        self.play(Write(step4_eq2), run_time=1.5)368 369        # 闪烁D'区域(绿色)表示积分为0370        annular_fill = Annulus(371            inner_radius=0.5, outer_radius=2.2,372            color=GREEN, fill_opacity=0.2, stroke_width=0,373        ).move_to(center)374        self.play(FadeIn(annular_fill), run_time=0.8)375        self.play(FadeOut(annular_fill), run_time=0.8)376 377        # = 0378        step4_eq3 = MathTex(r"= 0", color=TEAL).scale(0.5)379        step4_eq3.next_to(step4_eq2, DOWN, buff=0.1)380        self.play(Write(step4_eq3), run_time=0.6)381 382        # 关键结论383        step4_result = MathTex(384            r"\therefore\ \oint_L = -\oint_{C_\varepsilon^-} = \oint_{C_\varepsilon^+}",385            color=YELLOW,386        ).scale(0.42)387        step4_result.next_to(step4_eq3, DOWN, buff=0.2)388        result_box = SurroundingRectangle(step4_result, color=YELLOW, buff=0.06, corner_radius=0.05)389        self.play(Write(step4_result), Create(result_box), run_time=1.5)390        self.wait(2)391 392        # ========== 场景5:转化为小圆积分(可视化计算) ==========393        # 清理场景4公式(保留黄框结论)394        scene4_cleanup = VGroup(step4_eq1, step4_eq2, step4_eq3)395        self.play(FadeOut(scene4_cleanup), run_time=0.6)396 397        # 将黄框结论移到问题框下方保留398        self.play(399            step4_result.animate.scale(0.9).next_to(problem_box, DOWN, buff=0.15),400            result_box.animate.scale(0.9).move_to(401                problem_box.get_bottom() + DOWN * 0.35402            ),403            run_time=1,404        )405        # 重新包围406        new_result_box = SurroundingRectangle(step4_result, color=YELLOW, buff=0.06, corner_radius=0.05)407        self.play(Transform(result_box, new_result_box), run_time=0.3)408 409        # 移除外边界相关,放大小圆410        self.play(411            FadeOut(outer_curve), FadeOut(outer_fill), FadeOut(l_arrows),412            FadeOut(l_label), FadeOut(d_label),413            FadeOut(vector_field),414            eps_circle.animate.scale(3).move_to(LEFT * 2.5 + DOWN * 0.3),415            hole_fill.animate.scale(3).move_to(LEFT * 2.5 + DOWN * 0.3),416            eps_arrows.animate.scale(3).move_to(LEFT * 2.5 + DOWN * 0.3),417            eps_label.animate.scale(1.3).next_to(LEFT * 2.5 + DOWN * 0.3, DOWN, buff=0.6),418            run_time=1.5,419        )420 421        # 放大后重新定位422        big_center = LEFT * 2.5 + DOWN * 0.3423        big_circle = eps_circle424 425        # 参数化标注:在圆上标点426        angle_tracker = ValueTracker(0)427        radius = 1.5  # 0.5 * 3428 429        moving_dot = always_redraw(430            lambda: Dot(431                big_center + radius * np.array([432                    np.cos(angle_tracker.get_value()),433                    np.sin(angle_tracker.get_value()), 0434                ]),435                color=ORANGE, radius=0.08,436            )437        )438        # 半径线439        radius_line = always_redraw(440            lambda: DashedLine(441                big_center,442                big_center + radius * np.array([443                    np.cos(angle_tracker.get_value()),444                    np.sin(angle_tracker.get_value()), 0445                ]),446                color=ORANGE, stroke_width=2,447            )448        )449        # 角度标注450        angle_label = always_redraw(451            lambda: MathTex("t", color=ORANGE).scale(0.45).move_to(452                big_center + 0.4 * np.array([453                    np.cos(angle_tracker.get_value() / 2),454                    np.sin(angle_tracker.get_value() / 2), 0455                ])456            )457        )458 459        self.play(FadeIn(moving_dot), Create(radius_line), Write(angle_label), run_time=0.8)460 461        # 右侧参数化公式462        param_eq = MathTex(463            r"x = \varepsilon\cos t,\ y = \varepsilon\sin t",464            color=ORANGE,465        ).scale(0.4)466        param_eq.to_edge(RIGHT, buff=1.2).shift(UP * 1.5)467        param_range = MathTex(r"t: 0 \to 2\pi", color=ORANGE).scale(0.4)468        param_range.next_to(param_eq, DOWN, buff=0.1)469        self.play(Write(param_eq), Write(param_range), run_time=1)470 471        # 动画:点沿圆逆时针转一圈472        self.play(angle_tracker.animate.set_value(2 * PI), run_time=3, rate_func=linear)473        self.wait(0.5)474 475        # 代入计算476        calc1 = MathTex(477            r"\oint_{C_\varepsilon^+} Pdx + Qdy",478            color=WHITE,479        ).scale(0.4)480        calc1.next_to(param_range, DOWN, buff=0.3)481        self.play(Write(calc1), run_time=0.8)482 483        calc2 = MathTex(484            r"= \int_0^{2\pi} \frac{\varepsilon^2}{\varepsilon^2}\,dt",485            color=WHITE,486        ).scale(0.4)487        calc2.next_to(calc1, DOWN, buff=0.1)488        self.play(Write(calc2), run_time=1)489 490        calc3 = MathTex(491            r"= \int_0^{2\pi} 1\,dt = 2\pi",492            color=YELLOW,493        ).scale(0.5)494        calc3.next_to(calc2, DOWN, buff=0.1)495        self.play(Write(calc3), run_time=1.2)496 497        calc_box = SurroundingRectangle(calc3, color=YELLOW, buff=0.06, corner_radius=0.05)498        self.play(Create(calc_box), run_time=0.5)499        self.wait(1)500 501        # 最终结论:更新问题框的答案502        answer_eq = MathTex(503            r"\text{求:}\ \oint_L Pdx + Qdy = 2\pi",504            color=YELLOW,505        ).scale(0.42)506        answer_eq.move_to(problem_eq)507        answer_group = VGroup(problem_desc.copy(), answer_eq)508        answer_box = SurroundingRectangle(answer_group, color=GREEN, buff=0.08, corner_radius=0.05)509        self.play(510            Transform(problem_eq, answer_eq),511            Transform(problem_box, answer_box),512            run_time=1.5,513        )514 515        summary = Text('挖洞法:绕开奇点,转化为小圆上的简单积分!', font="SimSun", color=GREEN).scale(0.32)516        summary.to_edge(DOWN, buff=0.4)517        self.play(Write(summary), run_time=1.2)518        self.wait(3)519 520 521def main():522    import os523    os.system("manim -pql greens_theorem_hole.py GreensTheoremHole")524 525 526if __name__ == "__main__":527    main()

讲解

这个视频处理格林公式里最容易被忽略的一种情况:区域内部有奇点。向量场

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

在原点无定义。虽然在原点之外有

QxPy=0,\frac{\partial Q}{\partial x}-\frac{\partial P}{\partial y}=0,

但如果闭曲线 LL 包围原点,就不能直接写成

LPdx+Qdy=D0dσ=0.\oint_L P\,dx+Q\,dy=\iint_D 0\,d\sigma=0.

问题不在公式本身,而在使用条件:格林公式要求向量场在区域内满足相应光滑性。原点这个奇点落在 DD 内部时,DD 不是可以直接套用格林公式的区域。

挖去奇点

开场画面先定义绕原点旋转的向量场,并在靠近奇点时避免绘制。目标积分提示把向量场、奇点和待求边界积分放在同一张图上:

LPdx+Qdy?\oint_L P\,dx+Q\,dy\quad ?

错误尝试画面展示“直接用格林公式”的问题:外边界 LL 围成的区域 DD 内有奇点,所以不能把右侧二重积分简单写成 00

挖洞法从这一处开始。动画在奇点周围挖去一个小圆

Cε: x2+y2=ε2,C_\varepsilon:\ x^2+y^2=\varepsilon^2,

把原区域改成没有奇点的环形区域 DD'。这样一来,格林公式可以在 DD' 上使用。

边界方向

对带洞区域,边界不只是一条曲线。边界方向说明专门解释复连通区域的正方向:

D=L+Cε.\partial D'=L+C_\varepsilon^-.

外边界 LL 取逆时针方向;内边界 CεC_\varepsilon^- 取顺时针方向。这样沿边界行走时,区域 DD' 始终在左手边。

DD' 上使用格林公式时,可以得到

L+Cε=D0dσ=0.\oint_L+\oint_{C_\varepsilon^-} = \iint_{D'}0\,d\sigma =0.

于是

L=Cε=Cε+.\oint_L = -\oint_{C_\varepsilon^-} = \oint_{C_\varepsilon^+}.

也就是说,原来沿外边界 LL 的积分,可以转化成沿小圆正向 Cε+C_\varepsilon^+ 的积分。

小圆积分

小圆积分部分把内边界参数化为

x=εcost,y=εsint,0t2π.x=\varepsilon\cos t,\qquad y=\varepsilon\sin t,\qquad 0\le t\le 2\pi.

代入向量场后,

Pdx+Qdy=ε2ε2dt=dt.P\,dx+Q\,dy = \frac{\varepsilon^2}{\varepsilon^2}\,dt =dt.

因此

Cε+Pdx+Qdy=02π1dt=2π.\oint_{C_\varepsilon^+}P\,dx+Q\,dy = \int_0^{2\pi}1\,dt =2\pi.

最终得到

LPdx+Qdy=2π.\oint_L P\,dx+Q\,dy=2\pi.

这个结论也解释了为什么“旋度为零”不等于“闭合曲线积分一定为零”:如果区域里有洞或奇点,格林公式的使用条件必须先处理清楚。