简介:面向计算物理与数值模拟学习者的FPU问题Python实现,代码基于非线性弹簧链模型,通过调节alpha与beta参数可分别引入二次、三次非线性项,用于观察能量在谐振子链中的迁移与回归现象。资源共6个文件,含fpu-simulation.py主程序、README说明文档、开源许可证与.gitignore配置,并附带gif与mp4两种格式的演示动画,便于快速预览默认参数下的模拟效果。压缩包整体约15.34MB,文件结构简洁,适合已掌握基础Python语法、希望动手复现经典物理实验的学生或研究人员直接运行与二次开发。目前已有262人学习下载。借助示例动画与文档中的依赖及avconv安装提示,读者可避开matplotlib版本与视频编码方面的常见坑点,更快得到非谐波力作用下的可视化结果。
1. FPU 问题是什么:一次没等到能量均分的模拟,成了非线性物理的起点
Fermi-Pasta-Ulam(FPU)问题,是计算物理史上一则被反复引用的"失败"实验:三位物理学家想用早期电子计算机模拟一条两端固定的非线性弹簧链,验证能量能否从单一模态流向全部模态,达成统计力学预言的能量均分。结果跑了远超预期的模拟时长,能量非但没有均匀铺开,反而周期性地回到初始模态,像是"后悔"了自己刚才的扩散。这个反直觉的结论就是 FPU 回归,也是孤立子研究的起点。这篇文章把它当作一道数值模拟题来拆:怎么在本地写代码复现"能量外泄→回流"的完整过程,并搞懂时间步长、非线性强度和模态分解到底在控制什么。适合正在学计算物理、非线性动力学,或者想用经典数值实验做论文开篇图的读者。
2. 从物理模型到可计算的差分方程:FPU 问题的数学骨架
FPU 问题看着简单,真动手第一步就容易卡住:该用哪个势能形式?α 非线性和 β 非线性有什么区别?要不要做无量纲化?我一般会先把链的势能写出来,再做一次归一化,后面所有参数才有讨论基准。这一节把模型、模态分解和积分算法三件事一次讲清,后面的代码才能落到物理上。
2.1 一维非线性晶格的运动方程:从哈密顿量到可执行差分
考虑一排 N 个质量为 m 的粒子,相邻粒子由弹簧连接,两端固定在第 0 和第 N+1 个位置上。粒子相对平衡位置的位移记为 y_i,固定边界即 y_0 = y_{N+1} = 0。线性链的势能是每个弹簧的 1/2 k (y_{i+1} - y_i)^2,FPU 问题在这之上加了平方或立方修正项。常见的无量纲化势能写法为:
V = Σ_{i=0}^{N} [ 0.5 * (y_{i+1}-y_i)^2 + α/3 * (y_{i+1}-y_i)^3 + β/4 * (y_{i+1}-y_i)^4 ]
其中已经取弹簧常数 k 和质量 m 都为 1。α 对应势能三次方项,叫 α-FPU;β 对应势能四次方项,叫 β-FPU。两者在弱非线性极限下描述相似的现象,但强非线性下行为很不一样。对第 i 个粒子取 -∂V/∂y_i,得到运动方程:
m * a_i = (y_{i+1} - 2 y_i + y_{i-1}) + α[(y_{i+1}-y_i)^2 - (y_i - y_{i-1})^2] + β[(y_{i+1}-y_i)^3 - (y_i - y_{i-1})^3]
线性项就是经典的拉普拉斯离散形式,非线性项则是左右两个弹簧弹性系数的差值造成的。α 取正值时弹簧是"变硬"还是"变软"取决于相对位移符号,这一点直接决定了模态间能量转移的方向。边界粒子 y_1 和 y_N 的方程里,y_0 和 y_{N+1} 恒为 0,代码里用拼接零向量的方式处理即可。
从力学直觉上讲,"弱非线性"并不是一个数学定义,而是相对线性项尺度而言。比如初始振幅 A 和 α 的乘积远小于 1 时,非线性只是微扰;而振幅大时,同一个 α 也能把系统推向混沌。因此写代码时重要的不只是 α 的值,还有初始条件里注入的总能量。我在做模拟时习惯把总能量也打出来,作为系统状态的第一指标。无量纲化后,线性色散关系的最大频率约为 2,这为后面选时间步长提供了明确上限;而最小基频约为 π/(N+1),决定了从初始模态泄漏能量需要多长的模拟时间。
2.2 模态坐标与能量分解:为什么用正弦模态而不是普通 FFT
FPU 实验要观察的是能量分配,所以必须把实空间的位移变换到模态空间。固定边界条件对应的线性本征函数是正弦函数:
u_k(j) = sin(π * k * j / (N+1)),j = 1, 2, ..., N
对每个模态 k,模态坐标 Q_k(t) = Σ_j y_j(t) * u_k(j),速度同理投影得到 P_k(t)。在做数值计算时,通常先构造一个 N×N 的模态矩阵 S,让 S[k, j] = sqrt(2/(N+1)) * sin(π * k * (j+1) / (N+1)),这样矩阵是正交归一的,S @ q 就得到所有 Q_k。线性极限下每个模态是独立谐振子,能量为:
E_k = 1/2 * (P_k^2 + ω_k^2 * Q_k^2)
其中模态频率为 ω_k = 2 * sin(k * π / (2*(N+1)))。这个色散关系是整条链动力学的时间标尺,也是后续所有时间参数换算的基础。
为什么要强调用正弦基而不是普通 FFT?因为普通 FFT 隐含周期边界条件,而 FPU 经典问题用的是固定边界。若拿 np.fft.rfft 的结果来算模态能量,谱会在两端出现对称镜像,能量分布看起来和真实模态坐标对不上,属于最容易踩的坑之一。固定边界的正确选择是离散正弦变换(DST-I),而手写投影矩阵几乎不可能出错,代价是 O(N²),对 N=64 或 128 来说足够快。
还有一个新手常忽略的点:模态能量的归一化。S 矩阵乘上 sqrt(2/(N+1)) 之后才是正交归一基,漏掉这个系数会让 Q_k 整体放大或缩小,模态能量的绝对数值出错。好在大多数物理结论只关心相对能量 E_k / E_total,归一化错误最多让曲线幅度整体偏移,不会改变回归的周期性;但如果你要精确对比总能量,就必须在每个时间步都做一次守恒检查。
2.3 辛积分器选型:为什么 Velocity-Verlet 是长时程模拟的默认答案
FPU 回归的特征时间通常是线性周期的几百到几千倍,要复现一个完整回归至少需要几十万步积分。这里选积分算法就非常关键。RK4 单步精度高,局部误差是 O(Δt⁵),但它是非辛算法,对哈密顿系统长时间积分会让总能量出现系统性漂移,表现为能量缓慢爬升或缓慢衰减,时间越长越明显。Velocity-Verlet 是辛积分器,它保的是相空间中的辛结构,长期能量只在初始值附近小幅度振荡,不会单向漂移。
Velocity-Verlet 的更新顺序是这样的:先用当前位置和加速度把位置推半个时间步,再在新的位置重新计算加速度,最后用新旧加速度的平均值更新速度。写成公式就是:
q_{n+1} = q_n + p_n * dt + 0.5 * a_n * dt² p_{n+1} = p_n + 0.5 * (a_n + a_{n+1}) * dt
注意这里 p 是速度还是动量取决于质量归一化,质量 m=1 时二者等价。时间步长的选择受线性最大频率约束,稳定的位移 Verlet 需要 ω_max * dt < 2,ω_max≈2 时 dt 上限约 1.0。但这是稳定性下限,不是精度下限。要在一个线性周期里采样足够多,我一般取 dt = 0.02 到 0.1,然后通过总能量相对误差来验收——相对误差在 1e-6 量级是合格线,到了 1e-3 就要警惕步长过大了。
用线性晶格自检是个好习惯:把 α 和 β 都设成 0,跑几千步,此时总能量应该几乎完全守恒,相对误差可以压到 1e-10 以下。如果这一步都过不去,先别急着讨论非线性现象,问题基本出在力函数或边界条件的实现上。非线性加进来之后,总能量不再严格守恒是正常的,但相对误差仍应保持在 1e-4 以下,超出这个量级就先缩小 dt。
3. 用 Python 复现 FPU 模拟:最小可运行代码与模态能量分析
这一章直接给出一套能跑的 Python 实现,包含参数设置、单模态初始条件、Velocity-Verlet 主循环、模态能量投影四个模块。我刻意不用任何第三方动力学库,只用 NumPy,这样每一步的物理意义都是透明的。整套代码按顺序拼进同一个脚本就可以运行。
3.1 构造单模态初始条件与模态矩阵
FPU 回归的前提是初始能量集中在一个模态上,通常是基频模态 k=1。如果初始条件包含多个模态,线性部分会各自振荡,非线性耦合的信号会被不同模态的拍频淹没,回归现象很难看清。推荐的做法是将链的位移铺成一条标准的半波正弦,速度设为零。
import numpy as np # 物理参数 N = 64 # 粒子数 alpha = 0.25 # 二次非线性系数(α-FPU) beta = 0.0 # 三次非线性系数(β-FPU) k_spring = 1.0 # 线性弹簧常数,已归一化 mass = 1.0 # 粒子质量,已归一化 # 数值参数 dt = 0.05 # 时间步长,远小于线性振动周期 total_time = 2000.0 # 总模拟时间,单位 1/ω0 steps = int(total_time / dt) # 初始条件:单模态激发 mode_k = 1 # 初始激发模态 amplitude = 1.0 # 初始位移振幅 j_arr = np.arange(1, N + 1, dtype=float) # 粒子坐标 1..N q = amplitude * np.sin(np.pi * mode_k * j_arr / (N + 1)) p = np.zeros(N)这里的 q 是长度为 N 的一维数组,每个元素代表一个粒子相对平衡位置的位移。第 0 和第 N+1 个固定点没有显式存进数组,而是由力函数里的边界拼接来处理。amplitude 取 1.0 配合 alpha=0.25,属于弱到中等非线性区;如果 amplitude 加到 3 以上,总能量提高两个量级,系统会更快进入强非线性甚至混沌区,那是第 4 章会讲的临界问题。
3.2 主循环:Velocity-Verlet 的力计算实现
力函数是整段代码最容易出错的位置。我采用一种稳妥的向量化写法:先把 q 两头补零得到扩展数组,再用 np.diff 得到所有相邻位移差,这样左右弹簧的位移差在数组里天然对齐,线性项和非线性项都可以一次性算完。
def fp_force(q, alpha, beta): ext = np.concatenate(([0.0], q, [0.0])) offset = np.diff(ext) # 相邻位移差,长度 N+1 left = offset[:-1] # 每个粒子左侧弹簧的伸长 right = offset[1:] # 每个粒子右侧弹簧的伸长 force = (right - left) + \ alpha * (right**2 - left**2) + \ beta * (right**3 - left**3) return force def run_simulation(q, p, dt, steps, alpha, beta): acc = fp_force(q, alpha, beta) / mass for step in range(steps): q = q + p * dt + 0.5 * acc * dt * dt new_acc = fp_force(q, alpha, beta) / mass p = p + 0.5 * (acc + new_acc) * dt acc = new_acc # 这里按需采样保存,下面会讲到采样点 return q, poffset 数组的长度是 N+1,索引 0 对应第一个固定锚点到粒子 1 的弹簧,索引 N 对应粒子 N 到最后一个固定锚点的弹簧。left 取 offset 的前 N 项,right 取后 N 项,正好对应每个粒子左右两根弹簧的伸长量。线性项 right - left 就是 y_{i+1} - 2 y_i + y_{i-1},非线性项则是左右弹簧弹性系数差异带来的额外力。这个写法比逐粒子显式循环快,也几乎不可能出现索引越界。
3.3 用矩阵投影计算模态能量
模态能量需要把实空间位移和速度投影到正弦模态上。我直接构造归一化模态矩阵 S,用矩阵乘法一次性完成所有模态的投影。频率数组由线性色散关系给出,注意这里的 k 从 1 开始,对应物理模态序号。
def modal_energy(q, p): k_idx = np.arange(1, N + 1, dtype=float) S = np.sqrt(2.0 / (N + 1)) * np.sin( np.pi * k_idx[:, None] * j_arr[None, :] / (N + 1) ) Q = S @ q P = S @ p omega = 2.0 * np.sin(np.pi * k_idx / (2.0 * (N + 1))) return 0.5 * (P**2 + omega**2 * Q**2) def total_energy(q, p, alpha, beta): ext = np.concatenate(([0.0], q, [0.0])) offset = np.diff(ext) kinetic = 0.5 * mass * np.sum(p**2) potential = np.sum(0.5 * offset**2 + alpha / 3.0 * offset**3 + beta / 4.0 * offset**4) return kinetic + potential矩阵 S 的行是模态,列是粒子。k_idx[:, None] 和 j_arr[None, :] 通过广播生成 N×N 矩阵,每一行对应一个模态的波形。omega 数组给出对应模态的线性振动频率,单位是归一化角频率。模态能量的本质上就是每个谐振子的机械能,由速度投影项的动能部分和位移投影项的势能部分组成。实际观察时,我一般把 E_k 除以总能量得到相对模态能量,这样不受总能量绝对大小影响。
3.4 推荐参数表与运行时长估算
不同目标对应不同参数组合,我常用的几组给在下面。N 决定频率谱的密集程度,太小谱太稀疏,太大模拟变慢;对经典结果复现,N=64 是性价比最高的选择。
| 参数 | 符号 | 典型值 | 说明 |
|---|---|---|---|
| 粒子数 | N | 32 ~ 128 | 64 最常用,谱足够密 |
| 二次非线性 | alpha | 0.0 / 0.1 / 0.25 | 弱非线性看回归 |
| 三次非线性 | beta | 0.0 或 1.0 | 强非线性时启用 |
| 时间步长 | dt | 0.02 ~ 0.1 | 先小步长跑通再加大 |
| 总时长 | total_time | 500 ~ 5000 | 需要覆盖多个线性周期 |
| 初始振幅 | amplitude | 0.1 ~ 3.0 | 控制注入总能量 |
| 初始模态 | mode_k | 1, 2, 5 | 单模态激发才见回归 |
| 采样间隔 | sample_rate | 每 100 步存一次 | 控制输出数据量 |
总时长和采样率要一起考虑。dt=0.05 时跑 2000 个时间单位即 40000 步,如果每步都记录则很快撑爆内存,所以要在循环里按步数取模采样,比如每 50 或 100 步存一次模态能量。模拟前先用线性自检测试力函数,再把非线性加上,这个顺序能帮你把数值实现和物理现象两类问题分开排查。
4. 模拟实验设计:从 FPU 回归到能量均分的临界
代码就绪后,下一步是设计能说明问题的实验。不要一上来就跑一大组参数,我建议按三条线走:弱非线性复现回归、增强非线性观察均分、改变初始模态对比扩散路径。每一条线都能在图上给出明确的判断标准。
4.1 弱非线性下的周期回归:一个可复现的判据
在 N=64、alpha=0.25、amplitude=1.0 的条件下,观察模态能量时间序列。你首先会看到初始模态 E_1 缓慢下降,能量泄漏到 k=2、3、4 等低阶模态,同时这些模态的能量呈波浪式上升。如果总时长足够长,E_1 会在某个时刻回到接近初始值的位置,其余模态能量几乎同时回落,这就是一次 FPU 回归。这个过程会反复发生,周期与非线性强度相关。
为了不再"凭肉眼"判断回归时刻,写一个自动检测函数:
def first_recurrence(E1_series, E1_init, threshold=0.97, sample_interval=50): for idx, e in enumerate(E1_series): if e >= threshold * E1_init: return idx * sample_interval * dt return None这里 threshold 取 0.97 表示 E_1 恢复到初始能量的 97%。由于 E_1 在振动,可能短暂穿越阈值后又掉下去,所以更稳妥的是要求连续若干个采样点都高于阈值,或用滑动窗口取最小值判断。我在实际使用中习惯把阈值提高到 0.99,并加上连续 10 个点的窗口,能显著减少误判。
为什么弱非线性下能量会"回流"?从动力学角度看,非线性项让模态之间产生耦合,但耦合强度不足以让能量在所有模态间形成统计意义上的扩散,系统在低维环面上运动,因此存在准周期性的振荡。这个现象推翻的是当时统计力学对孤立哈密顿系统"总会趋近能量均分"的朴素预期,也正是它让物理学家意识到有限自由度系统的不可积性会带来非常复杂的长期行为。
4.2 强非线性下的能量均分:多少非线性才算"够强"
把 alpha 从 0.25 提到 1.0,或者把 amplitude 从 1.0 提到 3.0,会观察到完全不同的图景:E_1 不再回到初始值,能量在各模态间持续扩散,谱熵稳步上升并趋近饱和。此时系统进入了强混沌区,模态间耦合足够强,有限时间平均后各模态能量接近均分。
判断阈值在哪里,最直观的工具是谱熵。定义第 k 模态的占比 p_k = E_k / E_total,谱熵为:
S = -Σ p_k * ln(p_k) / ln(N)
谱熵最大值是 1,对应完全均分;最小值是 0,对应能量集中在单一模态。严格来说谱熵最大值的准确值受 N 影响,所以固定 N=64 后,可以预先算一个理想均分谱熵值作为参考。强非线性下 S 逐渐爬升并在 0.8 以上趋平;弱非线性下 S 会呈周期性起伏,涨落越大,回归信号越清晰。用 S 曲线区分"回归"和"均分"比单看 E_1 可靠得多。
4.3 初始模态选择:激发 k=1 和 k=5 看到的不是同一台机器
把 mode_k 改为 5,重跑同一组参数,你会发现回归时间长短、能量扩散路径都变了。高模态的振动频率更高、空间波长更短,非线性耦合的强度感受也更强,因此通常更容易进入强非线性状态,回归的完整度也更差。另一个值得尝试的是 mode_k=2 或 3 的偶数模态,它们与某些低频模态之间的耦合结构不同,能量会先流向更集中的一小簇模态。
这类实验的意义在于提醒你:FPU 问题的结论不能简单概括为"能量不会均分"。它对初始条件、非线性强度和边界条件都高度敏感。做参数扫描时,我一般固定 N 和 dt,改变 alpha 或 amplitude,每一个参数点都记录"最终谱熵"和"首次回归时间是否出现",这样画出来的相图才是对系统行为更完整的描述。
5. FPU 模拟的五个常见坑与排查方法
这一章是跑题的血泪经验汇总。FPU 模拟代码量不大,绝大多数问题出在边界条件、归一化和时间尺度三个地方。每条我都按"现象 → 原因 → 解决"来写,方便你对照排查。
5.1 现象:总能量随时间单调爬升,几千步后直接发散
这是最吓人的错误之一。原因几乎是两类:一是时间步长 dt 过大,超过了线性最大频率的稳定性限制;二是力函数里边界没处理好,导致在边界粒子处出现了数值不守恒的虚假力。解决方法是先在 α=β=0 的线性链上跑总能量监测,如果线性情况也发散,就是力函数错了;如果线性正常、加上非线性才发散,优先减小 dt。我见过不少次是 dt=0.2 时线性链刚好稳定,加了非线性后能量爬坡,这是正常的非线性稳定性变差,把 dt 降到 0.02 后能量曲线立刻平了。
5.2 现象:跑了很长时间,模态能量时间序列全是直线
E_1 完全不动,或者几乎不变,通常说明初始条件没有落在单一模态上,或者模态投影用错了基。如果你用初始位移 q0 是随机数,能量天然分布在所有模态里,非线性耦合几乎不会产生可辨识的回归信号。解决方法是改用严格的单模态初始条件,并做一次投影检查:跑 0 步就把模态能量打出来,确认只有目标模态能量的占比在 99% 以上。另外还要检查 q 和 p 是否在同一个模态上,比如 q 是单模态但 p 设成正比于另一个模态的波形,会让动力学变得很复杂。
5.3 现象:模态能量谱出现左右对称的镜像
用普通 FFT 的 np.fft.rfft 算模态能量时,非对称激发出的谱会在正负频率各出现一份能量,看起来像镜像。这是因为 FFT 的基函数是周期边界下的复指数,而固定边界需要的是正弦基。虽然对某个具体算例,你可以在前 N/2 个频率上看到近似正确的分布,但频率和色散关系对不上,长时间动力学也会被污染的频率分量干扰。解决方法是改用文中第 3 节的 S 矩阵投影,它对应 DST-I 的归一化版本。换完后再验证 S @ S.T 是否等于单位矩阵,这一步可以放在代码开头做断言。
5.4 现象:FPU 回归一直不出现,能量直接进入混沌
这通常不是代码 bug,而是物理参数选得太强。alpha=0.25 配合 amplitude=5,或者 N 太小时,系统已经越过回归区进入均分区。FPU 回归在弱非线性下才明显,回归时间会随着非线性强度降低而急剧增长。如果你只是想看到经典现象,就用 alpha=0.1 到 0.25、amplitude=1.0 的组合;如果目标是研究回归的破坏过程,那再逐渐加大 alpha。另外,总时长太短也会导致看不到回归——我最早一次模拟只跑了 200 个时间单位,所有曲线都还处在能量泄漏的早期,误以为代码写错了,后来看过回归时间的量级才意识到模拟时长要按几千个时间单位来算。
5.5 现象:能量看起来守恒,但模态能量算出来是负的
模态能量出现负值,十有八九是 ω_k 的色散关系用错了。有人套用连续弦的 ω_k = πk/(N+1),和真实离散晶格的 ω_k = 2 sin(kπ/(2(N+1))) 相比,低速模态偏小、高速模态偏大,投影后势能项和动能项不匹配,就会算出负值。解决方法是先单独测试线性链:初始让单一模态有纯动能,模态能量应该恒定;再把初始换成纯位移,同样应该恒定。如果两种情况下模态能量都守恒,色散关系就基本正确。
6. 再往前一步:用谱熵做参数扫描,并向前沿问题收敛
复现回归只是第一步,接下来可以把谱熵当作核心观测器,系统地扫描非线性强度和初始振幅,画出系统行为的相图。扫描时固定 N=64、dt=0.05,让 alpha 在对数网格上取 0.01 到 2.0,每个点记录最终的谱熵 S 和是否存在首次回归。代码可以这样组织:
def spectral_entropy(E_modes): p = E_modes / E_modes.sum() p = p[p > 0] return -np.sum(p * np.log(p)) / np.log(len(p)) alpha_list = np.logspace(-2, 0.3, 20) for alpha in alpha_list: q0 = amplitude * np.sin(np.pi * mode_k * j_arr / (N + 1)) p0 = np.zeros(N) q_final, p_final = run_simulation(q0, p0, dt, steps, alpha, 0.0) E_final = modal_energy(q_final, p_final) S_final = spectral_entropy(E_final) print(f"alpha={alpha:.3f}, S_final={S_final:.3f}")跑完会看到谱熵从接近 0 逐渐爬升到接近 1,中间存在一个过渡区。这个过渡区对应 FPU 回归到能量均分的临界,也正是后续许多研究聚焦的地方:到底多强的非线性才能破坏低维环面结构?过渡区的标度行为是否与孤立子有关?你甚至可以在这个基础上尝试连续极限,把离散链的位移差当作近似导数,在长波极限下推导出 KdV 方程,这会解释为什么 FPU 回归和孤立子有内在联系——能量不是"消失"了,而是以孤立子这种局域化结构的形式重新聚合。
我自己的习惯是,任何一组新参数的模拟都要同时保存总能量相对误差、谱熵和初始模态能量三条曲线,并把模态矩阵正交性断言放在代码最前面。这样不管模拟结果多奇怪,都能快速定位是物理现象还是数值问题。FPU 模拟是个典型的"代码量少但陷阱全在细节里"的问题,把归一化、时间尺度和色散关系这三关守好,之后跑任何非线性晶格模拟都会顺手很多。希望帮到你。
本文还有配套的精品资源,点击获取