ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零实现FPU模拟:一维非线性晶格能量回归的数值实验

从零实现FPU模拟:一维非线性晶格能量回归的数值实验 简介基于 Python 的 Fermi-Pasta-UlamFPU问题模拟演示资源面向物理、非线性动力学与计算物理方向的本科生和研究人员既可用于课堂教学演示也可作为科研入门复现的参考。FPU 问题通过研究带非线性弹性项的线性链在振动中的能量分布揭示了弱非线性系统中能量并不迅速趋向热平衡的经典现象是非线性晶格动力学的基础算例。核心代码为 fpu-simulation.py基于 matplotlib 绘制质点运动与能量演化过程支持手动调整二次方alpha、三次方beta非线性系数令参数大于 0 即可引入非谐波力便于对比线性与非线性情形下的行为差异代码结构简洁、参数修改方便。压缩包共 6 个文件包含可直接运行的 Python 脚本、示例动画图片与视频gif/mp4、说明文档 README 及开源许可文件整体大小约 15.34MB。示例动画展示了 alpha0、beta0.3 时的演化过程画面直观流畅已有 262 人浏览学习适合需要快速复现 FPU 数值实验、理解晶格能量输运机制或作为教学演示素材的读者。 我第一次把FPU模拟跑出结果时对着屏幕上的曲线愣了挺久。做统计物理习题多年脑子里根深蒂固的想法是系统只要带一点非线性能量最终会在所有自由度之间摊平。可Fermi、Pasta和Ulam在1955年的数值实验给出一个反直觉现象——能量在少数模式间来回流动过一段时间还会整体回到初始形态。这个Fermi-Pasta-Ulam问题FPU问题后来成了非线性科学里绕不开的经典模拟题。这篇文章不打算泛泛讲历史我会直接带你从零写一个一维非线性晶格的FPU模拟怎么建立模型、怎么选择积分器、用什么量去观测能量分布以及实际跑起来会踩哪些坑。完整代码、参数组合和出图思路都会给出来新手可以直接照着跑已经懂一点原理的人也能从调试细节里拿走点东西。1. 先把物理图景讲清楚一维链、非线性耦合和能量均分1.1 一个弹簧链但弹簧不是线性的想象一排N个质量相同的小球用弹簧连成一串两端固定在墙上这是最经典的一维晶格模型。每个小球只在链的方向上运动用q_i表示第i个粒子偏离平衡位置的位移固定边界条件就是q_00、q_{N1}0。如果弹簧势能只保留到二阶项系统是完全线性的各个振动模式彼此独立、各搬各的课永远不会交换能量。FPU实验干的事情就是在势能里额外塞进非线性修正U Σ[ 1/2(q_{i1}-q_i)² α/3(q_{i1}-q_i)³ β/4(q_{i1}-q_i)⁴ ]只保留三次项叫FPU-α模型只保留四次项叫FPU-β模型。原版实验用的是α模型但我在实际模拟中更推荐β模型。原因很直接α模型的三次势能在位移差足够大时没有下界粒子可能被弹飞数值上不够稳β模型的四次项永远为正长时间跑起来让人放心得多。1.2 统计力学直觉和现实对不上这正是它的价值平衡态统计物理给出的预言很明确只要系统存在非线性相互作用在足够长的时间后能量应该均匀分布到所有简正模式上每个模式的平均能量基本相等。这个结论太自然了以至于很少有人去怀疑它。但FPU的数值实验把弱非线性放在了一个只有几十个粒子的链上初始把能量全部给到最低频的那个模式结果出乎所有人意料。能量没有铺平反而只在前几个低频模式之间来回流动经过一段时间还能整体回到初始状态。换句话说系统像是记得自己的初始条件而不是迅速变成一团热汤。这个现象动摇了非线性必然导致热化的朴素直觉。后来它牵出了KdV方程、孤子、低维系统热传导等一系列方向所以直到今天FPU问题仍然是检验数值方法和动力学理论的一块试金石。2. 模拟方案设计积分器、模式分解和初始化全攻略2.1 为什么选Verlet而不是Runge-KuttaFPU模拟要跑几十万步积分器的选择直接影响结果可信度。我的建议是用速度Verlet而不是教科书里更常讲的四阶Runge-Kutta。原因在于Verlet是辛积分格式它保持相空间体积长时间运行下总能量的漂移有界只是围绕初始值小幅震荡不会像RK4那样单调增长或衰减。Verlet的更新过程分为半步速度、全步位移、再半步速度v_{half} v 0.5·dt·a(q) q_new q v_{half}·dt a_new a(q_new) v_new v_{half} 0.5·dt·a_new这里的加速度a来自每个粒子收到的合力在固定边界下只需要在位移数组两端补零就能很自然地处理边界弹簧。对整个系统来说Verlet的编程量比RK4小稳定性却更好属于典型的做得少赚得多。2.2 把坐标换成简正模式用正弦基看能量分布想观察能量均分直接看每个粒子的位移意义不大得转到简正模式空间。固定边界条件下线性链的本征模式是sin函数所以对粒子位移做一个离散正弦变换Q_k(t) sqrt(2/(N1)) · Σ_{j1}^{N} q_j(t) · sin(j·k·π/(N1))k是从1到N的模式序号。第k个模式的线性能量近似为E_k(t) 1/2·[ (dQ_k/dt)² ω_k²·Q_k² ]其中ω_k 2·sin(k·π/(2(N1)))。注意速度项的变换和位置项完全一致因为我们用的同一个正交矩阵在位置和速度空间作用所以直接用粒子的速度做同样的正弦变换就能得到dQ_k/dt。这里有个细节要说明E_k只保留线性势能部分没有包含非线性修正项。弱非线性条件下这个近似足够好用观察能量在模式间的迁移完全不碍事。如果非要把非线性势能也分解到每个模式上数学上会复杂很多实际模拟中没必要。2.3 初始条件精确地把能量填进第一个模式FPU实验最经典的出发点是只激发最低频模式也就是让所有粒子的初始位移按第一个正弦模式分布初速度为零q_j(0) A·sin(π·j/(N1))由于正弦基的正交性这个位移只会激发k1这个模式。根据前面给的E_k表达式模式1的初始能量可以算出来E_1 1/4 · ω_1² · A² · (N1)所以给定目标能量E_target振幅A就是A sqrt(4·E_target / (ω_1²·(N1)))用这个公式直接控制初始能量比随手调振幅高效得多。我刚开始写代码时直接拍脑袋给A结果能量每次都和我预想差一大截后来老老实实推导了一遍才解决。这里补充一句初始时线性模式能量约等于总能量但因为势能里还有四次项严格的总能量会比E_target略大一点弱非线性下这点差别完全在可接受范围内。3. 可复现代码不到四十行核心搞定FPU模拟3.1 参数选区为什么从N32起步我给的参数组合是多次调试后挑出来的默认配置如下N32beta0.1alpha0.0E_target0.1dt0.05steps200000N选32而不是更大的64主要是想让现象明显、跑得快。32个粒子已经有足够多的模式而且低模式之间的能量流动在图上非常清楚。dt0.05的确定依据是最高线性频率约等于2每个周期能采到六十个点左右数值精度足够。如果一开始就在N64上调试模型本身没问题但回归周期会拉长单次实验跑起来也慢不利于快速验证代码。3.2 核心代码加速度、Verlet和模式分解完整代码分三段看。第一段是系统参数和初始条件import numpy as np N 32 alpha 0.0 beta 0.1 dt 0.05 steps 200000 E_target 0.1 j np.arange(1, N 1) omega1 2.0 * np.sin(np.pi / (2 * (N 1))) A np.sqrt(4.0 * E_target / (omega1**2 * (N 1))) q A * np.sin(np.pi * j / (N 1)) v np.zeros(N)第二段是加速度计算。做这个函数时最重要的就是边界处理我用补零数组把两端固定墙的位移看成0然后一次性计算每个粒子左右两段弹簧的伸长量def acceleration(q): q_ext np.zeros(N 2) q_ext[1:N1] q u q_ext[1:N1] - q_ext[:N] # q_i - q_{i-1} v q_ext[2:N2] - q_ext[1:N1] # q_{i1} - q_i a (v - u) alpha * (v**2 - u**2) beta * (v**3 - u**3) return a第三段是Verlet积分主循环配合模式能量提取。先构建正弦变换基矩阵再在循环里采样ks np.arange(1, N 1) basis np.zeros((N, N)) for k_idx in range(N): basis[:, k_idx] np.sqrt(2.0 / (N 1)) * np.sin(np.pi * j * (k_idx 1) / (N 1)) omegas np.array([2.0 * np.sin(np.pi * k / (2 * (N 1))) for k in ks]) mode_energy_history [] for step in range(steps): v_half v 0.5 * dt * acceleration(q) q_new q v_half * dt a_new acceleration(q_new) v_new v_half 0.5 * dt * a_new q, v q_new, v_new if step % 50 0: Q basis.T q P basis.T v mode_E 0.5 * (P**2 omegas**2 * Q**2) mode_energy_history.append(mode_E)这个循环跑完mode_energy_history里每一行就是某个时刻全部N个模式的线性能量。实际运行时间取决于机器我这边大概几十秒到一两分钟完全可以接受。3.3 三张图看懂FPU回归拿到mode_energy_history后我最推荐画三张图。第一张是时间-模式热图横轴是时间纵轴是模式序号k颜色用log10(E_k)表示。这张图能一眼看出能量到底在哪些模式之间流动有没有往高模式扩散的趋势。第二张是把E1、E2、E3、E4四条曲线画在同一个折线图里。FPU回归现象在这张图上最直观你能清楚看到能量从第一模式漏出去过了某个时刻又涌回来呈现明显的周期性。第三张是取三个关键时刻初始时刻、能量铺散到最大时、回归时刻的模式能量快照对比E_k的分布形态。这个图能帮你理解所谓热化其实并不是均匀分布而是能量停留在前几个低模式里来回震荡。4. 结果长什么样从看起来热化到突然回归4.1 前期的热化假象跑完beta0.1的参数组合你会看到模拟初期E1下降得很快紧接着E2、E3、E4被依次激发高模式基本不动。如果只看最早这一段你很容易得出结论系统正在热化。这正是FPU问题最迷惑人的地方。很多人在这个阶段就停了以为数值实验不过是验证了统计力学预期。但请继续跑下去因为真正关键的剧情还没上演。4.2 长时间尺度上能量回来了继续跑下去到了某个特征时间之后能量会集中回到前几个模式甚至在初始模式附近重新聚拢。这种近乎周期性的回归就是FPU回归。注意它不是严格可逆的过程中间经历了复杂的非线性相互作用但宏观上确实呈现出回到初始状态的趋势。你可以做个对比实验把β从0.1降到0.05回归时间会明显变长把β调到0.2回归来得更早但同时高模式也被激发得更充分热化迹象更强。这个依赖关系说明非线性强度不是简单的干扰项而是决定能量输运速度的核心参数。4.3 真正的热化分界在哪在N32、beta0.1这样的弱非线性条件下即使能量铺开得最充分的时刻高模式能量也远低于几个低模式分布远谈不上均匀。也就是说FPU问题里的假热化和真正的热力学平衡不是一回事。真正的能量均分需要把非线性强度推到更大、粒子数推到更多或者模拟时间拉到足够长。搞清从回归到热化之间那条分界在哪里正是FPU研究几十年来的核心问题。对普通模拟来说记住一句话就够了看到前期能量铺设不等于看到热化多跑两个量级的时间再下结论。5. 常见问题排查与参数调节速查5.1 总能量快速漂移或发散最可能的原因是dt太大。最大线性频率约等于2从数值稳定性角度dt需要满足dt·ω_max远小于1我建议dt不要超过0.1。如果β设得比较大比如超过0.5或者初始能量较高非线性力贡献显著更要把dt压到0.01到0.05之间。每次跑模拟都应该顺带监测总能量。完整的总能量包含动能和所有N1段弹簧的势能两端边界弹簧也不能漏def total_energy(q, v): q_ext np.zeros(N 2) q_ext[1:N1] q d np.diff(q_ext) T 0.5 * np.sum(v * v) V 0.5 * np.sum(d**2) alpha / 3.0 * np.sum(d**3) beta / 4.0 * np.sum(d**4) return T V长期模拟中如果总能量相对漂移超过千分之一的量级先减小dt再重新跑。能量漂移是积分步长不够细的最直接信号别指望换更高阶方法能根治。5.2 模式能量抖动的两个隐藏原因模式能量出现高频抖动第一个原因是位置Verlet和速度Verlet混用导致的速度定义不一致。如果你在循环里只保存位置然后用(q_new - q_prev)/(2dt)算速度那模式能量的速度项自然会有抖动。用速度Verlet直接拿v来做变换就能消除这个问题。第二个原因是正弦基的归一化系数搞错。这个函数的口径特别多有的实现用sqrt(2/(N1))有的用sqrt(1/(N1))还有的干脆不归一化。排查办法很简单把基矩阵和它的转置乘一下看是不是接近单位阵。不是的话模式序号和能量大小都会出错但表面上又看不出明显异样属于最隐蔽的bug。5.3 参数速查表参数推荐区间作用备注N16~64模式数量越大回归周期越长dt0.01~0.1积分步长盯紧总能量漂移beta0.01~0.5非线性强度太大直接热化E_target0.01~1.0初始能量与beta共同决定回归时间steps5万~50万模拟长度没看到回归就加长如果你只是想复现现象直接抄第一组推荐配置N32beta0.1dt0.05E_target0.1steps200000。跑完之后把E1到E4的曲线画出来FPU回归基本能让每个人都看明白。我自己折腾FPU最大的体会是选对一个观测量比盲目堆粒子数更有用。这个题目当年能颠覆直觉不是因为它用了多少粒子而是因为它找到了一种特别干净的观察方式——简正模式。做模拟也一样先把模型、积分器、观测量的逻辑理清楚再上手改代码比对着参数瞎试高效得多。最后分享一个小技巧先在N16的小系统上把代码跑通再放大到N32甚至N64。小系统回归快、现象明显也更容易定位数值bug。我在N16上把Verlet和模式分解调试利索之后切到N64基本就没再遇到过程序层面的问题。本文还有配套的精品资源点击获取
返回列表