惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
Lohrmann on Cybersecurity
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recorded Future
Recorded Future
S
Schneier on Security
I
Intezer
Latest news
Latest news
N
News and Events Feed by Topic
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
量子位
博客园 - 【当耐特】
S
Security @ Cisco Blogs
Google Online Security Blog
Google Online Security Blog
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
NISL@THU
NISL@THU
The Cloudflare Blog
李成银的技术随笔
T
ThreatConnect
L
LINUX DO - 最新话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
有赞技术团队
有赞技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Jina AI
Jina AI
T
Tor Project blog
The Hacker News
The Hacker News
人人都是产品经理
人人都是产品经理
小众软件
小众软件
S
Security Archives - TechRepublic
美团技术团队
博客园 - Franky
Security Latest
Security Latest
J
Java Code Geeks
P
Proofpoint News Feed
V
V2EX
The GitHub Blog
The GitHub Blog
WordPress大学
WordPress大学
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Help Net Security
PCI Perspectives
PCI Perspectives
Cyberwarzone
Cyberwarzone
Hugging Face - Blog
Hugging Face - Blog
N
Netflix TechBlog - Medium
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
SecWiki News
SecWiki News
腾讯CDC
爱范儿
爱范儿
D
Docker

掘金

juejin.cn juejin.cn juejin.cn Transformer 原论文怎么训出来的:8 张 P100、12 小时、warmup 4000 步 Hermes 升级后,我的 Telegram 附件突然发不出来了 Transformer 中的前馈网络:那个看似平平无奇的两层 MLP,其实是「记忆」所在 juejin.cn 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (1)---基础 juejin.cn juejin.cn juejin.cn 我让 AI 加了一个开关,结果代码走了原本不该走的分支 我也该升级了,陪伴了我7年的博客 juejin.cn juejin.cn MCP 高德地图实战:当 AI 学会使用工具,一个协议如何重塑大模型的行动边界 用魔法打败魔法:我让AI替我去面试前端岗,AI面试官给我打了92分,还发了offer juejin.cn juejin.cn juejin.cn juejin.cn Android Input Spy Window Claude Code CLI 命令大全:60 个原生命令一次讲清 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn 关于一个新手小白靠claude帮助下的全栈留言板项目开发 juejin.cn juejin.cn juejin.cn 从本地开发到生产部署:用 Docker Compose 跑通 NestJS、MySQL 与 Milvus AI应用开发七:可以替代 RAG 的技术 juejin.cn 小书匠:一款本地优先、去中心化的全能笔记软件 juejin.cn juejin.cn juejin.cn Shadow实战接入与生产落地:从零搭建到稳定运行 Shadow Transform:编译期的魔法——字节码替换实战 juejin.cn juejin.cn Hermes Agent:一个真正“会成长”的开源 AI Agent,正在改变 AI 自动化玩法 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn CryptoJS:数据安全的JavaScript加密利器 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn ArkClaw AI 盯盘管家 —— 从手动口令到自动推送,4 套预置定时任务模版一键启用 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn “杀!杀!杀!”、“我最讨厌事后道歉”——骂“杀哥”之前,谁还没当过情绪崩溃的人 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn Crawlee StagehandCrawler:自然语言点 Load More 的工程化爬虫 juejin.cn juejin.cn juejin.cn juejin.cn 人人都在鼓吹的OPC,我想给你泼盆冷水 juejin.cn juejin.cn juejin.cn Redis内存用爆了,原来我们都忽略了这个配置 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn
Manim物理模拟:别自己写欧拉了!
databook · 2026-05-25 · via 掘金

做物理模拟动画时,我遇到过一个坑。

当时想做一个弹簧振子的 Manim 动画:一个小球连接在弹簧上,在平衡位置附近往复振动。

我一开始的思路是——手动写欧拉法迭代

# 当时写的“玩具级”数值积分代码
x = 1.0   # 初始位移
v = 0.0   # 初始速度
dt = 0.02 # 时间步长
k = 2.0   # 弹簧劲度系数
m = 1.0   # 质量

positions = []
for i in range(300):
    a = -k/m * x          # 加速度
    v += a * dt           # 更新速度
    x += v * dt           # 更新位置
    positions.append(x)

这段代码跑起来之后,小球确实动起来了。但看了几秒之后——小球越振幅度越大,能量明显不守恒。

欧拉法的数值误差在逐帧累积,像个隐形的外力在不断推着小球。

我当然可以换龙格-库塔法,但那意味着更复杂的代码、更长的调试时间。我只是想做一个弹簧振子动画,为什么要从数值分析开始写起?

直到我开始用 SymPydsolve,才发现原来我根本不需要自己写数值积分。

1. 痛点场景还原

Manim 中做物理模拟动画,常见的“手工数值积分”方案有三大痛点:

痛点具体表现
数值误差累积欧拉法能量不守恒,弹簧振子越振越离谱
步长调参折磨dt 设太小渲染慢,设太大精度炸,反复试错
无法利用解析解明明简谐振子有精确的 cos(ωt) 解析表达式,却硬要用数值去逼近

更关键的是,Manim 的动画是基于 ValueTracker 的时间驱动的

我们真正需要的是一个函数 x(t),能根据当前时刻 t 精确返回小球的位置。

这和 SymPy 的解析解简直是天作之合。

# Manim 动画的本质:需要一个时间→位置的映射函数
def get_position(t):
    # 如果这里能直接用解析解,该多好!
    return ???  

SymPy 的 dsolve 可以帮我们求出这个解析的 x(t),然后通过 lambdify 把它转成 NumPy 函数,直接注入 ValueTracker 的更新逻辑中。全程无误差累积,因为每一帧的位置都是独立计算的。

2. SymPy 解决方案

2.1 dsolve:符号求解微分方程

SymPy 的 dsolve 可以求解常微分方程(ODE)的解析解。以弹簧振子(简谐振动)为例:

运动方程:md2xdt2+kx=0m\frac{d^2x}{dt^2} + kx = 0,即 d2xdt2+ω02x=0\frac{d^2x}{dt^2} + \omega_0^2 x = 0,其中 ω0=km\omega_0 = \sqrt{\frac{k}{m}}是系统的固有角频率

import sympy as sp

# 定义符号
t = sp.Symbol('t', real=True)           # 时间
x = sp.Function('x')(t)                 # 位移函数 x(t)
m, k = sp.symbols('m k', positive=True)  # 质量和劲度系数
omega = sp.sqrt(k/m)                     # 角频率

# 定义微分方程:m * x'' + k * x = 0
ode = sp.Eq(m * sp.diff(x, t, 2) + k * x, 0)

# 代入初始条件求解
# 设初始位移 x(0) = 1,初始速度 x'(0) = 0
initial_conditions = {
    x.subs(t, 0): 1,                # x(0) = 1
    sp.diff(x, t).subs(t, 0): 0     # x'(0) = 0
}

# dsolve 求解!
solution = sp.dsolve(ode, ics=initial_conditions)
print(solution)  # 输出:x(t) = cos(√(k/m)·t)

SymPy 直接给出了解析解:x(t)=cos(ωt)x(t) =\cos(\omega t),这就是我们熟知的简谐振动公式。

2.2 lambdify:符号表达式 → 高性能数值函数

有了符号表达式 x(t)=cos(ωt)x(t) =\cos(\omega t),下一步是把它变成 Manim 可以每秒调用几十次的快速函数。lambdify 就是干这个的:

import numpy as np

# 提取解表达式右侧的 x(t)
x_expr = solution.rhs  # cos(sqrt(k/m)*t)

# 代入具体参数值:k=4, m=1 → ω=2
x_expr_sub = x_expr.subs({k: 4, m: 1})  # cos(2*t)

# lambdify:将 SymPy 表达式编译为 NumPy 函数
# 参数是 t,返回 NumPy 数组
x_func = sp.lambdify(t, x_expr_sub, modules='numpy')

# 现在 x_func 可以像普通 NumPy 函数一样使用
print(x_func(0))      # 1.0
print(x_func(np.pi/4)) # cos(π/2) ≈ 0.0
print(x_func(np.pi/2)) # cos(π) = -1.0

lambdify 的三个关键优势

  1. 速度快:底层转成 NumPy 运算,支持向量化,比 SymPy 的 .subs() 快几个数量级
  2. 无缝衔接:返回的就是标准 Python 可调用对象,直接用在任何需要函数的地方
  3. 支持数组输入:可以一次计算整段时间的所有位置,便于做轨迹预览

2.3 扩展到阻尼振动和单摆

对于更复杂的微分方程,dsolve + lambdify 的组合依然好用:

欠阻尼振动d2xdt2+2βdxdt+ω02x=0\frac{d^2x}{dt^2} + 2\beta\frac{dx}{dt} + \omega_0^2 x = 0):

beta, omega0 = sp.symbols('beta omega0', positive=True)

# 欠阻尼方程
ode_damped = sp.Eq(sp.diff(x, t, 2) + 2*beta*sp.diff(x, t) + omega0**2*x, 0)
sol_damped = sp.dsolve(ode_damped, ics={
    x.subs(t, 0): 1,
    sp.diff(x, t).subs(t, 0): 0
})

# 代入参数后 lambdify
x_damped_expr = sol_damped.rhs.subs({beta: 0.3, omega0: 2})
x_damped_func = sp.lambdify(t, x_damped_expr, 'numpy')

小角度单摆θ+gLsin(θ)=0\theta'' + \frac{g}{L}\sin(\theta) = 0,小角度近似sin(θ)θ\sin(\theta) \approx \theta):

g_val, L_val = 9.8, 2.0
omega = sp.sqrt(g_val/L_val)  # 角频率 ω = √(g/L)

# ========== 直接写出通解形式 ==========
# 小角度近似后的方程:θ'' + ω²θ = 0
# 通解:θ(t) = A·cos(ωt) + B·sin(ωt)

A, B = sp.symbols('A B')  # 待定系数
theta_expr = A * sp.cos(omega * t) + B * sp.sin(omega * t)

# ========== 手动代入初始条件 ==========
theta_0 = sp.pi / 6          # 初始角度 30°
dtheta_expr = sp.diff(theta_expr, t)  # 角速度表达式

# 条件1:θ(0) = π/6  →  A = π/6
eq1 = sp.Eq(theta_expr.subs(t, 0), theta_0)

# 条件2:θ'(0) = 0  →  ω·B = 0  →  B = 0
eq2 = sp.Eq(dtheta_expr.subs(t, 0), 0)

# 求解待定系数
solution = sp.solve([eq1, eq2], [A, B])
print(f"A = {solution[A]}, B = {solution[B]}")

# 代入得到精确解
theta_final = theta_expr.subs(solution)
print(f"θ(t) = {theta_final}")
# 输出:θ(t) = pi*cos(√(g/L)*t)/6

# ========== lambdify 转为 NumPy 函数 ==========
theta_func = sp.lambdify(t, theta_final, 'numpy')

3. Manim 联动实战

下面是一个弹簧振子动画的核心代码示例。

核心思路是:用 SymPy 解出 x(t),用 lambdify 转成快速函数,再通过 ValueTracker 的追踪器模式驱动小球运动

from manim import *
import sympy as sp
import numpy as np

class SpringOscillator(Scene):
    def construct(self):
        # ========== SymPy 符号求解微分方程 ==========
        t_sym = sp.Symbol("t", real=True)
        m_sym, k_sym = sp.symbols("m k", positive=True)

        # 弹簧振子方程:m·x'' + k·x = 0  →  通解 x(t) = A·cos(ωt) + B·sin(ωt)
        A, B = sp.symbols("A B")
        omega = sp.sqrt(k_sym / m_sym)
        x_expr = A * sp.cos(omega * t_sym) + B * sp.sin(omega * t_sym)

        # 手动代入初始条件:x(0)=2, x'(0)=0
        eq1 = sp.Eq(x_expr.subs(t_sym, 0), 2)              # A = 2
        eq2 = sp.Eq(sp.diff(x_expr, t_sym).subs(t_sym, 0), 0)  # ω·B = 0
        sol_const = sp.solve([eq1, eq2], [A, B])

        # 代入参数:m=1, k=4 → ω=2 → x(t) = 2cos(2t)
        x_final = x_expr.subs(sol_const).subs({m_sym: 1, k_sym: 4})
        x_func = sp.lambdify(t_sym, x_final, "numpy")  # 编译为 NumPy 函数

        # ========== Manim 场景搭建 ==========
        number_line = NumberLine(x_range=[-3, 3, 1], length=8).shift(DOWN * 1.5)
        self.play(Create(number_line))

        fixed_point = number_line.number_to_point(-3) + UP * 0.8
        ball = Circle(radius=0.2, color=RED, fill_opacity=0.8)
        ball.move_to(number_line.number_to_point(x_func(0)) + UP * 0.8)

        self.play(DrawBorderThenFill(ball))

        # ========== ValueTracker 驱动动画(核心机制)==========
        time_tracker = ValueTracker(0)  # 虚拟时钟

        # 更新器:每帧用 SymPy 解析解计算精确位置
        ball.add_updater(lambda m: m.set_x(
            number_line.number_to_point(
                x_func(time_tracker.get_value())  # x(t) 精确值!
            )[0]
        ))

        # 播放:虚拟时间从 0 流逝到 5 秒
        self.play(
            time_tracker.animate.set_value(5),
            run_time=5,
            rate_func=linear,
        )
        self.wait(1)

4. 效果展示说明

运行上面的脚本,你会看到这样的动画流程:

观察重点

  • 小球的运动完全符合余弦规律:在两端停顿、中间最快,完美再现简谐振动
  • 弹簧长度随小球位置实时变化,视觉上非常自然
  • 整个过程没有任何数值积分代码——你写的只是方程和初始条件,剩下的交给 SymPy

换成阻尼振动有多容易?

只需要改两行代码:

# 阻尼振动方程
ode_damped = sp.Eq(
    sp.diff(x_sym, t_sym, 2) + 0.6*sp.diff(x_sym, t_sym) + 4*x_sym, 0
)
# dsolve 会自动处理,lambdify 之后的用法完全一样

小球的振幅会逐渐减小,直到停在平衡位置。而这一切,你不需要修改任何动画逻辑——因为 x_func 这个黑盒函数的内部实现已经被 SymPy 替换了。

5. 本期小结

核心公式dsolve → lambdify → ValueTracker

步骤工具作用
1. 定义微分方程sp.Eq(sp.diff(...))用符号表达物理规律
2. 求解析解sp.dsolve(ode, ics=...)得出 x(t) 的封闭表达式
3. 编译为快函数sp.lambdify(t, expr, 'numpy')符号表达式 → 每秒可调用千次的 NumPy 函数
4. 驱动 Manim 动画ValueTracker + add_updater每帧把虚拟时间 t 喂给函数,更新物体位置