做光栅反射镜、超表面相位调控或者谐振腔设计的朋友,大概都经历过同一个困惑:反射率曲线看得很清楚,一到反射相位就抓瞎。其实反射率只告诉了你“有多少光被弹回来”,反射相位才告诉你“弹回来的光被推迟了多少”。这两个量合起来才是完整的复数反射系数,很多工程问题的本质追到最后都落在相位上。
Lumerical和COMSOL是两套最常用的仿真工具,前者做纳米光子学器件效率高,后者做多物理场耦合灵活。两边都能算反射率,但提取反射相位的思路、步骤和坑点差别很大。这篇文章我按自己实际调模型的过程来写,把Lumerical(FDTD和STACK)与COMSOL两种路径的完整操作、参数设置、归一化细节和踩过的坑逐个讲清楚,希望能帮你在两套软件之间无缝切换、交叉验证。
1. 反射相位到底是什么,为什么值得专门提取?
1.1 从反射率到反射相位——被忽略的第二个关键参数
把一束单色平面波打到光栅或膜系表面,反射光可以写成一个复振幅:
r = |r| · exp(i·φ)
|r|的平方就是反射率,大多数人日常只看这个。φ就是反射相位,它代表反射光相对入射光在某个参考面上的相位延迟。你可能觉得相位只是抽象的理论量,但实际工程里它直接影响最终性能。
举个例子,谐振腔设计里,腔体的反射镜不只要高反射率,两端的反射相位还得满足谐振条件,否则谐振波长会发生偏移。再比如超表面透镜或波束偏转器件,靠的就是每个单元反射相位不同,从而在宏观上形成相位梯度,这跟反射率绝对值的相关性反而不大。还有相变材料(PCM)光开关、可调超表面,这类器件的核心指标经常是“相变前后反射相位差能达到多少度”,而不是简单的反射率变化。
所以反射率只是“能量维度”的指标,反射相位是“时间/相位维度”的指标。一个完整的反射仿真,必须把这两个维度都提取出来,才能支撑后续的相位匹配、干涉叠加、全息设计等工作。
单波长下的相位还不算难,真正麻烦的是宽带仿真。你需要在整个目标波段内获得连续的复数反射系数,这要求仿真器可以输出每个频率点的复振幅,而不只是模值。这也是Lumerical和COMSOL在设置上容易踩坑的地方:软件默认界面经常只展示反射率,相位要你自己想办法从原始数据里挖出来。
1.2 两种软件的设计哲学差异
Lumerical和COMSOL虽然都能算光栅反射,但它们底层的求解思路完全不同,这也决定了提取相位的方式不一样。
Lumerical FDTD是时域有限差分法,在空间网格上直接求解麦克斯韦方程组的时间推进,一个宽频脉冲打进去,通过傅里叶变换得到频域响应。好处是一次仿真覆盖全波段,宽带相位信息天然齐全。另外Lumerical还有一个STACK求解器,走的是严格耦合波分析(RCWA)路线,适合周期性多层膜、规则光栅的快速计算,计算速度快得多,在膜系设计阶段非常好用。
COMSOL走的是频域有限元路线,在频域内直接求解偏微分方程组,每个频点都要单独算一次,然后扫参合并成一条曲线。好处是可以把电磁场和热、力、载流子输运等物理场耦合起来,做多物理场仿真时是刚需。它的波长扫描不像FDTD那样天然全场覆盖,但通过参数化扫描也能得到连续的相位谱。
还有一个隐藏差异:FDTD的边界条件天然吸收,所以通常只在端口处取场;COMSOL的端口边界条件自带S参数功能,它给出的S11本身就是复数的,反射相位就是arg(S11)。这听起来更直接,但S11的相位对参考面的定义极其敏感,这往往是COMSOL提取相位时最大的坑。
1.3 一个需要先明确的物理概念:参考面
提取反射相位时,“参考面”是第一重要的概念。反射相位不是一个绝对量,它依赖你在哪个位置定义“反射发生的位置”。同一个光栅,你在光栅顶部界面取相位,和在顶部上方300nm处取相位,结果相差的相位约等于2π/λ乘以2倍距离(来回双程光程)。
这个道理和射频电路里的S参数很相似——S11的相位必须标注参考面,换一个参考面,相位整体旋转。如果你拿Lumerical的结果和COMSOL的结果直接对比,却没有先统一参考面,那大概率相位对不上,问题往往不在求解器精度,而在参考面定义不一致。
实操中,我通常统一在“光栅顶部(入射界面)或某个明确标志面”提取相位,并在后处理里补偿参考面偏移带来的传播相位。这块后面具体步骤里会再展开。
2. 快速找到Lumerical中提取反射相位的三个入口
2.1 STACK求解器:多层膜和规则光栅的秒级方案
Lumerical的STACK求解器适合平面多层膜和周期性规则光栅,它的计算方式是严格的RCWA,输入层堆栈和光栅参数,直接给出整个结构的复数反射/透射系数。它最大的优势是快,通常一次扫描几秒到几十秒就完成,非常适合前期参数扫描和趋势判断。
在STACK界面里,你需要定义:
- 基底结构:包括每个膜层的厚度、折射率(或自定义材料)
- 光栅层:光栅周期、占空比、光栅深度、材料
- 入射条件:入射角、偏振(TE/TM)、波长范围
设置完运行后,在结果窗口里选中“Reflection”相关项,不要只看反射率曲线。更关键的是找到复数形式的反射系数(很多版本叫R复数或者反射系数),通过它的实部虚部来计算相位:
φ = atan2(Im(r), Re(r))
如果你用的脚本模式,可以通过getresult("STACK","R")这类方式把反射系数的复数数组拉出来,然后angle()函数直接得到相位。注意这里的相位象限需要用atan2而不是atan,否则角度会跑到错误的象限。
STACK的局限也很明显,它假设结构在平面内严格周期,对复杂的二维非周期结构无能为力。但作为Lumerical的第一个验证入口,它价值巨大。我在做直接激光写入光栅这类不太严格周期的结构时,还是会先用STACK扫一遍膜系厚度组合,把大概趋势摸清楚,再用FDTD精算。
2.2 FDTD的频域监视器与S参数提取
对于复杂光栅或超表面单元,Lumerical FDTD是更通用的选择。在FDTD中提取复振幅的主流方案是使用“功率监视器”加“折射率监视器”的组合,或者直接用“S参数”分析组。
最稳定的做法是:
- 在光栅结构上方放置一个“频率域功率监视器”(Power Monitor),记录反射功率。
- 同时放一个“频率域场监视器”(Profile Monitor),记录该截面上每个网格点的复数电场分布。
- 通过脚本计算该截面上的反射复振幅。
脚本里可以用getresult("monitor_name","E")把监视器的场数据取出来,E是一个带x/y/z分量的复数数组。要得到整体的反射系数,需要对监视器平面做空间积分,这一步要和入射场做归一化。这里稍微绕,直接取某个点的E会受干涉条纹影响,必须做面积分或使用Lumerical自带的S参数方法。
Lumerical FDTD其实内置了S参数计算功能,在“S-parameter”分析组里可以直接得到反射S参数,S11就是复数。用它来算相位最省事:
S = getresult("S","S11"); wavelength = 1e6/lum.data("f"); % 频率转波长 phase = angle(S);这里有个必须注意的细节,S11的参考面不在光栅表面,而是在S参数分析组内置的监视器位置。所以拿到相位后,你得自己补偿监视器平面与光栅顶面之间的传播相位,否则相位谱整体有一条倾斜的斜率。这个补偿量是:Δφ = 4π·n·d/λ,其中d是监视器平面到参考面的距离,n是介质折射率,因子2对应光往返双程。
2.3 从时域信号到相位的后处理细节
如果你更喜欢高度自定义的提取方式,也可以用原始时域场来算。具体做法是放一个时间监视器,记录某个空间点在每个时间步的电场E(t),然后用傅里叶变换得到频域的复数电场:
E_f = fft(E_t); f = linspace(f_min, f_max, length(E_t)); phase_f = angle(E_f);这种方式的好处是你能完全控制归一化和参考面,坏处是要非常小心。时域监视器的长度直接决定频域分辨率,如果监视时间不够长,低频部分会严重失真;而FDTD的自动关闭(autoshutoff)阈值决定了时域仿真跑多久,我一般建议把auto shutoff level从默认的1e-5调低到1e-6甚至1e-7,尤其是锐利谐振结构,否则谐振尾部的长时间衰减没包含完,提取出的相位在谐振附近会明显失真。
另一个常见错误是直接在某个空间点取相位,然后拿它代表整个光栅的反射相位。对于光栅结构,近场有非常强的周期调制和倏逝波成分,单点数据根本不能代表模式的反射响应。正确做法是用“模式展开”或“端口功率归一化”等方法,只取基阶反射模式的复振幅。在Lumerical里,最贴近这个做法的是在入射端口设置模式源,并在光源同一位置放监视器,利用模式展开来提取反射系数。
3. COMSOL提取反射相位的完整实践路径
3.1 几何建模与边界条件的选择逻辑
在COMSOL里做光栅反射,几何就是一层或几层周期性结构加基底。建模本身不复杂,关键是边界条件的选择思路。
研究电磁波频域接口(Electromagnetic Waves, Frequency Domain)时,场分量分为面内分量和面外分量,这决定了你是在TE还是TM偏振下工作。几何上,建议把入射面建在x-z平面,结构在x方向周期、z方向为入射方向、y方向拉伸(二维模型)或有限厚度(三维模型)。
边界条件这里有几层要考虑:
- 周期性边界条件(Periodic Condition,现在新版叫Periodicity):沿光栅周期方向(比如x方向)设置,如果斜入射还需要在周期边界上施加Flouquet周期,也就是Bloch周期条件,相位因子要按入射角计算。
- 端口边界条件(Port):在结构上方的入射面和下方的出射面各设一个端口。入射端口设置为“激活端口”,指定入射功率、波类型和入射角;出射端口设置成“开启端口”,允许能量离开计算域。端口自带阻抗归一化,可直接输出S参数。
- 完美匹配层(PML):如果不想用端口(比如只想看近场分布),可以在上下边界加PML吸收入射和反射波,但相位提取会麻烦很多,不如直接用端口。
对于一个光栅反射问题,我推荐“双端口+周期边界”的组合,原因很简单:COMSOL端口自带S参数计算和阻抗归一化,你几乎不费吹灰之力就拿到复数S11和S21,反射相位就是arg(S11)。这是COMSOL相比FDTD的一大便利。
3.2 端口设置与S11相位输出的注意事项
端口边界条件在COMSOL里默认输出的S参数,实际上是一个与参考阻抗相关的复数。默认参考阻抗是z0 = sqrt(μ/ε),这个值在空气里约等于377Ω,但在介质里要注意,如果介质有损耗或色散,参考阻抗不再是实数,S11的相位会叠加一个额外的旋转。
取相位时我建议直接使用COMSOL的派生值计算:
arg(S11) = atan2(imag(S11), real(S11))
在“派生值”里新建一个“全局计算”,表达式输入arg(sParam.S11)或类似的实际内部变量名(不同版本变量名略有差异),然后扫描波长,就能直接得到相位随波长的变化曲线。
这里最大的坑在于参考面。COMSOL端口的S11参考面默认在端口边界处,如果你的端口平面没有恰好放在光栅顶面,相位就会多走一段传播路径。这个传播相位同样要按Δφ = 4π·n·d/λ补偿回去,和Lumerical完全一样。
此外,COMSOL里端口的入射波定义有多种方式(包括用户自定义模式),在光栅反射场景下默认的“面内波”或“面外波”类型要和你的偏振选择一致。很多新手在TE模下建模,却选了面外波模式,结果反射率差别非常大,相位自然也全错。建议在端口设置里明确选择“面内电场”或“面外电场”,并对照理论上的菲涅尔反射率做一次自检。
3.3 通过电场分量手工提取相位的方法
除了S参数,你有时还需要从近场数据提取相位。比如要画某个截面的相位分布,或者S参数提取和你手算的端口方法对不上时,手工从电场分量提取可以帮你排查问题。
方法是在结果里创建一个“二维截线”或“二维截点”,导出该位置在某一偏振方向上的复数电场E_x,然后用以下方式计算局部相位:
phase_local = atan2(imag(ewfd.Ez), real(ewfd.Ez))注意,这里直接取某个位置的相位,通常会包含驻波效应——入射波和反射波干涉产生的空间振荡。要还原反射波的固有相位,需要做“行波分离”。在COMSOL里可以通过设置两个相距一定距离的点或一条线上的场值,用传递矩阵法分离出前向波和后向波的复数幅值,再用后向波的相位作为反射相位。这是最严谨的手工方案,适合自建端口模式类型或非常规结构。
如果只用端口法,这个步骤可以跳过。建议第一次做时同时用两种方法对拍一下,如果两个相位结果能对上一部分,说明模型设置基本没有问题。
4. 两套软件结果对比,如何对上相位?
4.1 归一化与参考面是头号嫌疑
我见过最多的“为什么Lumerical和COMSOL相位不一样”的案例,最后查下来基本都不是算法问题,而是参考面对不齐。Lumerical的S11参考面在监视器平面,COMSOL的S11参考面在端口界面,两个面离真实光栅顶面的距离很可能不同。
解决办法是设计一个“参考面平移函数”,把两边都换算到光栅顶面。设定d1为Lumerical监视器到光栅顶面的距离,d2为COMSOL端口到光栅顶面的距离,则统一后的相位为:
φ_corrected = φ_original - 4π·n·d/λ
注意这里的符号取决于你定义“向上为正”还是“向下为正”。如果参考面在光栅上方,光从上方入射,反射波向下传播,参考面从上方移到光栅表面意味着反射波提前了一段距离,相位要减去传播相位。在我自己的脚本里,我会专门写一个函数处理这个修正:
def shift_reference(phase, d, n, lam): # d: 参考面到目标面的距离,正值表示参考面在目标面上方 return phase - 4 * np.pi * n * d / lam用这个函数把两边的相位都修正到同一个参考面,然后对比,通常就能看到很好的重合。
4.2 用基底平面反射做基准自检
在正式对比光栅结构之前,强烈建议先跑一个“已知解析解”的基准模型。最简单的基准就是“无光栅平面界面”,比如空气到玻璃的平面反射。这个结构有菲涅尔公式的解析解,反射率、反射相位都能手算。
先在Lumerical里用FDTD或STACK算这个平面界面,提取反射相位;再用COMSOL建同样的平面界面,提取S11相位。两边统一参考面后,应该都和菲涅尔公式符合。这一步能快速暴露“入射方向定义、偏振定义、端口方向、符号约定”等系统性偏差。
我记得自己第一次做这个自检时,发现Lumerical和COMSOL给出的相位正好差了π。排查了半天,发现是入射方向的正方向定义不同——一个默认入射光沿z正方向,另一个建模时端口法向取反了。这种π级别的系统性偏差,如果没有基准模型对拍,单看光栅结构几乎不可能定位。
4.3 Lumerical与COMSOL相位提取要点对比
| 对比项 | Lumerical FDTD/STACK | COMSOL Frequency Domain |
|---|---|---|
| 求解原理 | 时域有限差分/严格耦合波分析 | 频域有限元 |
| 宽带获取 | 单次仿真宽频 | 逐频点扫描 |
| 相位入口 | S参数分析组 / 场监视器复振幅 | 端口S11复数 / 场分量atan2 |
| 参考面位置 | 监视器平面 | 端口边界 |
| 斜入射支持 | 有周期Bloch边界/Grating | 周期条件加相位因子 |
| 谐振结构处理 | 需要足够仿真时间 | 需要足够细的频点分辨率 |
| 多物理场扩展 | 弱 | 强 |
| 归一化风险 | 需手动补偿监视器距离 | 需注意参考阻抗和端口方向 |
这张表基本覆盖了你在两个软件之间切换时需要检查的所有关键点。我的习惯是在Lumerical跑参数扫描、找趋势,然后用COMSOL做精细的多物理场耦合和交叉验证,两边相位对上之后,这个结果我才敢拿去给后面的系统设计用。
5. 常见问题与排查技巧实录
5.1 相位在±π处跳变
这是最常见的现象,原因是角度表示的周期性。相位超过π后,atan2会返回-π,导致曲线看起来很刺眼的“跳变”。这不一定是错误,但如果你要拿相位做进一步计算(比如求群延迟或拟合),就必须做相位展开(unwrap)。
在MATLAB或Python里:
import numpy as np phase_unwrapped = np.unwrap(phase) # 按默认的π阈值展开麻烦的是,如果你的原始相位不是从接近0开始,而是一开始就在π附近,直接unwrap可能展错方向。我的建议是提取原始相位时用连续复数r而不是模值和角度存储,这样任何时候都可以无歧义地恢复角度。如果在后处理时发现跳变发生在π附近,可以在unwrap之前先把相位乘2、再unwrap、再除2,避免数据点在边界来回抖动。
5.2 反射率对但相位对不上
反射率对说明模值基本正确,相位不对说明复数谱的整体算术平均旋转出了问题,通常是参考面或符号约定问题。先检查两个软件的入射方向定义,再检查参考面位置,然后检查端口的参考阻抗是否统一。如果这些都排除了,再考虑网格或频率分辨率。
FDTD的谐振结构相位对不上的另一个原因是仿真时间不够长。反射率可能已经收敛了,但相位还对不上,因为反射率的模值对尾部的弱信号不敏感,而相位对整个时域信号的细节敏感——相位受所有时刻的贡献影响,尾部如果截断了,相位可能会偏差。这时候把autoshutoff阈值调低,或者手动增加仿真时间。
COMSOL这边则要小心频点扫描步长。在谐振峰附近,相位会急剧变化,频率扫描太粗会把尖锐的相位旋转“磨平”。我一般会在谐振波长附近把频率步长加密到原步长的1/10甚至1/50,才能得到平滑的相位曲线。
5.3 膜层厚度和网格误差导致的相位偏差
相位比反射率对膜层厚度更敏感。举一个我实际调过的例子:设计SiO2/Si3N4多层反射镜,在两套软件里反射率曲线几乎完全重合,但反射相位差了约10°到20°。后来检查发现原因是两套软件对膜层厚度的定义差了一点点,虽然只有1-2纳米,但在相位上就体现出来了。
这种厚度误差在实际加工中更是常事。所以当你把仿真相位和实验测量对比时,一定要做厚度敏感性分析:在±1nm范围内扫描膜层厚度,看相位变化多少,然后选择与SEM实测厚度一致的仿真参数来提取相位。另外网格尺寸也会引入相位误差,FDTD里我习惯把光栅区域和膜层界面附近的网格加密到约λ/(20n)甚至更细,COMSOL里则在膜层厚度方向至少划分5层以上网格,才能保证相位曲线稳定。
5.4 多层膜和光栅混合结构的结果验证
对于既有多层膜又有光栅的结构,比如DBR光栅、带光栅的VCSEL反射镜,我强烈建议用“分层验证”策略:
- 先只仿真没有光栅的多层膜,和Lumerical STACK或COMSOL的1D平面膜系结果对比,确认相位基准。
- 只仿真光栅但去掉膜层,和光栅的解析或文献结果对比。
- 最后把两者合在一起完整仿真。
这个策略看起来很笨,但排查问题非常快。我有一次合在一起怎么都对不上,最后发现是COMSOL里膜层材料的有损折射率复数符号用错了(虚部正负号),导致光在膜层里指数增长而不是衰减。这种错误如果不分层验证,单看总反射率几乎看不出来,因为膜层对反射率的贡献远大于这个损耗项。
6. 我的常用后处理流程与总结
6.1 从仿真到设计的自动化相位输出
做了一堆提取后,最终还是要落到工程使用。我的固定流程是把两个软件的结果都导出为CSV或JSON格式,然后用Python统一处理,输出三个标准文件:反射率谱、反射相位谱(unwrap后)、复数反射系数(实部虚部)。这样后续做谐振腔设计或超表面相位分布设计时,可以直接加载数据。
处理脚本的核心逻辑就是参考面修正加unwrap:
import numpy as np import pandas as pd def process_phase_from_csv(file_path, d_ref, n_ref, wavelength): data = pd.read_csv(file_path) phase_raw = np.angle(data['S11_Re'] + 1j * data['S11_Im']) # 从复数恢复角度 phase_corrected = phase_raw - 4 * np.pi * n_ref * d_ref / wavelength phase_unwrapped = np.unwrap(phase_corrected) return wavelength, phase_unwrapped参数d_ref来自仿真记录,n_ref是参考面附近介质的折射率。把Lumerical和COMSOL的原始输出都喂给这个函数,输出结果直接对比图一画,有没有系统偏差一目了然。
6.2 一个最容易被忽视的小建议
我强烈建议在你的仿真项目文件夹里建立一个“REFERENCE”目录,专门放每次仿真的参考面位置、监视器坐标、端口边界坐标等元信息。这个习惯救过我很多次——隔了两周再回去看一个仿真,完全忘了当时的S11是参考在哪里,元信息记录能帮你省下大半天核对时间。
6.3 根据个人经验的一点体会
做了很多光栅反射相位提取之后,我的体会是:Lumerical和COMSOL哪边更准并不重要,重要的是把“参考面、归一化、符号约定”这三件事搞清楚。这三件事没理顺,再精密的求解器也会给出互相矛盾的相位;理顺之后,两套软件的结果通常能在1%以内的误差范围重合。如果你在实际操作中遇到相位对不上的问题,不要急着怀疑求解器,先检查这三个因素,大概率就能找到原因。后面我还会写一篇关于相位展开与群延迟计算的实操文章,到时候再把这部分内容深化,欢迎继续关注。