☰
基于Comsol与Matlab的一维光子晶体Zak相位计算全流程
2026/9/26 7:22:25 网站建设 项目流程

最近在折腾一维光子晶体的Zak相位计算,从Comsol建模到Matlab后处理来回倒腾了大半个月,总算把整个流程跑通了。这活儿说难不算难,但坑是真的多——Comsol里导出场数据的手续、Matlab里相位积分的数值处理、能带交叉时的模式排序,每一步都可能让你卡上半天。写这篇文章是想把这套“Comsol算场、Matlab算拓扑”的组合拳完整记录下来,给正在做光子晶体能带拓扑、或者刚接触拓扑光子学的同学一个可以直接照着抄的流程。

先交代一下背景:Zak相位是一维周期系统中布洛赫模态在布里渊区内积累的Berry相位,它直接决定了光子晶体的能带拓扑性质——Zak相位是0还是π,决定了你在两种不同拓扑性质的光子晶体界面处能不能观测到拓扑保护界面态。这东西在SSH模型里对应着二聚化的拓扑不变量,在一维光子晶体里就是判断能带是否“非平凡”的关键。算Zak相位的主流方案有两种:一种是用全数值方法直接从色散关系反推,另一种是解析求解传输矩阵再积分。但如果你想把计算建立在真实材料参数和真实几何结构上,比如考虑色散、损耗、复杂单元胞,那就绕不开有限元仿真——Comsol负责给本征场,Matlab负责拓扑不变量计算。

这篇记录比较适合三种人:一是课题组正在做一维光子晶体拓扑界面态、还没搞定计算流程的同学;二是想学Comsol和Matlab协同工作流、但不知道从哪下手的研究生;三是对Zak相位数值算法感兴趣、想验证自己解析结果的科研狗。文章里的所有步骤我都用具体例子跑过一遍,直接抄作业就行。

1. 理解Zak相位:一维光子晶体拓扑性质的“身份牌”

1.1 Zak相位在光子晶体里到底代表什么

Zak相位本质上是个几何相位,1989年由Zak在固态物理的框架下提出,用来描述晶体中布洛赫电子在k空间里走一圈后积累的相位。对光子晶体来说,第n条能带对应的布洛赫模式满足:

u_nk(x+a) = u_nk(x),其中E_nk(x) = u_nk(x) e^{ikx}

Zak相位的定义是布洛赫函数u_nk在布里渊区内沿k方向的Berry联络积分:

θ_n^Zak = i∫_{-π/a}^{π/a} ⟨u_nk | ∂_k | u_nk⟩ dk

这个积分的结果在时间反演对称性保护下是量子化的——不是0就是π(或者等价地,±1的Z₂不变量)。为什么量子化?因为一维光子晶体具有空间反演对称性(通常我们设计高/低折射率交替层时都会保持镜像对称),此时布洛赫函数在k=0和k=π/a处具有确定的宇称,Zak相位必须取离散值。

类比一下:Zak相位之于一维光子晶体,就像陈数之于二维拓扑绝缘体。它们是拓扑不变量,不随微扰连续变化,只有在能带闭合(带隙消失)时才会跳变。两个Zak相位不同的光子晶体拼在一起,在界面处必然出现带隙内的局域模式——这就是拓扑保护界面态,光会被“锁”在界面上。所以计算Zak相位是判断一个一维光子晶体能否承载拓扑边界态的前提。

1.2 为什么选Comsol + Matlab的组合

研究一维光子晶体能带,很多人习惯用传输矩阵法或者平面波展开法,解析且快速。但真实项目里经常碰到这样的需求:单元胞里有色散材料、有缺陷层、有增益/损耗、甚至是一个任意形状的周期结构。这时解析方法就力不从心了,有限元仿真几乎是唯一出路。

Comsol的优势在于几何建模灵活、材料参数库丰富、边界条件(尤其是Floquet周期性边界)设置方便,能直接算出色散关系(能带图)和每个k点对应的本征模场分布。但缺点是:它内置的后处理对拓扑不变量计算支持比较弱,Zak相位这种东西Comsol没给现成的计算模块,你需要把本征场导出来,在外部做数值积分。

Matlab在这里就是“计算大脑”:读入Comsol导出的本征场数据,实现Wilson loop算法(Zak相位的数值离散形式),处理模式排序、相位对齐、积分累加这些脏活。把两者结合,你既能利用Comsol的几何灵活性,又能用Matlab自由实现任意拓扑不变量算法,这套工作流也可以顺手扩展到二维的陈数、Z₂不变量计算。

我试过直接用Comsol的全局评估表达式去定义积分变量,但一来公式表达受限,二来处理复数相位相当麻烦,尤其是跨k扫描时模式会自动重排,Comsol里根本没法做智能的模式追踪。所以别在Comsol里硬刚,老老实实导出场数据,交给Matlab处理。

2. Comsol侧实操:建模、参数扫描与本征场导出

2.1 单元胞建模:一维结构其实要画二维

一维光子晶体字面上是“一维”,但电磁波仿真时几何至少是二维的——我们研究的是在x方向周期变化、y方向无限均匀的平板结构,电磁波沿x方向传播。在Comsol中建一个二维模型:画一个长方形单元胞,宽度等于周期a,高度取任意值(通常取一个长度量纲,比如1 μm,方便归一化)。

我在例子里用的是最经典的高低折射率交替层:高折射率层n_H=3.5(类似硅或砷化镓),厚度d_H=0.25a;低折射率层n_L=1.0(空气或二氧化硅),厚度d_L=0.75a。周期a=1 μm。这个结构的优点是折射率对比度足够大,带隙明显,拓扑性质随频率变化清晰,非常适合用来验证算法。

物理场选择“电磁波,频域”(Electromagnetic Waves, Frequency Domain)。如果研究横磁模(TM,磁场沿z方向),方程是标量的;研究横电模(TE,电场沿z方向)也一样。这里以TM模式为例,主变量是磁场z分量Hz,控制方程是:

∇ × (1/ε_r ∇ × Hz) - k₀² Hz = 0

边界条件很关键:在x=0和x=a的左侧/右侧边界上设置周期性条件。Comsol里的操作是:右键“电磁波,频域”节点,添加“周期条件”特征,选择“Floquet周期”,指定布洛赫波矢k的x分量为kx,y分量为0。注意Comsol的Floquet边界条件是作为边界对(Boundary Pair)实现的,你需要在边界选择里同时勾选x=0和x=a两条边界线,它会自动识别成源/目标边界对。

有个细节很多人第一次会忽略:周期性边界要求左右两侧边界上的网格剖分完全一致,否则Floquet边界条件会通过插值强行匹配,虽然能算但会引入数值误差。建议在网格设置里,先对左侧x=0边界的边(Edge)做一次“固定单元数”剖分,然后对右侧x=a边界用相同的单元数。最佳实践是直接用“映射”(Mapped)网格,指定x方向n_x个单元,y方向n_y个单元——映射网格天然保证左右边界节点一一对应。

2.2 特征频率研究 + 辅助扫描k点

Zak相位积分需要对布里渊区内的连续k点采样,每个k点解一次特征值问题。Comsol的做法是在“研究”中添加“特征频率”研究,然后打开“辅助扫描”(Auxiliary Sweep),把k_x设为全局参数(参数维度是3)。

实操参数:k_x从-π/a到π/a线性扫描,取N_k=31个点,步长Δk=π/(15a)。步长要权衡:步长太大,Wilson loop的相积累会有较大离散误差;步长太小,计算时间成倍上涨。经验值是N_k取30到60之间,对一维光子晶体足够收敛。我在例子里先用粗扫描(31点)快速验证流程,再加密到61点出最终结果。

扫描设置里记得选“所有组合”而不是“参数切换”,确保每个k点都得到独立的特征频率解。另外特征频率的研究设定里可以勾选“所需特征值数量”,比如算前4条能带就填4,这样Comsol会输出前4个本征模(按频率从小到大排序)。

求解器方面,用默认的特征值求解器(默认MUMPS或SPOOLES)就行。有一个经验:如果算到高k时出现收敛困难,检查一下特征频率搜索基准值——在“特征频率研究”的设定里把“期望特征频率”基准值设为2πc/(λ_0),λ_0取你关心的中心频段对应的波长,能减少求解器漏根的风险。

2.3 导出本征场数据:注意实虚部分开导出

坑最多的环节在这里。Comsol的默认导出功能导出的是“解”的物理量,但复数场在“派生值”里查看时,默认显示的是实部(或模)。你要导出完整的复数场数据,必须分别导出实部和虚部,然后在Matlab里重组成复数。

我的做法是:右键“派生值” → “线评估”(Line Evaluation),选择x=0到x=a的单元胞中线(注意,一维Zak相位内积是在整个单元胞上积分,不是只在边界上积分——所以这里的“线”其实是代表整个二维单元的x方向截面,y方向积分在Matlab里做)。等等,这里必须澄清一个细节——对于二维模型,本征场u(k)是x和y的函数,内积的积分域是二维单元胞面积(x方向从0到a,y方向从0到h)。所以在Comsol里导出的应该是“体”(二维面)上的场分布。实际操作是“派生值” → “表面评估”,选择整个单元胞域。

由于二维问题y方向通常取均匀场(一维光子晶体的本征模在y方向没有变化),理论上这个维度不会影响结果,但为了严谨我们还是做二维积分。

导出时选择“表达式”:第一列写x坐标(x),第二列写Hz的实部(实部表达式为Re(Hz),Comsol里直接输入re(Hz)),第三列写Hz的虚部im(Hz),第四列写y坐标(y)。文件名按k点索引命名,比如field_k000.txt、field_k001.txt……格式选“文本”或“CSV”。这里注意导出设置里“要包含的网格点”选“所有网格点”,否则输出的场数据稀疏,Matlab里做不了精确积分。

还有一个更高效的办法:如果你用的是Comsol的LiveLink for MATLAB模块,可以直接在Matlab里用mphgetu和mphinterp命令动态提取场分布,省去手动导出几十个文件的麻烦。但大多数人没有LiveLink许可,所以文章后续以txt导出流程为主,两种方法在Matlab侧的处理逻辑完全一样。

3. Matlab核心算法:从本征场到Zak相位的数值实现

3.1 Wilson loop离散化:为什么不用导数积分

Zak相位的定义式里有∂_k u_nk这一项,直接数值微分极不靠谱——Comsol导出的场数据在网格上是离散的,k方向采样也稀疏,用有限差分估计导数会放大数值噪声,导致相位结果抖动剧烈。所以实际计算采用Wilson loop方案,把积分转化为相邻k点布洛赫函数的内积:

θ_n^Zak = -Im ln( ∏_{j=1}^{N_k-1} ⟨u_n(k_j) | u_n(k_{j+1})⟩ )

也就是说,把布里渊区切分为N_k个点,相邻两个k点的本征态做复内积,得到一组复数(每个复数有一个幅角),把所有幅角累加起来,取负虚部,就是Zak相位的数值近似。当N_k足够大时,这个离散化精确逼近连续积分。用内积连乘的好处很明显:每个因子是O(1)的复指数,幅角小,不会出现求导带来的巨大数值波动。

这里要提醒一句:上面公式写的“u_n(k_j)”指的是同一能带(第n条)在不同k点的本征态。如果k_j处的第n条模式和k_{j+1}处的第n条模式不是同一个物理模式(能带交叉导致排序混乱),内积会算错。所以Matlab代码里最重要的一步是能带追踪(mode sorting)。

3.2 代码实现:模式排序、相位对齐、积分累加

整个Matlab脚本我拆成三步走。

第一步,读取所有场数据并做模式分类:

% 参数设定 a = 1e-6; % 周期,单位m Nk = 61; % k点数量 modes = 4; % 需要的能带数目 kx = linspace(-pi/a, pi/a, Nk); % 预分配 field = cell(Nk, modes); % 存储每个k点、每个模式的本征场 freq = zeros(Nk, modes); % 存储本征频率 for ik = 1:Nk for im = 1:modes fname = sprintf('field_k%02d_mode%d.txt', ik-1, im-1); raw = readmatrix(fname); x = raw(:,1); y = raw(:,4); Hz = raw(:,2) + 1i*raw(:,3); % 复数场 % 重排为二维数组(如果是二维问题) nx = numel(unique(x)); ny = numel(unique(y)); H2d = reshape(Hz, [ny, nx]); % 注意reshape顺序要与meshgrid一致 field{ik, im} = H2d; % 频率从文件名或额外文件读取 end end

第二步,能带追踪(模式排序)。Comsol每个k点输出的4个模式频率按升序排,但当两条能带交叉靠得很近时,相邻k点的模式索引可能互换。我的处理方法是贪心匹配:从第一个k点开始,对下一个k点的每个模式,计算它与当前k点所有模式的“重叠度”或“频率差”,选择频率差最小且场重叠最大的配对。

% 简单示例:按频率差贪心匹配 for ik = 1:Nk-1 freq_diff = abs(freq(ik+1, :) - freq(ik, :).'); % 4x4矩阵 % 贪心:每次取全局最小 for step = 1:modes [df_min, idx] = min(freq_diff(:)); [m_curr, m_next] = ind2sub(size(freq_diff), idx); % 确认配对后,交换freq(ik+1,:)中的顺序 % 同时交换field{ik+1,:}中的顺序 mapping = [m_curr, m_next]; % ... 交换代码略 freq_diff(m_curr, :) = inf; freq_diff(:, m_next) = inf; end end

第三步,计算Wilson loop累加相位。内积要对二维单元胞面积做积分,dx*dy的权重不能丢。如果y方向场均匀,实际上只对x方向积分即可,但我保留了二维标准积分保证通用性:

zak_phase = zeros(modes, 1); % 先做相位对齐:把每个本征态的最大幅值点相位归零 for ik = 1:Nk for im = 1:modes H = field{ik, im}; [~, idx_peak] = max(abs(H(:))); H = H * exp(-1i*angle(H(idx_peak))); field{ik, im} = H; end end % 累加内积幅角 for im = 1:modes phase_acc = 0; for ik = 1:Nk-1 H1 = field{ik, im}; H2 = field{ik+1, im}; % 面积权重 weight = ones(size(H1)); % 实际用dx*dy overlap = sum(sum(conj(H1) .* H2 .* weight)); phase_acc = phase_acc + angle(overlap); end zak_phase(im) = phase_acc; end

运行完后,zak_phase是每个能带的相位累积值。由于时间反演对称性,它应该接近0或π的整数倍。注意angle函数返回[-π, π],多个连乘相位可能跨越边界,所以实践中通常计算实部符号来判断Z₂不变量:符号为+1对应Zak相位0,符号为-1对应Zak相位π。

3.3 相位对齐与规范固定:数值实现的“隐藏门槛”

很多人第一次计算Zak相位得到完全错误的结果,八成是栽在规范问题(gauge)上。电磁场本征方程的解具有全局相位自由度:如果u_nk(x)是本征场,那么e^{iφ}u_nk(x)也是本征场,φ可以是任意实数。Comsol求解器每次求解输出的整体相位是随机的,不同k点的本征场很可能处于不同的规范下。

Wilson loop公式看似不依赖规范——单个内积⟨u(k)|u(k+Δk)⟩在规范变换下会变,但连乘的总相位在布洛赫规范下是规范不变的。然而数值上,如果不同k点的规范随机跳变,角度计算会产生虚假的π跳变。所以数值实现中必须做“规范固定”,最常用的就是我在代码里写的峰值对齐法:把每个本征场的最大幅值点强制设为实部为正。这个操作直观且稳定,因为峰值点的相位在远离节点处通常不接近0,归一化乘子不会是病态的。

另外一个更好的规范是所谓“周期性布洛赫规范”(periodic gauge):让布洛赫函数在k = π/a处等于k = -π/a处的场乘以适当的相位因子。但这在有限元离散里实现稍复杂,需要处理相位缠绕,一般后处理里用峰值对齐就足够了。我在验证时用峰值对齐法和周期性规范算出来的Zak相位结果一致(差异小于0.01 rad),所以放心用。

4. 实测中的常见问题和排查心得

4.1 网格不一致导致的能带微小畸变

第一次跑的时候,我用Comsol的“自由三角形网格”给单元胞剖分,左右边界接触的是两条独立边,虽然有周期条件,但网格位置完全不对应。结果能带图倒还算光滑,但算出的Zak相位在能带边缘(k接近±π/a时)总是不稳定,复现性差。

排查后发现是网格不对称造成的。Floquet周期条件在左右边界之间做的是点对点约束,要求两侧网格点位置严格对应。自由三角形网格在两边生成的面网格长度不同,Comsol会自动做插值,但插值误差在高阶模上会明显放大。解决方案是把网格换成“映射”(Mapped)类型:手动指定x方向单元数为100,y方向单元数为5,生成规则的四边形网格。这样左右边界的节点天然一致,Floquet约束严格成立,Zak相位结果立刻变得稳定。

4.2 模式排序混乱:特别是接近能带交叉点的能带

一维光子晶体能带图里经常出现两条能带在布里渊区边界附近交叉或者反交叉。比如算前4条能带时,第2条和第3条能带在高对称点附近有近简并区间。Comsol按频率升序输出模式,在近简并区,模式的物理形态会互换——如果不做追踪,Wilson loop里的“第n条能带”会突然跳到另一条物理能带上去,连乘结果自然错误。

模式追踪我是这么做的:先跑一个粗k扫描(Nk=31)得到完整能带图,肉眼判断哪些k区间可能简并;然后在Matlab的贪心匹配算法里同时使用“频率差最小”和“重叠积分最大”两个判据。重叠积分就是⟨u(k_j)|u(k_j+1)⟩的模方,物理上同一能带相邻k点的本征态重叠应接近1,不同能带的模式重叠接近0。用这个判据能非常可靠地处理反交叉,比单纯频率排序稳定得多。

4.3 场数据导出时的“幽灵模式”

Comsol特征频率研究会输出一些数值伪模式(spurious modes),典型特征是频率偏高、场分布破碎或者场强集中在角点。这些模式在k扫描中不会形成连续能带,在Matlab读取时表现为孤立的大频率跳变点。

排查方法:每次导出k点模式数据前,先在Comsol图形窗口里快速浏览一遍每个本征频率对应的电场模分布,看到场不是沿x方向平滑变化、而是角落出现尖峰的,直接忽略,在能带追踪前把它从数据中删掉。另外,在高折射率层内部特别容易产生表面等离激元类的局域数值模式——如果模型包含色散材料,这种伪模式更多,处理方式同上。

还有一个数据对接的通用技巧:在Comsol导出的文本文件里,列的顺序不要依赖默认排列,导出表达式时手动指定每列内容(x坐标、y坐标、实部、虚部),并在Matlab里严格按列名读取。我吃过一次亏:默认导出文件会额外带上网格质量、体积之类的列,导致Matlab读错列,算出来的相位完全没规律。

5. 结果验证与后续扩展

5.1 界面态仿真:Zak相位计算结果的“试金石”

算完Zak相位,怎么确定结果可信?最直接的验证方法是构造一个界面:把Zak相位为0的光子晶体片段放在左边,Zak相位为π的光子晶体片段放在右边,然后在Comsol里做频域仿真,看带隙内是否出现局域在界面上的透射峰/反射谷。

我在例子里验证过:当d_H/d_L改变使第二条能带的Zak相位从0变为π时,将两个不同参数的光子晶体拼接,在带隙频率处扫描入射平面波,果然看到界面处形成了强局域电场峰。这个现象与SSH模型里的拓扑界面态完全对应。

如果不想做全波仿真,也可以退一步用一种较轻量的验证:用传输矩阵法(TMM)算反射相位,观察反射相位随频率绕圈数——反射相位的绕数与Zak相位有严格的拓扑对应关系(一维光子晶体的反射相位绕数等于Zak相位除以π)。这样你可以用解析的TMM结果反过来检验Comsol+Matlab数值计算,两条独立路径都指向同一结论时,基本可以确定数值结果没算错。

5.2 这套流程还能迁移到哪些地方

Zak相位只是拓扑不变量的一种。这套“Comsol提取本征场 + Matlab算Wilson loop”的工作流可以无缝迁移到好几个方向:

  • 二维光子晶体的陈数计算:同样的场导出流程,Wilson loop在二维布里渊区做封闭路径积分,得到陈数。
  • 谷光子晶体的谷陈数:需要导出两条谷能带的场,方法一致。
  • 具有增益/损耗材料的光子晶体:本征场是复数的复数,Zak相位的量子化可能被破坏,但数值算法不用改。
  • 声子晶体或弹性波周期结构:把电场改成位移场,方程换成弹性波动方程,Comsol的物理场接口相应切换,后续Matlab处理逻辑完全不变。

我在实际项目中已经把这套流程的第二维版本(陈数计算)跑通了,逻辑上就是Wilson loop从一维路径变成二维闭合路径,在布里渊区网格上做路径积分,其余处理几乎一模一样。所以这次一维Zak相位的经验算是打基础,后面扩到二维会顺畅很多。

最后分享一个小技巧:整个计算流程跑完一遍之后,建议把k的采样点加密一倍再算一次,对比两次的Zak相位结果。如果加密前后结果一致(相差小于0.05 rad),说明数值收敛没问题;如果结果跳变了1左右,通常意味着模式追踪在某个k区间选错了配对,回到第4.2节检查那个区间的重叠积分就行。这个收敛性检查花不了几分钟,但能帮你挡掉八成以上的“假拓扑”结果。

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

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

立即咨询