用SymPy自动计算抛物线求根、判别式与顶点

Manim 动画时,我想让抛物线 y=x2+bx+2y=x^2+bx+2 y=x2+bx+2随着系数 b 的变化,自动、精准地显示它与 x 轴的交点。

手写求根公式不仅繁琐,还要自己处理判别式为负的情况,稍不注意 math.sqrt 就会让整个动画崩溃。

本文我们就用 SymPy 彻底解决这个痛点。

1. 痛点场景还原

假设我要做一个演示:固定 a=1, c=2,让 b 从 -3 滑到 3,观察抛物线与 x 轴交点个数的变化。

如果纯手算,我可能会这样写 Manim 代码:

python 复制代码
from manim import *
import math

class PainfulDemo(Scene):
    def construct(self):
        a, c = 1, 2
        b_tracker = ValueTracker(-3)
        axes = Axes(x_range=[-5,5], y_range=[-1,6])

        # 抛物线
        graph = always_redraw(lambda: axes.plot(
            lambda x: a*x**2 + b_tracker.get_value()*x + c
        ))

        # 计算交点 ------ 这里就是噩梦开始的地方
        def get_roots():
            b = b_tracker.get_value()
            disc = b**2 - 4*a*c
            if disc >= 0:
                root1 = (-b + math.sqrt(disc)) / (2*a)   # 负数平方根直接报错
                root2 = (-b - math.sqrt(disc)) / (2*a)
                return [root1, root2]
            else:
                return []  # 如果忘了判断,上面一行就炸了

        dots = always_redraw(lambda: VGroup(*[
            Dot(axes.coords_to_point(r, 0)) for r in get_roots()
        ]))
        self.add(axes, graph, dots)
        self.play(b_tracker.animate.set_value(3), run_time=5)
        self.wait(1)
  • 我必须手动写出求根公式,反复检查符号。
  • 判别式 <0 时要手动跳过,否则 math.sqrt 抛异常,动画中断。
  • 得到的只是浮点近似值,不能显示精确的根式表达(如 2 \sqrt{2} 2 )。
  • 如果再加「自动标注顶点」,还得再手算一次导数或配方法。

这些体力活完全可以交给符号计算库 SymPy,让动画代码只关心"展示什么",而不是"怎么算"。

2. SymPy 解决方案介绍

SymPy 可以帮我们把 求根、判别式计算、顶点坐标求解 全部自动化,而且返回精确的符号表达式。

python 复制代码
import sympy as sp

x = sp.Symbol('x', real=True)
a_val, c_val = 1, 2
b_sym = sp.Symbol('b')

# 定义二次函数
expr = a_val * x**2 + b_sym * x + c_val

# 1. 判别式
delta = b_sym**2 - 4 * a_val * c_val   # b² - 8

# 2. 求根 ------ 一行搞定,自动给出含根号的精确解
roots = sp.solve(expr, x)              
# 输出:[-b/2 - sqrt(b**2 - 8)/2, -b/2 + sqrt(b**2 - 8)/2]

# 3. 求顶点(导数求极值)
vertex_x = sp.solve(sp.diff(expr, x), x)[0]   # -b/2
vertex_y = expr.subs(x, vertex_x)             # 代入得到顶点纵坐标
  • solve 返回的根自带根号,当判别式 <0 时,它会变成复数形式(如 -b/2 - I*sqrt(8 - b**2)/2),我们只需判断虚部是否为 0 就能筛出实根。
  • 顶点坐标也不用背公式,diff 求导 + solve 一步到位。

Manim 中,我们只需要把数值 b 传入 SymPy 表达式,调用 evalf() 就可以快速得到高精度结果,彻底告别手写公式。

3. Manim 联动实战

下面是一个完整的动画场景:b 变化时,抛物线、交点、顶点、判别式与交点个数文本全部自动更新。

python 复制代码
from manim import *
import sympy as sp

class QuadraticRootDance(Scene):
    def construct(self):
        # ========== SymPy 符号准备 ==========
        x_sym = sp.Symbol("x", real=True)
        a_val, c_val = 1, 2  # 固定 a, c,只让 b 变化
        b_sym = sp.Symbol("b")
        expr = a_val * x_sym**2 + b_sym * x_sym + c_val

        # 判别式表达式
        delta_expr = b_sym**2 - 4 * a_val * c_val  # b² - 8
        # 顶点 x 坐标(求导)
        vertex_x_expr = sp.solve(sp.diff(expr, x_sym), x_sym)[0]  # -b/2
        # 顶点 y 坐标
        vertex_y_expr = expr.subs(x_sym, vertex_x_expr)

        # ========== Manim 场景搭建 ==========
        axes = Axes(
            x_range=[-5, 5, 1],
            y_range=[-1, 7, 1],
            axis_config={"include_numbers": True, "font_size": 18},
            tips=False,
        ).add_coordinates()
        self.play(Create(axes))

        b_tracker = ValueTracker(-3)  # b 初始值 -3

        # 抛物线:always_redraw 保证系数一更新图像就重绘
        graph = always_redraw(
            lambda: axes.plot(
                lambda x: a_val * x**2 + b_tracker.get_value() * x + c_val, color=BLUE
            )
        )
        self.add(graph)

        # 交点集合(实心圆点)
        roots_dots = always_redraw(
            lambda: self.get_roots_dots(axes, b_tracker, x_sym, expr)
        )
        self.add(roots_dots)

        # 顶点标记
        vertex_dot = always_redraw(
            lambda: self.get_vertex_dot(axes, b_tracker, vertex_x_expr, vertex_y_expr)
        )
        self.add(vertex_dot)

        # 动态文本:判别式 & 交点个数
        info_text = always_redraw(
            lambda: self.get_info_text(b_tracker, delta_expr, x_sym, expr)
        )
        info_text.to_corner(UR)
        self.add(info_text)

        # 动画:b 从 -3 滑到 3
        self.play(b_tracker.animate.set_value(3), run_time=5, rate_func=linear)
        self.wait()

    # ---------- 辅助方法(内部封装 SymPy 计算)----------
    def get_roots_dots(self, axes, tracker, x_sym, expr):
        """返回当前参数下所有实根对应的 Dot"""
        b_val = tracker.get_value()
        # 用 SymPy 解方程,并数值化
        roots = sp.solve(expr.subs("b", b_val), x_sym)
        real_roots = []
        for r in roots:
            r_num = complex(r.evalf())  # 转为 Python 复数判断虚实
            if abs(r_num.imag) < 1e-8:  # 虚部为 0 -> 实根
                real_roots.append(r_num.real)
        # 为每个实根创建红点
        dot_group = VGroup()
        for rx in real_roots:
            dot_group.add(Dot(axes.coords_to_point(rx, 0), color=RED))
        return dot_group

    def get_vertex_dot(self, axes, tracker, vx_expr, vy_expr):
        """返回顶点位置的 Dot"""
        b_val = tracker.get_value()
        vx = float(vx_expr.subs("b", b_val).evalf())
        vy = float(vy_expr.subs("b", b_val).evalf())
        return Dot(axes.coords_to_point(vx, vy), color=YELLOW)

    def get_info_text(self, tracker, delta_expr, x_sym, expr):
        """生成判别式与交点个数的信息文本"""
        b_val = tracker.get_value()
        delta_val = float(delta_expr.subs("b", b_val).evalf())
        # 判断实根个数(用 solve 求全部根,再筛实根)
        roots = sp.solve(expr.subs("b", b_val), x_sym)
        real_count = sum(1 for r in roots if abs(complex(r.evalf()).imag) < 1e-8)

        text1 = MathTex(
            f"\\Delta = {delta_val:.2f}",
            tex_to_color_map={f"\\Delta = {delta_val:.2f}": GREEN},
            font_size=24,
        )
        text2 = MathTex(
            f"\\text{{交点个数:}}{real_count}",
            tex_template=TexTemplateLibrary.ctex,
            font_size=24,
        )
        text = VGroup(text1, text2).arrange(RIGHT, buff=1).shift(UP)
        return text

关键点解释:

  • SymPy 提前准备好符号表达式,always_redraw 里只做数值代入 + 求值,保证运行流畅。
  • complex(r.evalf()).imag 判断虚部是否为 0,优雅地区分实根与复根,完全不用手动写条件分支。
  • 顶点坐标直接由 diff 推导,动画中总有一个黄色圆点稳稳跟随抛物线顶点。
  • 左上角文本实时显示判别式的值和交点个数,看一眼就能对应上「 Δ>0\Delta >0 Δ>0两个交点, Δ=0\Delta=0 Δ=0一个交点, Δ<0\Delta<0 Δ<0无交点」。

4. 效果展示说明

运行这个场景,你会看到:

  • 一根蓝色抛物线,开口向上(a=1),与 y 轴交于 2。
  • 随着 b-3 匀速滑到 3
    • 开始 b=-3 时,判别式 Δ=1>0\Delta=1>0 Δ=1>0,抛物线与 x 轴有两个红色交点。
    • 当 b 经过 −8≈−2.828 -\sqrt{8} \approx -2.828 −8 ≈−2.828时,两交点靠拢,重合为一个点 Δ=0\Delta=0 Δ=0),此时左上角显示「交点个数:1」。
    • 紧接着 Δ\Delta Δ变成负数,所有红点消失,抛物线悬浮在x 轴上方,与 x 轴无交点。
    • 当 b 跨越 8 \sqrt{8} 8 时,两点再次出现并逐渐远离。
  • 整个过程,黄色顶点 一直精准地落在抛物线最低点,随 b 移动而滑动。
  • 左上角的 Δ\Delta Δ 数值和交点个数同步刷新,完全不需要手动干预。

5. 小结

SymPyManim 动画里的角色很纯粹:把数学计算还给计算机,把视觉表达留给你。

手算求根公式、判断判别式、求导数零点......这些重复且易错的体力活,SymPy 一句 solve、一句 diff 就能完美代劳。

动画代码的逻辑因此变得清晰------你只负责告诉 Manim "什么东西应该画在什么位置",而"位置怎么算"就让 SymPy 这个符号大脑去完成。

相关推荐
新手村领路人2 小时前
在macos python中安装dlib
开发语言·python·macos
老码观察3 小时前
设计模式实战解读(十一):外观模式——给复杂系统套一层壳
python·设计模式·外观模式
ss2733 小时前
【Python实战】基于FastAPI的绿植养护管理系统 - 完整项目
python·fastapi
FreakStudio3 小时前
大话电容传感器和电容SOC芯片,看这一篇就够了
python·单片机·嵌入式·面向对象·并行计算·电子diy·电子计算机
love530love3 小时前
根治 PyTorch CUDA `pynvml` 弃用警告:直接修改 `torch/cuda/__init__.py` 的实践记录
人工智能·pytorch·windows·python·深度学习·机器学习·pynvml
程序员小远3 小时前
接口测试详解
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
CTA量化套保3 小时前
期货实盘委托成交持仓对不上:天勤排查顺序与字段对照
python
机汇五金_3 小时前
从钣金加工到成品装配,弱电箱是如何制造出来的?
网络·python·制造
键盘上的猫头鹰4 小时前
【Linux 基础教程(四)】文件内容查看、打包压缩与搜索、重定向管道及环境变量
linux·服务器·python