☰
FDTD平面波模拟实战:MATLAB实现与PML边界关键技巧
2026/10/20 11:20:05 网站建设 项目流程

简介:有限差分时域(FDTD)方法是一种广泛应用于电磁场、声学与地震波数值模拟的求解技术。这一MATLAB项目以平面波与面波传播模拟为场景,提供了简洁可运行的入门脚本,很适合初学FDTD算法的学生与科研爱好者逐步跟读代码、动手验证。压缩包内仅包含一个m格式的脚本文件(FDTD.m),整体大小约1KB,代码量不大,却覆盖了初始化网格与介质参数、设置平面波源、时间步进迭代、边界条件处理以及结果可视化等完整流程,且关键位置配有注释,便于逐行理解。目前已有1565人浏览学习,是公认的低门槛上手示例。借助这个程序,学习者既能直观看到FDTD离散网格上的波场演化,也能通过修改参数对比不同波长、介质或边界下的传播特征,为后续开展更复杂的二维、三维仿真打下扎实基础。

1. FDTD 平面波模拟:不是所有“平面波”都能叫平面波

做电磁场仿真的人大概都经历过这种尴尬:明明照着论文敲了 FDTD 代码,电场图出来却是弯的;或者在边界处看到一圈圈反射波纹,以为是自己算法写错了,改了一整天,最后发现是总场/散射场边界条件没搞清楚。这套 FDTD 平面波 MATLAB 资源包解决的问题,就是把「平面波源怎么注入」「PML 怎么吸收」「CFL 条件怎么设」「时间步进怎么稳」这几件事一次讲透。

单看标题里“FDTD 平面波 matlab”这几个词,圈内人都知道分量:FDTD 是时域有限差分法,平面波是最干净的入射源,MATLAB 是最快的验证环境,三者凑齐意味着你可以在半小时内得到一个二维自由空间的平面波传播场图,再花两小时改成带介质柱的散射模型。适合三类人:刚入门 FDTD 的研究生、需要用平面波做超表面或光子晶体模拟的工程师、以及想把手写代码和商用软件对不上的结果搞清楚的仿真党。

2. FDTD 核心原理与平面波的“正确打开方式”

2.1 Yee 元胞与蛙跳格式:为什么电场磁场永远差半步

FDTD 的底层逻辑是 K.S. Yee 在 1966 年提出的元胞离散方案:把空间切成矩形网格,电场和磁场分量在空间上交错半个网格,在时间上也交错半个时间步。这套空间交错、时间蛙跳的方案,让麦克斯韦旋度方程可以离散成一组显式差分格式,不需要求解线性方程组,每个时刻的场值直接由上一时刻场值算出。

在 MATLAB 实现中,二维 TM 波(电场只有 Ez 分量,磁场有 Hx、Hy 分量)是最常见的起步模型。核心更新公式长这样:

% TM 波二维 FDTD 核心更新 % 网格参数 dx = 1e-3; % 空间步长 1mm dy = dx; dt = dx / (c * sqrt(2)); % CFL 稳定条件 % 电场 Ez 更新:由旋度 H 计算电场时域变化 Ez(2:end-1, 2:end-1) = Ez(2:end-1, 2:end-1) ... - (dt/(eps0*dx)) * (Hy(2:end-1, 2:end-1) - Hy(1:end-2, 2:end-1)) ... + (dt/(eps0*dy)) * (Hx(2:end-1, 2:end-1) - Hx(2:end-1, 1:end-2));

这里 Ez 的更新用到了周围四个 H 分量,严格遵循 Yee 元胞的空间交错关系。dt/(eps0*dx)这个系数看起来简单,实际上决定了波在网格间传播的数值速度——如果 dx 取太大,波前会有明显色散;如果 dt 不满足 CFL 条件,场值会指数发散。参数 dx(空间步长)一般取最小波长的十分之一到二十分之一,dt 则按二维 CFL 上限dx/(c*sqrt(2))取值,保险起见常用其 90%。

2.2 平面波源的注入方式:硬源、软源与解析源

FDTD 里“平面波”这个说法本身就藏着坑。很多人第一次写 FDTD,直接在最左边一列网格上赋值Ez(:, 1) = sin(w*t),跑出来发现波不是一条直线往前传,而是从上下边界冒出杂散场。这是因为这样做相当于在网格里塞了一个无限长的线源,波向四面八方辐射,根本得不到平面波。

三种源注入方式的差异如下表:

注入方式代码实现优点缺点
硬源Ez(50,:) = sin(w*t)简单直接场值被强制覆盖,会产生反射
软源Ez(50,:) = Ez(50,:) + sin(w*t)源点场与总场叠加反射波会被源再次散射
解析源+总场/散射场在边界上按解析表达式追加场值干净、业界标准实现复杂需边界处理

最常见的做法是软源注入,配合总场/散射场(TF/SF)框架,把计算域分成总场区和散射场区。入射场只在总场区传播,散射场区只含目标散射出的场。

2.3 吸收边界:PML 为什么是默认首选

波遇到边界反射回来,是 FDTD 模拟中最烦人的问题之一。早期用一阶 Mur 吸收边界,对入射角敏感;后来 Berenger 提出 PML(完美匹配层),在边界区域人为构造一种各向异性的吸收介质,让透射波在 PML 内指数衰减而不产生反射。

一份能直接跑的二维 PML 代码核心实现如下:

% PML 边界更新,以 x 方向电场为例 % 更新 Hx 分量中的 sigma_z 项 for j = 2:Ny-1 for i = 2:Nx-1 % x 方向 PML 参数随位置变化 sigx(i) = sig_max * (i / Npml)^3; % 三次渐变提升吸收效果 % Hx 更新(PML 修正项) Hx(i,j) = Hx(i,j) - (dt / (mu0 * dx)) * (Ez(i,j+1) - Ez(i,j)); end end

PML 参数的设定直接影响吸收效果:PML 厚度一般为 8~16 个网格,电导率 sigma 从内边界到外边界按多项式渐变,多项式阶数用 2~3 次,理论反射系数设置在1e-4量级。用三层 PML 逐层剥开测试时,如果最外层仍有反射,问题多半出在 sigma_max 取太小或 PML 内差分格式写错,这两个原因占了 PML 失效案例的八成。

3. 从零搭一个二维 FDTD 平面波模型:参数设置与完整流程

3.1 计算域与色散关系分析:先定频率,再定网格

动手写代码前,先把工程参数定下来。仿真遇到的第一个岔路口是:你的模型是色散的还是非色散的?如果是自由空间传播验证,非色散模型足够;如果模拟金属、等离子体或增益介质,就必须引入 Drude/Lorentz 模型。debye 模型用两个极点近似水的介电常数,在生物组织模拟里很实用。

编写 FDTD 代码前需要先分析色散关系。在均匀介质中,FDTD 的数值色散方程决定了网格色散误差:

% 数值色散关系分析 % 平面波色散方程: [ (1/c) * sin(omega*dt/2) / dt ]^2 = ... omega = 2 * pi * f0; % 角频率 k = omega / c; % 真空中波数 % 数值色散误差随每波长网格数的变化 N_lambda = 10:5:50; % 每波长网格数 dispersion_error = zeros(size(N_lambda)); for i = 1:length(N_lambda) dx_val = lambda / N_lambda(i); dt_val = dx_val / (c * sqrt(2)); % 数值波数 k_num = (2 / dx_val) * asin(c * dt_val / dx_val * sin(omega * dt_val / 2)); dispersion_error(i) = abs(k_num - k) / k; end

每波长 10 个网格时,色散误差约 2%~3%;每波长 20 个网格时,误差降到 0.5% 以下。这组数据说明,初学时为了跑得快把网格放粗到每波长 8 个网格以下,得到的结果就和真实物理有明显偏差了。这个分析函数建议保留成脚本,以后换介质、换频率都能复用。

3.2 主程序模块化设计:初始化、源注入、边界吸收、场记录

主程序建议拆成四个模块,每个模块单独调试。这里我按自己习惯的写法给一份可直接运行的骨架,模块划分清楚,后续改参数不迷路:

% FDTD 平面波模拟主程序骨架 % 参数初始化部分 clear; clc; c = 3e8; % 真空光速 f0 = 5e9; % 工作频率 5GHz lambda = c / f0; % 波长 dx = lambda / 20; % 每波长 20 个网格,色散误差小于 1% dy = dx; dt = dx / (c * sqrt(2)) * 0.9; % CFL 系数取 0.9 保证稳定 Nx = 200; Ny = 200; % 计算域网格数 Npml = 10; % 每边 PML 厚度 Steps = 500; % 时间步迭代数 % 初始化场数组与材料分布 Ez = zeros(Nx, Ny); Hx = zeros(Nx, Ny); Hy = zeros(Nx, Ny); eps_r = ones(Nx, Ny); % 相对介电常数,可改成介质柱阵列 mu_r = ones(Nx, Ny); % 时间步进:源注入 + 场更新 + 边界吸收 for n = 1:Steps % 1. E 场更新(含 PML 区域修正) Ez(2:end-1, 2:end-1) = Ez(2:end-1, 2:end-1) ... - (dt / (eps0 * dx)) * (Hy(2:end-1, 2:end-1) - Hy(1:end-2, 2:end-1)) ... + (dt / (eps0 * dy)) * (Hx(2:end-1, 2:end-1) - Hx(2:end-1, 1:end-2)); % 2. 平面波源注入(软源方式) Ez(50, :) = Ez(50, :) + sin(2 * pi * f0 * n * dt); % 3. H 场更新 Hx(:, 1:end-1) = Hx(:, 1:end-1) ... + (dt / (mu0 * dy)) * (Ez(:, 1:end-1) - Ez(:, 2:end)); Hy(1:end-1, :) = Hy(1:end-1, :) ... - (dt / (mu0 * dx)) * (Ez(1:end-1, :) - Ez(2:end, :)); % 4. PML 吸收(外层边界) % ... 调用 PML 子程序,这里省略 end

这个骨架里,Ez(50, :)是平面波注入位置,可以用变量控制。源的类型可以改成正弦波、高斯脉冲或调制高斯脉冲——做宽频仿真时需要高斯脉冲,做单频稳态成像时需要正弦波。注意 H 场更新在 E 场之后,这是蛙跳格式顺序,任何一处倒序都会导致时间离散不稳定。

3.3 结果可视化:什么样子的场图才算“对”

模拟跑完,得知道结果对不对。我总结了一个三看法:

第一看波形剖面:沿传播方向取一条线上的 Ez 值,和理论平面波相位对比。如果波前是弯的,说明网格太粗或者源注入方式不对。第二看时间演化:不同时刻的场快照对比,波前应该匀速平移不衰减。第三看 PML 区域:PML 内场值应单调衰减到接近零,如果 PML 内出现驻波图案,说明吸收效果差。

% 结果可视化:场快照与波形对比 figure(1); imagesc(1:Nx, 1:Ny, Ez'); % 注意 MATLAB 显示需要转置 axis xy; colorbar; title(sprintf('Ez Field at step %d', n)); xlabel('x 方向网格数'); ylabel('y 方向网格数'); % 沿传播方向提取中线波形 figure(2); plot(Ez(:, Ny/2), 'b-', 'LineWidth', 1.5); xlabel('x 方向网格'); ylabel('Ez 场值'); % 对比解析平面波 analytic = sin(2*pi*f0*(1:Nx)*dx/c); hold on; plot(analytic, 'r--'); legend('FDTD', 'Analytic');

两张图配合检查:场快照看全局,中线波形看定量一致性。经常有同学问,为什么模拟场图整体有一条斜的亮纹?这大概率是 PML 角区处理不当造成的角点反射,会在后面避坑章详细说。

4. 面波模拟的关键进阶:总场散射场与周期结构

4.1 TF/SF 分离:让“入射场”和“散射场”各归其位

平面波模拟的精髓在于把场拆成入射和散射两部分。总场/散射场(TF/SF)方法可以在计算域内部定义一个虚拟边界,入射场只在总场区存在,散射场在散射区传播。这样做的好处是:得到的散射场直接就是物理上有意义的结果,不需要事后做总场减入射场的数值减法——后者的误差在有界域内会逐渐累积,这是我踩过的坑。

TF/SF 的边界实现不需要额外改场更新公式,只是在边界网格处做源补偿:

% TF/SF 边界源注入(以 x 方向传播为例) % 在入射边界网格处,用解析入射场值修正 Ez(50, 50:end-50) = Ez(50, 50:end-50) + E_inc(n); % E_inc 为入射场在 n 时刻的值 % 需要注意:在 H 场更新时,TF/SF 边界也需要同样处理 Hy(49, 50:end-50) = Hy(49, 50:end-50) - (dt / (mu0 * dx)) * E_inc(n);

这里E_inc(n)是入射平面波在时间步 n 的采样值。TF/SF 边界最难处理的是角元胞,因为同时涉及两个方向的入射场修正,表达式一旦写错,角区会出现明显的伪源辐射。

4.2 散射体建模:介质柱、金属板与多层膜的 FDTD 实现

把散射体放进去之前,先在材料参数矩阵上做文章。介质柱就是让eps_r(i,j) = n^2,金属板则在 PEC 近似下直接强制场值为零,但更精细的做法是用 Drude 模型处理色散金属:

% 介质柱阵列参数设置 % 在 eps_r 矩阵中定义多个介质柱 for k = 1:num_pillars x1 = round(center_x(k) - r_pillar/dx); x2 = round(center_x(k) + r_pillar/dx); y1 = round(center_y(k) - r_pillar/dx); y2 = round(center_y(k) + r_pillar/dx); eps_r(x1:x2, y1:y2) = n_pillar^2; % 柱内介质折射率 end

注意round函数在做网格离散时,会把圆形柱变成锯齿状,这是 FDTD 阶梯近似的固有误差。如果柱的直径只占 5 个网格,锯齿效应不可忽略,建议至少让柱径向占 15 个网格。

多层膜结构的模拟在光电领域比较常用,多层膜就是沿传播方向交替排放高折射率/低折射率薄层,用 FDTD 可以看透射谱、反射谱中的禁带。层边界同样只是 eps_r 矩阵的赋值,不需要特别处理。

4.3 周期边界:减少计算量的经典方案

模拟周期结构时,比如光子晶体或超表面阵列,用周期边界条件可以用一个晶胞代替几十个周期的计算域。FDTD 周期边界实现起来比 PML 简单:

% 周期边界条件:沿 y 方向 % 每步更新后,把边界外的场值映射到另一侧 Ez(:, 1) = Ez(:, end-1); % 左边界取右边界内侧值 Ez(:, end) = Ez(:, 2); % 右边界取左边界内侧值 % 对 H 场做同样处理 Hx(1, :) = Hx(end-1, :); Hx(end, :) = Hx(2, :);

注意一个常见的概念混淆:周期边界适合周期性结构,但如果入射波带角度(斜入射),周期边界需要配合 Bloch 周期条件,引入相位偏移exp(j*kx*L)。斜入射不加相位修正,模拟结果会出现周期性伪影,这是《FDTD 周期边界模拟超表面》这个方向里最容易翻车的地方。

5. 参数调试与避坑指南:CFL 条件、PML 失效与源反射排查

做 FDTD 平面波模拟,Debug 的时间往往比写代码的时间多得多。下面这些坑都是自己跑出来的,按“现象→原因→解决”的格式写清楚。

5.1 场值发散:CFL 条件与网格比例的配合问题

现象:模拟跑到某一步后场值突然变成 NaN 或数值爆炸,图像上出现雪花斑。

原因:时间步 dt 不满足 CFL 稳定条件,或者空间步长 dx、dy 取值不同导致差分格式不稳定。CFL 条件是 FDTD 的底线,它限制了波在一个时间步内的传播距离不能超过网格对角线。二维情况下要求c*dt*sqrt(1/dx^2 + 1/dy^2) <= 1,如果 dx 和 dy 不相等,要按更严格的方向取 dt。

解决:先把 dx = dy 修正,再用dt = dx/(c*sqrt(2))*0.9设置时间步,留 10% 余量。如果仍然发散,检查介质参数:如果设定eps_r为负数(模拟金属但不做色散模型),场会在那个位置指数增长,这是物理模型设置错误,不是数值不稳定。

5.2 PML 区域出现驻波:渐变厚度与电导率曲线失配

现象:PML 内部场衰减不彻底,出现周期性驻波图案,并且外部边界持续有微弱反射回到计算域。

原因:PML 的电导率多项式阶数太高或太低,或者 PML 厚度太薄。我在参数调试时发现,PML 厚度为 10 个网格、sigma 按三次曲线渐变时效果最好;用线性渐变时反射增加 5~10 倍。另一个隐蔽原因是 PML 内部差分方程没有同步更新——PML 区域的场更新公式和正常区域不同,需要处理分场量。

解决:推荐设置 PML 厚度Npml = 10(小于 8 个网格吸收效果显著下降),sigma_max 取0.8 * (order+1) / (eta * dx),其中 eta 为波阻抗。跑完后单独检查一条从计算域中心到 PML 外边缘的场值曲线,看是否呈指数衰减。如果曲线出现“台阶”,说明离散格式写错了。

5.3 平面波变柱面波:源注入位置与 TF/SF 边界不配合

现象:明明每步都注入了Ez(50,:) = sin(w*t),但波前是弧形,像柱面波向内向外扩散。

原因:硬源或软源只在单个网格点/单列上强制赋值,从该点出发的波在二维空间中向所有方向传播。要得到严格的平面波,必须在整个入射截面上同时注入,且截面的场值在任何时刻相位都相同——这听起来简单,但很多新手在 TF/SF 框架里注入了解析入射场后,忽略了在 H 场更新阶段同步补偿,导致入射场在 TF/SF 边界处被“双重计算”,出现边缘衍射。

解决:检查 TF/SF 边界上 E 场和 H 场的补偿条件是否成对出现。E 场注入点,必须在相邻的 H 场更新处做对应修正,二者相差半步时间。用我的调试习惯,在边界处放一个探针,对比 FDTD 场值与解析平面波场值,如果误差在 2% 以内才算正确。

5.4 斜入射平面波模拟的相位匹配问题

现象:以 30 度角入射平面波模拟完毕,但场的空间条纹间距与理论值不符,或者出现 Moire 条纹。

原因:斜入射平面波需要在 TF/SF 边界每个网格点注入不同相位的场值,相位等于k * x * sin(theta)。如果所有网格点都用同一相位,等效成了垂直入射。

解决:把平面波源写成:

% 斜入射源注入 theta = 30 * pi / 180; % 入射角 30 度 % 入射场场值带空间相位 E_inc(x, n) = sin(2*pi*f0*n*dt - 2*pi*(x - x0)*dx/lambda*sin(theta));

同时注意,斜入射情况下周期边界条件必须加 Bloch 相位修正,这个上一章已经提过,二者要联动修改,不能单独改一个。

5.5 多源干涉模拟的时延控制

现象:用两个平面波源模拟干涉,但干涉条纹抖动不停,达不到稳态图案。

原因:两个源之间的相位差或时间延迟没有定义好。FDTD 是时域模拟,但如果只看总场而不做时间平均,条纹一直在“流动”。如果是单频源,需要等稳态建立;如果是宽带脉冲源,每个时刻的瞬时场就是宽频干涉结果,要看频谱响应而不是瞬时场图。

解决:单频源跑 5~10 个光周期后再记录场数据;两个源的初始相位差通过设置E_inc1(n)和E_inc2(n)的时间偏置实现,比如第二个源在n+delay_steps处激活。

6. 验证与进阶:从平面波到自定义结构的跳转技巧

6.1 无源区数值验证:让模拟结果先“自己证明自己”

拿到一份 FDTD 代码,最先做的不该是急着替换材料,而是先做无源区验证。我在每次换电脑、换 MATLAB 版本后,都会强制跑一遍这套验证:没有散射体、只有平面波源、PML 吸收边界,跑 500 步。验证的标准只有一个——计算域内的场值应该与解析平面波完全一致,误差不超过 1%。如果这一步不先通过,后面所有材料、结构的结果都是空中楼阁。

% 无源区验证脚本 % 计算 FDTD 场值与解析解的 L2 误差 L2_error = sqrt(sum(sum((Ez - Ez_analytic).^2)) / sum(sum(Ez_analytic.^2))); % 若误差超过 0.01,检查源注入和 PML 边界

这个误差值如果偏大,通常 90% 是源注入方式不对或 PML 反射污染了计算域边缘。

6.2 稳态分析用的时间平均技巧

FDTD 输出的是瞬态场,但实际工程中我们往往关心稳态响应。常用做法是等源发射 10 个周期后,再做 1~2 个周期的时间平均。这个操作的实现很简单:

% 稳态场提取:时间平均 start_step = 20 * round(1 / (f0 * dt)); % 源开启 20 个周期后 avg_steps = 2 * round(1 / (f0 * dt)); % 平均 2 个周期 Ez_steady = zeros(Nx, Ny); for n = start_step : start_step + avg_steps % ... 这里继续做场更新 ... Ez_steady = Ez_steady + Ez; % 累加 end Ez_steady = Ez_steady / avg_steps;

做稳态分析时,注意记录场值的时候不要再注入平面波进去,否则累加平均会包含源的瞬态变化,导致结果出现“伪振荡”。

6.3 从平面波跳转到自定义结构:三个必须改的参数位置

平面波模型跑通之后,改成自己需要的结构总共只需动三处代码:第一处是eps_r和mu_r矩阵,在初始化段替换为自定义材料分布;第二处是源的定义位置,如果是自由空间背景换成衬底,源的位置可能需要挪动;第三处是观察点/记录面的坐标,用于提取透射谱或散射场分布。

我一般会用注释行的方式标记修改点,避免每次重新搜索变量名。另外建议给每种材料一个编号:MAT_AIR = 1; MAT_SILICON = 2; MAT_METAL = 3;,材料矩阵先填编号,然后统一映射到介电常数。这个习惯在应对多层膜、渐变折射率器件时特别省事——只改映射表,不用改 FDTD 更新代码。

6.4 面向频谱响应的宽频源技巧:高斯脉冲激励与傅里叶变换

如果想一次模拟得到整个频段的响应,单频正弦源就得跑十几次。改用调制高斯脉冲作为激励源,配合探测点时域信号的傅里叶变换,一次就能拿到宽带频谱。这个技巧在透射谱、反射谱、吸收谱仿真里是标配操作。

% 宽频源:调制高斯脉冲 tau = 0.5 / f0; % 脉冲宽度 t0 = 4 * tau; % 脉冲峰值时间 t = n * dt; source = exp(-((t - t0) / tau).^2) .* sin(2 * pi * f0 * t); % 频率响应:对记录点做 FFT freq_axis = (0 : Nfft/2-1) * (1/dt) / Nfft; E_freq = fft(E_record, Nfft);

用宽频源时注意两点:脉冲宽度决定了频率覆盖范围,tau = 0.5/f0时频谱大约覆盖 0.5f0 到 1.5f0 的范围,如果想覆盖更宽频带就减小 tau;频点数量则受限于时间步数和总模拟时长的乘积,要提高频域分辨率就要跑更多的时间步。这个方法有一个技术门槛:如果只记录了一个点做 FFT,拿到的 S 参数只反映那个位置的场值。做透射谱时,需要把观察面上的场值做空间积分再 FFT,才能对应到远场透射。这两种做法结果差很多,我看到很多初学者在这上面翻车。

从那以后,我每次拿到一份 FDTD 资源包,都会强制自己先跑一遍无源区验证脚本,把误差数值记录在一个固定的文本文件里。下次再跑任何结构,第一件事不是看场图,而是看这份误差记录有没有变——FDTD 这种东西,一旦误差基线漂了,后面所有结果都不可信。希望这些经验能帮你在复现这份平面波模拟资源时少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询