简介:分步傅里叶法求解非线性薛定谔方程(NLS)的源代码解析文档,适合学习非线性光纤光学、光脉冲传输仿真以及数值求解方法的读者使用。文档聚焦科研与教学中的经典问题:如何用分步傅里叶法模拟色散与非线性共同作用下的脉冲演化。内容覆盖 NLS 方程建模、非线性步与色散步的拆分原理、傅里叶变换实现,以及 sech 脉冲和超高斯脉冲的参数设置与 chirp 影响。资源包整体小巧,仅 1 个 docx 文件、13KB,内含完整 MATLAB 源代码与逐段注释,便于对照运行和迁移修改。已有 1369 人学习下载,适合本科高年级、研究生及光通信方向初级研究者快速上手。通过该文档,读者可以掌握从方程离散、频域相移因子设计到结果可视化的完整流程,理解分步傅里叶法在孤子传播、非线性频谱演化等场景中的实际运用,是一份兼顾理论与代码实践的轻量参考资料。
1. 分步傅里叶法解NLS方程的源代码:核心循环不到二十行,凭什么成为孤子模拟的默认方案
分步傅里叶法解NLS方程的源代码,核心其实不到二十行。非线性薛定谔方程(NLS)是光脉冲在光纤中传播、玻色-爱因斯坦凝聚、水波调制不稳定性背后共用的主方程,而分步傅里叶法(Split-Step Fourier Method)是求解它最常用也最容易被低估的数值方案:把传播拆成线性衍射/色散和非线性相位两步,前者用 FFT 在频域精确对角化,后者在时域逐点相乘。这个思路让一段能稳定跑出孤子演化图的代码短到让人意外,也正因为短,参数怎么配、边界怎么处理、步长怎么选,全藏在细节里。这篇笔记从数学拆解讲到可直接运行的 Python 源代码,再列出五个踩坑记录,最后给到自适应步长和 GPU 加速的升级路径。适合要拿这套代码做课题仿真、写论文、复现理论结果的从业者,新手能照步骤跑通,熟手能在这看到边界与取舍。
2. 分步傅里叶法的数学拆解:线性步、非线性步与对称分裂为什么是二阶精度
2.1 NLS方程的标准形式与参数物理意义:β2、γ、α 分别管什么
先约定本文使用的方程形式,这是所有源代码的地基。考虑沿光纤(或一般色散介质)传播的复包络 u(z,t),用随脉冲移动的延迟时间坐标系,NLS 方程写成:
∂u/∂z = i(β2/2)·∂²u/∂t² + iγ|u|²u − (α/2)u
其中 z 是传播距离,t 是延迟时间。这个形式的好处是线性部分在频域里是精确的对角乘子,非线性部分在时域里是逐点乘子,正好对应分步傅里叶法的两个半步。
| 参数 | 物理含义 | 常见取值取向 | 说明 |
|---|---|---|---|
| β2 | 二阶色散系数 | 反常色散取负值(如 −1) | 负值为聚焦型 NLS,支持孤子解 |
| γ | 克尔非线性系数 | 正值 | 自相位调制强度,与 |
| α | 线性损耗 | 无源仿真取 0 | 有损耗时每公里扣功率 |
| P0 | 初始峰值功率 | 归一化后常取 1 | 决定非线性长度 |
| T0 | 脉冲宽度标度 | 归一化后常取 1 | 决定色散长度 |
我在给初学者讲这段时通常强调一句话:分步傅里叶法只是在解一个“线性导数算子 + 乘法算子”的演化方程,物理内容全在系数里。β2 的正负决定你看到的是光孤子还是脉冲迅速展宽,γ 和 P0 的乘积决定非线性相移的积累速度,α 则老老实实管损耗。很多翻车现场不是算法错了,是这三个系数的符号或量纲先错了。
2.2 对称分步格式推导:从一阶算子分裂到消灭 h² 误差
把方程写成算子形式:u_z = (D + N)u,其中 D = i(β2/2)∂²/∂t² − α/2 是线性微分算子,N = iγ|u|² 是一个依赖 u 的乘法算子(严格说 N 随传播变化,这就是 D 和 N 不对易的根本原因)。形式上 u(z+h) = exp(h(D+N))u(z),但 D 和 N 不对易,没法直接算指数。
最常见的做法是算子分裂。一阶格式是u(z+h) ≈ exp(hD)exp(hN)u(z):先走一整步线性,再走一整步非线性。用 Baker-Campbell-Hausdorff 公式展开,exp(hD)exp(hN) 与 exp(h(D+N)) 的差首项是 [D,N]·h²/2,也就是说局部误差是 O(h²),整体误差是 O(h)。
对称分步格式则是把线性步拆成两个半步行进:
u(z+h) ≈ exp(hD/2) · exp(hN) · exp(hD/2) · u(z)
BCH 展开后,h² 项被对称结构抵消,局部误差降到 O(h³),整体误差升到二阶 O(h²)。代价只是每个传播步多一次 FFT,换来的是同样步长下精度大幅提升。这个格式在量子力学、光学、流体数值计算里都叫对称分裂或 Strang 分裂,本质是同一件事。
实现上,exp(hD/2) 作用在频域很简单:D 在傅里叶域是乘子 D̂(ω) = −iβ2ω²/2 − α/2,所以exp(hD/2)就是频域里逐点乘exp((−iβ2ω²/2 − α/2)·h/2)。而 exp(hN) 作用在时域,是逐点乘exp(iγ|u(z)|²h)。这里有一个很多人误解的点:非线性步里的 |u|² 用的是当前步起点处的场强,不是步长中间的值,这恰恰是对称格式把线性步拆到两边的原因——尽量让非线性相移作用在“线性演化到中间”的场上,减少 D 与 N 不对易带来的误差。
2.3 离散化约定:FFT 下标、时间窗口与周期性边界条件
把连续方程离散化之前,必须先想清楚 FFT 的三个约定,否则后面全是玄学报错。第一,时间窗口是 2·Twin,采样点数为 N,时间间隔 dt = 2·Twin/N。第二,FFT 隐含信号在窗口上周期延拓,下标 0 对应 t = −Twin,而不是 t = 0,频域下标 0 对应频率 0。第三,np.fft.fftfreq 返回的频率排列是 0, 1/(N·dt), … 一直到奈奎斯特频率 ±1/(2dt),再翻转到负频。
我在代码里统一这样构造下标,避免后续所有错位问题:
import numpy as np N = 2**12 # 采样点数,取 2 的幂 Twin = 40.0 # 时间窗口半宽 dt = 2.0 * Twin / N # 时间采样间隔 t = (np.arange(N) - N / 2) * dt # 时域坐标,中心为 0 omega = 2.0 * np.pi * np.fft.fftfreq(N, dt) # 角频率坐标这里t与omega的配对关系是:时域数组第 N/2 个点对应 t = 0,正好对应频域数组第 0 个点(零频)。如果用np.linspace(-Twin, Twin, N)构造时间轴,实际间隔是 2·Twin/(N−1),与 fftfreq 假定的 dt 不一致,色散乘子就会系统性偏大,这是后面 5.4 节要展开讲的经典坑。
另一个必须接受的现实是周期性边界条件:FFT 等价于把信号当成周期函数,脉冲展宽碰到窗口边缘后会从另一侧“钻回来”。这不是 bug,是 FFT 的固有属性,所以时间窗口必须留足余量。奈奎斯特频率 f_max = 1/(2·dt) 决定了能表示的光谱上限,脉冲在传播中因非线性产生的新的高频成分如果超过它,就会混叠回低频,出现锯齿状伪结构。这两条是后面第四章参数设置和第五章排查的基础。
3. 用 Python 写分步傅里叶法源代码:从孤子初始条件到可运行的最小实现
3.1 参数定义与初始条件:一阶孤子 sech 脉冲怎么构造
写代码先定基准测试。一阶孤子是分步傅里叶法最好的验证用例:在 β2 = −1、γ = 1、P0 = 1 的归一化条件下,初始场 u(0,t) = sech(t) 应当以 sech(t)·exp(i·z/2) 的形式无畸变传播,峰值功率恒为 1,只是相位在转。这个解析解够简单,任何数值错误都能被它逼出来。
import numpy as np from numpy.fft import fft, ifft # ===== 物理参数(孤子单位制)===== beta2 = -1.0 # 反常色散,聚焦型 NLS gamma = 1.0 # 非线性系数 alpha = 0.0 # 线性损耗,验证阶段必须为 0 # ===== 离散网格 ===== N = 2**12 Twin = 40.0 dt = 2.0 * Twin / N t = (np.arange(N) - N / 2) * dt omega = 2.0 * np.pi * np.fft.fftfreq(N, dt) # ===== 一阶孤子初始条件 ===== P0 = 1.0 u0 = np.sqrt(P0) / np.cosh(t) # sech 脉冲,峰值场强 1 u = u0.copy() # ===== 传播参数 ===== Lz = 10.0 # 总传播距离 dz = 0.01 # 步长 nsteps = int(Lz / dz)这段代码的每个参数都能直接对上 2.1 节的物理量。sech 脉冲是孤子方程的本征解形状,np.sqrt(P0)/np.cosh(t)保证了峰值功率恰好是 P0。如果你做的是普通色散展宽实验,把初始条件换成高斯脉冲更贴近工程场景:
C = 0.5 # 啁啾参数,正值为前沿低频 T0 = 1.0 u0 = np.sqrt(P0) * np.exp(-(t / T0)**2 / 2) * np.exp(1j * C * (t / T0)**2 / 2)啁啾项exp(iC(t/T0)²/2)是模拟通信系统中真实光源的常见做法,它让脉冲在频域出现线性频率调制,传播中先压缩后展宽。验证代码用 sech,工程代码用高斯加啁啾,这两套初始条件建议都备着。
3.2 核心迭代循环:线性半步、非线性步、线性半步的完整代码
核心循环就是按 2.2 节的对称分裂格式推进,整套代码短到让人怀疑它在正经工作:
# ===== 线性半步算子预计算(频域),包含损耗 ===== lin_half = np.exp(1j * beta2 * omega**2 * dz / 4 - alpha * dz / 4) # ===== 记录演化过程 ===== z_record = [0.0] u_record = [u0.copy()] for n in range(nsteps): # 前半线性步:频域乘算子,IFFT 回时域 u = ifft(lin_half * fft(u)) # 非线性步:时域逐点乘,用当前 |u|² 计算相位 u = u * np.exp(1j * gamma * np.abs(u)**2 * dz) # 后半线性步:再走一次频域半步行进 u = ifft(lin_half * fft(u)) # 每 100 步记录一次快照 if (n + 1) % 100 == 0: z_record.append((n + 1) * dz) u_record.append(u.copy())注意lin_half指数里的系数。半步行进距离是 dz/2,线性算子 D̂(ω) = −iβ2ω²/2 − α/2,所以整段指数是(−iβ2ω²/2 − α/2)·(dz/2) = −iβ2ω²·dz/4 − α·dz/4。代码里写成1j·beta2·omega²·dz/4 − alpha·dz/4,负号来自算式里 −i 与 1j 的对应,β2 = −1 时代入正好给正指数。这个系数是我见过最容易被改错的地方,少乘或错位都会让色散强度翻倍。
非线性步的相位是gamma·|u|²·dz,|u|² 是当前时域场强的平方,每一步都要重新计算。因为 u 是复数数组,np.abs(u)**2与u·np.conj(u)完全等价,用哪个都行,但前者可读性更好。每一步三个 FFT 操作(fft、ifft、fft),1000 步就是 3000 次变换,N = 4096 时总共几秒,验证阶段完全够用。
3.3 验证与可视化:能量守恒、孤子相位和光谱演化
代码跑完后,第一件事不是画图,而是验证两个理论结果。其一是能量守恒:整个对称分裂格式的线性步和非线性步都是模长为 1 的乘子,理论上每一步都保模,能量 ∫|u|²dt 的相对偏差应该在机器精度附近。其二是孤子相位:一阶孤子在 z 处的理论相位是 z/2,对比数值结果必须对 2π 取模。
# ===== 能量守恒检查 ===== energy = np.array([np.sum(np.abs(v)**2) * dt for v in u_record]) rel_err = np.abs(energy - energy[0]).max() / energy[0] print("能量相对偏差:", rel_err) # 预期 < 1e-12 # ===== 孤子相位与峰值功率校验 ===== center = N // 2 phi = np.angle(u[center]) phi_theory = Lz / 2 dphi = abs(((phi - phi_theory + np.pi) % (2 * np.pi)) - np.pi) print(f"峰值功率: {np.abs(u[center])**2:.6f} (理论 1.0)") print(f"峰值相位偏差: {dphi:.2e} (理论 {phi_theory:.4f})")这套校验能一次性抓出三类错误:能量偏差大,说明某一步的乘子模长不是 1(最常见是丢了 1j);相位偏差大,说明线性算子的系数或符号不对;峰值功率变了,说明非线性强度没有正确抵消色散,N² 不是 1。
可视化用 matplotlib 就够了。把每个记录点的时域包络和频域光谱画出来,再用 pcolormesh 画 z–t 二维演化图,能直观看到孤子的“腰”在传播中保持宽度不变,这是分步傅里叶法所有后续工程验证的基准图。
import matplotlib.pyplot as plt freq = np.fft.fftfreq(N, dt) fig, axes = plt.subplots(1, 3, figsize=(14, 3.5)) for zz, uu in zip(z_record, u_record): axes[0].plot(t, np.abs(uu)**2, label=f"z={zz:.1f}") axes[1].plot(freq, np.abs(fft(uu))**2, label=f"z={zz:.1f}") axes[0].set_xlim(-8, 8) axes[0].set_title("时域包络") axes[1].set_xlim(-0.5, 0.5) axes[1].set_title("光谱") # z-t 演化图 field = np.array([np.abs(uu)**2 for uu in u_record]) Z, Tgrid = np.meshgrid(z_record, t, indexing='ij') axes[2].pcolormesh(Z, Tgrid, field, shading='auto') axes[2].set_title("z-t 演化") plt.tight_layout() plt.show()我一般在验证通过后会顺手把初始条件改成 2·sech(t) 跑第二组测试:二阶孤子应当以 π/2 为周期“呼吸”,峰值功率周期性起伏。这个测试比一阶孤子更严格,能暴露步长不足引起的呼吸周期畸变,也顺便确认代码能处理高阶孤子场景。
4. 分步傅里叶法源代码的参数怎么设:步长、窗口、归一化三个关键决策
4.1 步长 dz 的选取准则:用最大非线性相移兜底
步长 dz 是分步傅里叶法唯一的自由参数,也是最难拍脑袋定的事。经验法则是用每一步的非线性相移兜底:Δφ_NL = γ·|u|²·dz 应当控制在 0.01 到 0.05 弧度以内。对一阶孤子,γ·P0 = 1,所以 dz 取 0.01 能留出七八倍余量;但对高阶孤子,脉冲在压缩阶段峰值功率可以涨到初始值的数倍,这时候必须按演化过程中的最大峰值算,而不是初始值。
线性部分也有约束:色散引起的相位变化是 |β2|·ω²·dz/4,其中 ω 要取光谱有效支撑的边缘,而不是奈奎斯特边缘。奈奎斯特边缘频率极高,线性相位极大,但那里光谱能量为零,不影响精度;真正要管住的是光谱半高全宽附近那一带的相位。我在工程上通常同时看两个指标:非线性相移小于 0.02,色散相移在光谱支撑边缘小于 π。两个都不超,再开始扫参数。
一个实用的检查方法是固定其他参数,把 dz 减半重跑一遍,看结果变化。如果时域包络或呼吸周期的差异小于你设定精度的 1/10,当前 dz 就是合格的;如果差异明显,继续减半直到收敛。这个“步长减半对比”是分步傅里叶法最朴素也最可靠的自检手段。
4.2 时间窗口与采样点数:按展宽倍数和光谱支撑反推
时间窗口 Twin 的选取取决于传播距离内的最大展宽。对色散主导的脉冲,宽度约按 √(1+(z/LD)²) 增长,其中 LD = T0²/|β2| 是色散长度。例子:T0 = 1、β2 = −1 时 LD = 1,z = 10 处宽度大约展宽到初始的 10 倍,Twin = 40 勉强够,留到 80 更稳妥。孤子虽然自身不展宽,但色散波和辐射会把能量推向窗口边缘,所以即使做孤子模拟我也不会把 Twin 压到小于 20·T0。
采样点数 N 反过来由光谱支撑决定。脉冲展宽越窄、峰值功率越高,非线性产生的新频率越多,光谱越宽,需要的奈奎斯特频率越高。我用两个公式快速估算:频率分辨率为 1/(2·Twin),光谱最大频率为 1/(2·dt)。需要 dt = 2·Twin/N,所以 N ≥ 4·Twin·f_max。实际操作中我一般从 2^12 起步,先看频域图两端的值是否已经掉到峰值功率的 1e-6 以下,没掉就翻倍到 2^13 或 2^14。N 每翻倍,内存和计算量翻倍,但换来的是光谱混叠这个最难排查的隐性错误被提前消灭。
4.3 无单位化与孤子阶数 N²:让参数从玄学变成公式
很多源码在开头直接写 β2 = −1、γ = 1、P0 = 1,看起来像魔法数字,实际是做完无单位化后的约定。标准做法是引入色散长度 LD = T0²/|β2| 和非线性长度 LNL = 1/(γ·P0),用 ξ = z/LD、τ = t/T0、U = A/√P0 替换原方程,得到只剩下一个参数的归一化方程:
i·∂U/∂ξ = (1/2)·∂²U/∂τ² + N²·|U|²U
这个 N² = γ·P0·LD = γ·P0·T0²/|β2| 就是孤子阶数的平方,也是整套参数设计的核心判据。
| N² 取值 | 物理行为 | 数值模拟注意点 |
|---|---|---|
| N² ≪ 1 | 色散主导,脉冲展宽 | 步长由色散相位约束 |
| N² = 1 | 一阶孤子,形状不变 | 最佳验证用例 |
| N² > 1 | 高阶孤子,周期呼吸 | 峰值功率随压缩升高,dz 要相应减小 |
理解了 N²,参数设置就从“试出来的玄学”变成“按公式算出来的工程决策”。典型流程是:先定 T0 和 β2 算出 LD,再根据想要的现象定 N²,反推 P0 = N²·|β2|/(γ·T0²)。模拟完成后用 U = A/√P0 把归一化场换算回真实单位。我一般会在代码注释里写清楚每一步对应哪个归一化量,防止三个月后回来看不懂自己写的参数。
5. 分步傅里叶法常见问题排查:五个高频翻车点与解决记录
5.1 模拟后期窗口边缘出现“复活”的伪峰
现象:传播到中后段,时域图窗口边缘出现对称的伪峰,有时看起来像能量从两边“流”回中央,中央演化图出现干涉条纹。
原因:FFT 隐含周期性边界条件,脉冲在窗口内展宽或产生色散波后触及边缘,绕到窗口另一侧重新进入。这不是分步傅里叶法算错了,是时间窗口没能容纳整个演化区域。
解决:加大 Twin,按 4.2 节的展宽公式预留 5 倍以上余量。对于强非线性场景,还可以额外在窗口边缘加吸收层,常见做法是在时域乘一个超高斯窗函数,但注意这会破坏能量守恒,记录能量时要在滤波之前算。我通常是先加宽窗口,吸收层只在窗口已经很大但仍然出现边界效应时才上。
5.2 光谱高频端不归零,演化图出现锯齿
现象:频域图上光谱在奈奎斯特频率附近不跌到数值噪声水平,而是形成高台阶,时域演化图出现细密的锯齿状波纹。
原因:dt 太大,奈奎斯特频率 1/(2·dt) 小于光谱实际占有带宽。非线性自相位调制不断产生新的频率成分,超出采样能力后被混叠回低频,污染真实光谱。
解决:增大 N 或减小 Twin(减小 dt)。判断标准是看光谱两端的功率密度是否低于峰值 1e-6 量级。我习惯初始跑一遍就看np.abs(fft(u0))**2的边界值,如果初始光谱已经触边,后续必然混叠,直接翻倍 N 再开始。
5.3 明明给了 sech 脉冲,孤子却传播几下就散开
现象:初始条件是标准的 sech 脉冲,参数看着也对,但传播几个单位距离后脉冲明显展宽,完全不孤子。隔壁同学用同一套代码却能跑出漂亮的孤子。
原因:绝大部分是归一化没对上。你心里想的是 γ·P0 = 1 的聚焦 NLS,实际参数组合对应的 N² 远小于 1,色散主导自然展宽;或者 β2 取了正号(正常色散),聚焦型孤子根本不存在。另一个常见原因是 dz 过大,非线性相移误差累积后破坏了孤子条件。
解决:先用三件套 β2 = −1、γ = 1、P0 = 1 跑 sech 验证,确认能出孤子后再改参数。每改一次参数,用 4.3 节的公式算一次 N²,确保它在 1 附近。要检查 dz 是否满足 γ·|u|²_max·dz < 0.02,如果不满足一起改。
5.4 色散强度与理论对不上,差一个微小系数
现象:脉冲展宽速度比理论快一点或慢一点,孤子相位验证总是差一个固定比例,怎么调 dz 都不消失。
原因:时间轴与频率轴的采样间隔不一致。典型错误是用np.linspace(-Twin, Twin, N)构造时间轴,实际间隔是 2·Twin/(N−1),却给 fftfreq 传了 dt = 2·Twin/N,导致频率轴系统性偏大或偏小一个因子(N−1)/N。这个因子在 N 较小时非常明显,参数扫描时表现为色散长度始终对不上。
解决:统一用(np.arange(N) - N/2) * dt构造时间轴,dt 严格等于 2·Twin/N。如果坚持用 linspace,必须把 dt 改成 2·Twin/(N−1) 再传给 fftfreq。代码里加一行打印omega[1]与 2π/(N·dt) 是否相等,能在一分钟内揪出这类错位。
5.5 能量随传播距离持续上涨或下跌
现象:设定 α = 0,但记录的 ∫|u|²dt 随 z 单调变化,有时涨有时跌,幅度远超机器精度。
原因:非线性步里丢了虚数单位 1j,把exp(iγ|u|²dz)写成了exp(γ|u|²dz),能量按指数涨或跌;或者线性损耗的符号写反,−α·dz/4写成了+α·dz/4,损耗变成了增益;或者中间插入了模长小于 1 的滤波窗而没有补偿能量。还有人是把 u 声明成了 float32,累积误差在长距离传播后放大。
解决:先把 α 设成 0,用 3.3 节的能量检查确认相对偏差在 1e-12 量级。如果能量不再守恒,逐行检查乘子的模长:线性乘子abs(lin_half)必须全为 1,非线性乘子模长也恒为 1。确认是浮点精度问题时,整个数组改为 float64,这是默认值,不需要额外操作。这一步排查熟练后五分钟内就能定位,新手往往在这里耗一下午。
6. 让源代码更耐用的三个进阶习惯:守恒校验、自适应步长与 GPU 加速
6.1 守恒量校验:能量与光子数必须盯住
基础验证之外,我建议把守恒量校验写成一个独立函数,每次跑新参数前先过一遍。NLS 在 α = 0 时除了能量 ∫|u|²dt,还有光子数(频域模平方和)守恒。两者同时守恒,能确认频域和时域两个方向的数值操作都没做手脚。这个习惯在加滤波窗、做分步输出、改格式时特别有用,能第一时间暴露非物理的增益或损耗。
def check_conservation(u, dt): energy = np.sum(np.abs(u)**2) * dt photon = np.sum(np.abs(fft(u))**2) / u.size return energy, photon6.2 自适应步长:半步比较法自动调整 dz
固定步长在参数扫描时要么保守浪费算力,要么激进导致误差。常见做法是步长减半比较:把大步 dz 的结果和两个 dz/2 的结果对比,用最大相对误差估计局部误差,误差超阈值就折半步长,误差远小于阈值就翻倍,推进时用半步结果更准。
def take_step(u, h, beta2, gamma, alpha, omega): lin = np.exp(1j * beta2 * omega**2 * h / 4 - alpha * h / 4) v = ifft(lin * fft(u)) v = v * np.exp(1j * gamma * np.abs(v)**2 * h) v = ifft(lin * fft(v)) return v def adaptive_step(u, h, tol=1e-8): u_full = take_step(u, h) u_split = take_step(take_step(u, h/2), h/2) err = np.max(np.abs(u_full - u_split)) / np.max(np.abs(u_split)) return u_split, err # err>tol 时折半步长重算,err<tol/10 时下次翻倍这个自适应策略的代价是每一步做 6 个 FFT 而不是 2 个,贵,但换来的是对整个传播距离的误差可控。我在长距离仿真(z 超过几十个色散长度)和高阶孤子压缩这类峰值功率剧烈变化的场景里,默认开自适应,固定步长只用在验证阶段。
6.3 cupy 一行切到 GPU,二维问题同样套路
分步傅里叶法天然适合 GPU:FFT 和逐点乘法都是纯数值操作,没有复杂分支。把 numpy 换成 cupy,FFT 换成 cupy.fft,循环体一行不用改:
import cupy as cp from cupy.fft import fft, ifft # 所有 np. 换成 cp.,其余代码不变 # 画图前用 .get() 把数据拷回 CPU实战中注意两个边界:N 小于 2^14 时 CPU 已经够快,GPU 的优势不明显,反而要承担显存拷贝开销;涉及大量参数扫描时,让 GPU 常驻、把多个 z 段串行计算,收益更大。这套分裂思想同样能推广到二维 NLS(比如 BEC 涡旋模拟),只是把 FFT 换成二维 FFT,线性算子和非线性算子的结构完全不变。
我自己每写一版分步傅里叶法源码,都会先跑一阶孤子验证再开始加功能,这个习惯帮我挡掉了大部分参数错误。另一条教训是不信任一次性写对的参数,每次改完都打印能量和相位偏差,确认没问题再继续。这套代码的价值不在代码本身,而在你愿意为它配一套可复现的验证流程,希望帮到你。
本文还有配套的精品资源,点击获取