我先把话说在前头:这个项目名字听起来挺唬人,但本质上就是一件事,把一维光子晶体在给定晶格参数下的能带结构算出来,再沿着布里渊区扫描得到布洛赫本征态,最后用 Wilson 线的思路把每一条能带的 Zak 相位积分出来。整个过程最难的不是 COMSOL 建模,也不是 Matlab 写公式,而是两个软件之间的数据衔接、相位规范处理,以及能带交叉时的模式追踪。我做完这一整套之后最大的感受是:物理上没多难,工程上全是细节。
这篇文章我会把这趟从 mph 文件到 Matlab 脚本的完整路径写清楚,包括 COMSOL 怎么设置 Floquet 边界条件、怎么把每个 k 点的本征场和坐标导出来,再讲 Matlab 端怎么离散化算 Zak 相,最后把我在实际跑数据时踩过的一组坑整理成排查清单。适合刚接触光子晶体拓扑计算的研究生,也适合做超材料或声学系统、光学微腔计算的人参考。算 Zak 相这件事,原理公式十行就能写完,但只要你漏掉一个周期性规范,结果就会差出一个 π,整个过程就是这么敏感。
1. 从 Berry 相到 Zak 相:到底在算什么,以及为什么必须 COMSOL 加 Matlab 双人搭档
1.1 先花三分钟把 Zak 相的定义彻底捋清楚
先说物理背景。Zak 相本质上是 Berry 相在周期性体系里的特殊形态,它描述的是布洛赫波函数绕完整个布里渊区一圈后,累积的几何相位。具体定义大家都很熟悉:
[
\theta_n = i \int_{-\pi/a}^{\pi/a} \langle u_{nk} | \partial_k u_{nk} \rangle , dk
]
这里的 (u_{nk}) 是布洛赫模的周期部分,也就是满足 (u_{nk}(x+a)=u_{nk}(x)) 的那个函数,(n) 是能带序号。很多入门资料会告诉你 Zak 相只有 0 和 π 两个取值,前提是体系具有空间反演对称性。一旦体系失去了某个关键对称元素,Zak 相就会变成任意的连续值。一维光子晶体里最常见的讨论对象是 0 相和 π 相的交替,这决定了能带边缘是“有效电介质型”还是“表面阻抗型”,进而影响界面态是否存在。
在数值计算里,我们通常不会直接对 (k) 做解析积分,而是把整个布里渊区离散成 (N) 个点 (k_1, k_2, \ldots, k_N),然后使用 Wilson 型离散公式:
[
\theta_n = -\sum_{i=1}^{N-1} \arg\langle u_{n}(k_i) | u_{n}(k_{i+1}) \rangle
]
离散化的时候有一个隐含前提是相邻两点之间的相位差足够小,不能跳过一个 2π。如果布点太疏,内积相位接近 ±π,这时 unwrap 和累加都会出问题。这条公式看起来很朴素,但它在数值实现时对应着一整套操作:在 COMSOL 里把布洛赫函数算出来,然后把它们拿到 Matlab 里做高维内积,再沿着 k 路径累加相位。
1.2 一维光子晶体:验证和展示 Zak 相的天选体系
为什么这个题目要落在“一维”上,而不是二维三维?因为一维体系结构最简单,能带基本上是二维可视化,Floquet 边界条件也只有一个方向的波矢分量要扫。一维光子晶体就是两种折射率材料交替周期排列,原胞里厚度分别记作 (d_A) 和 (d_B),晶格常数 (a=d_A+d_B)。只要给定折射率比和占空比,就能算出前几条能带。
光子晶体的本征方程和 Schrödinger 方程在数学结构上有对应的关系,所以电子能带论里面的 Berry 相、拓扑不变量这些概念都可以平移过来使用。但光子体系直接得到一个额外的便利:不需要考虑费米能级,能带的填充完全由你关注的频率范围决定。这条性质在实验解释时特别舒服——你想研究哪条带,把频率调过去就行。
我在实际建模时用的参数是 (d_A=0.5a)、(d_B=0.5a),材料 A 取折射率 1.0(空气缝隙),材料 B 取折射率 3.5(硅层),晶格常数 (a=1,\mu m)。这样做的好处是计算时间短,归一化频率 (a/\lambda) 的色散图非常漂亮,而且前两条带的 Zak 相恰好有清晰的 0/π 切换,方便对照理论。
1.3 为什么不用纯 COMSOL 一条龙,也不建议纯 Matlab 梭哈
单论画能带,纯 COMSOL 已经能出漂亮的色散曲线。但 Zak 相不是能带图上的直接输出量,它需要你拿到本征函数之后,在连续 k 点之间做内积和相位累加。COMSOL 对特征值和场分布的后处理能力很强,但对这种“跨参数模式追踪 + 相位累积”的专业操作,脚本化程度不够灵活,每次改动规范就要重新搭建一堆节点,效率低。
纯 Matlab 传家宝式手写一维光子晶体也是可行的,画几条色散曲线非常快,严格说也能算 Zak 相。难点在于当你后面想扩展:加入材料损耗、色散介质、非线性材料、或者把 1D 结构改成 2D 波导阵列时,手写代码的成本会迅速失控。COMSOL 负责精确建模和高效求解 PDE,Matlab 负责灵活后处理和相位算法,两者分工是最舒服的搭配。所以我选择了 COMSOL 建模仿真 + Matlab 数值后处理这种混合工作流。mph 文件承担中间交换媒介,Matlab 用 Livelink 直接打开,工程链路很省事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. COMSOL 端建模:从 mph 文件开始搭一维光子晶体
2.1 几何、材料和求解域设置
打开 COMSOL,组件选择二维空间维度,物理场接口用“波动光学”模块里的“电磁波,频域”,研究类型选择“特征频率”。二维模型意味着整个结构在面外方向(z 方向)是无限延伸的,因此可以计算 TE 或 TM 两种面外极化模式。我在这里只关注电场面外分量,变量名是 Ez。选择哪种极化不影响 Zak 相的计算流程,你只要保证后续导出的场分量和参与内积的权重一致就行。
几何构建特别简单,不需要把整个周期阵列拉出来,只需要建一个原胞。原胞由两个矩形组成:左边矩形宽度 0.5 μm,材料设为空气;右边矩形宽度 0.5 μm,材料设为硅层。在“材料”节点里直接定义折射率实部,空气 n=1,硅 n=3.5。所有介质透明度设成 1,忽略吸收,这样求出来的特征频率才是实数,特征函数也好归一化。
网格设置要保证每个介质层内部至少有 6 到 8 层网格。因为一维光子晶体的本征场在界面上变化剧烈,网格太粗会导致内积中低频误差被放大,最后 Zak 相的小数部分就飘了。我用的是自由三角形网格,最大单元尺寸设置为 (0.04,\mu m),这个尺度对 0.5 μm 厚的层来说已经足够精细,求解时间仍然在秒级。如果后期你换到高折射率对比材料,比如折射率高达 4 以上的,建议把最大网格尺寸再降到 (0.02,\mu m),尤其是验证表面态或者计算反射相位的时候。
2.2 Floquet 周期边界条件和布洛赫波矢扫描是建模核心
一维周期性结构需要把原胞左右两条边界设置为 Floquet 周期边界,也就是布洛赫周期边界条件。在 COMSOL 中选中模型左边界作为源边界,右边界作为目标边界,指定波矢分量 (k_x)。这里有个细节,对于一维周期结构,周期边界条件只需要成对的边界双方就行,上下边界不需要特殊处理,因为面外无限延伸结构天然没有边界。
要让 (k_x) 成为扫描变量,我的做法是定义一个全局参数 k0,代表约化波矢,范围从 -0.5 到 0.5,那么真实的波矢就是 (k_x = 2\pi/a \cdot (-0.5+\text{k0}))。在 Floquet 周期边界条件设置框里,直接把波矢 x 分量填成 2*pi/(dA+dB)*(-0.5+ k0)。然后在研究节点里增加参数扫描,扫描变量设为 k0,扫描范围从 -0.5 到 0.5,步长 0.005。这个步长对 Zak 相计算已经足够,每次求解的特征频率数量设定为 6 条,关注前两条能带时留了 4 条富余。
这个名义上的“参数扫描”是为了拿到不同布洛赫波矢下的本征态。需要注意,COMSOL 的特征频率求解器在每个 k0 扫描步都会独立求解一次,每次算出的特征向量会带随机全局相位。这个全局相位是后续计算 Zak 相最大的坑,等到了 Matlab 端必须做相位对齐处理,不然算出来的相位完全没有意义。
2.3 能带数据的验证:先画出色散图再往下走
继续往下走之前,我强烈建议先检查能带长什么样。在“派生值”节点里添加“全局计算”,选择表达式为 freq 或特征频率的角频率 ω,对每个扫描参数记录前几条本征频率,导出成表格,然后在 Matlab 里画 ((k_x, \omega a / 2\pi c)) 的能带图。
以我的参数为例,第一带和第二带之间会有一个明显的光子带隙,带隙在归一化频率大约 0.5 到 0.7 之间。如果你画出的能带在边界处不闭合,或者存在毛刺,大概率是 Floquet 边界条件的波矢方向填错了,或者参数扫描的表达式用了扫描变量本身但没有把它乘入波矢表达式。这一步检查绕不开,因为后续一切 Zak 相物理都依赖这个能带是否正确。
检查完能带没问题,就可以进入导出环节了。这里要格外注意:COMSOL 导出的场量默认是完整布洛赫函数 (\psi_k(x)),而不是周期部分 (u_k(x))。在 Floquet 周期边界条件下,COMSOL 的因变量包含了平面波因子 (e^{ik_x x})。理论上,计算 Zak 相应该用周期性函数 (u_k(x)=\psi_k(x)e^{-ik_x x})。如果你直接拿 COMSOL 的场做内积,每两个相邻 k 点之间会多出一个由平面波因子带来的动态相位,根本对不上 Zak 相的定义。这就是为什么需要在 Matlab 端做“脱平面因子的修正”。
3. 导出细节:把 mph 里的本征场和坐标安全地搬进 Matlab
3.1 用 Livelink for Matlab 直接打开 mph,数据交换最顺畅
数据搬运有两条路线。第一条比较原始:在 COMSOL 里手动导出每个 k 点的场数据,生成一堆文本文件,在 Matlab 里用 readtable 或 load 逐个读取。适合一次性处理,步长之间切换不频繁,代码也直观。第二条是直接买 Livelink for Matlab 模块,用 mphopen 打开模型文件,再用 model.mesh、model.sol、mphinterp 这些接口在 Matlab 里取网格节点、解数据和任意表达式,省去文件转换的麻烦。
我推荐第二条路线,因为频率扫描过后,COMSOL 的“数据集”里会自然带上参数维,你可以直接用 model.result.dataset('dset1').getReal 或者更常用的 mphinterp(model, 'Ez', 'coord', coords, 'dataset', 'dset1') 一次性取多个 k0 下的场数据。每次 mphinterp 可以指定一个具体的参数值,配合循环就能把所有 k0 下的本征场取出来。这条路线最大的好处是不需要处理网格节点重新排序,COMSOL 内部插值保证每个 k0 下都使用同一套节点坐标,之后做内积非常舒服。
3.2 关键一步:取周期函数 u_k 而不是完整场 ψ_k
在 Matlab 里,假设你已经拿到 COMSOL 的求解坐标数组 coord_x、coord_y(本题主要是 x 方向),以及某个 k0 下的电场 z 分量矩阵 Ez_all,维度是 num_nodes * num_bands。接下来的一步不能漏:
[
u_{\text{periodic}} = E_z(x) \cdot e^{-i k_x x}
]
这里要用元素对应乘法逐点去除平面波相位。k_x 要换算成真实波矢值,也就是 2*pi/(a) * k0。经过这一步之后,取出来的场满足周期性边界条件 (u(x+a)=u(x)),这样才能用于 Zak 相内积。如果你在 COMSOL 里设置 Floquet 边界条件时选了“周期性”里的“Bloch 周期条件”,并且勾选了解变量为周期部分,则不需要这一步。但根据我的经验,默认情况下 COMSOL 因变量仍是全波场,所以在脚本里统一加一步“脱因子”是最稳妥的。
为了让内积在离散网格上有意义,需要带上积分权重。如果使用均匀网格且面积积分统一,可以直接用全场的积分归一化,但更稳妥的做法是使用 COMSOL 里的 prev 或 dx 权重。在一维模型的导出中,如果你导出的场在 x 方向上一列节点,且节点大致均匀,用每个节点对应的宽度权重做梯形积分即可。若要更精确,最好从 model.mesh 里取三角形元面积,然后按节点面积分配。
3.3 批量导出的组织逻辑:一个高效脚本骨架
我在实际跑流程时,会在 Matlab 里建立一个 struct 数组 phc_data,每个元素存放某个 k0 下的坐标、频率数组和本征场矩阵。整体组织逻辑如下:
matlab复制model = mphopen('photonic_crystal_1d.mph');
kList = -0.5:0.005:0.5;
dataGrid = load('coords.mat'); % 预先保存的网格坐标
for ii = 1:length(kList)
kVal = kList(ii);
% 设置参数
model.param.set('k0', num2str(kVal));
% 重新求解或在已有解上重新载入
model.sol('sol1').run;
% 取出电场实部虚部
Ez = mphinterp(model, 'Ez', 'coord', [dataGrid.x; dataGrid.y], 'dataset', 'dset1');
freq = mphinterp(model, 'freq', 'coord', [dataGrid.x; dataGrid.y], 'dataset', 'dset1');
% 维度恢复
phc_data(ii).field = reshape(Ez, [num_nodes, num_bands]);
phc_data(ii).freq = reshape(freq, [num_nodes, num_bands]);
phc_data(ii).kx = 2*pi/1.0 * kVal;
end
这段代码的关键在于 reshape 之后,矩阵的列顺序是特征值的升序。对于简单原胞、低能带,可以直接信任频率排序;但当能带交叉或简并出现时,排序不可靠,需要后续的模式追踪。
4. Matlab 端 Zak 相位数值计算:公式五分钟,调试两小时
4.1 离散 Wilson 相位公式的数值实现
现在数据都搬到了 Matlab,可以正式算 Zak 相了。按照前面的离散公式:
[
\theta_n = -\sum_{i=1}^{N-1} \arg \langle u_{n}(k_i) | u_{n}(k_{i+1}) \rangle
]
内积定义为网格上的数值积分:
[
\langle u_i|u_j \rangle = \sum_{p=1}^{N_\text{node}} u_i^*(x_p), u_j(x_p), w_p
]
式中 (w_p) 是积分权重。如果网格是均匀的,可以把权重设为常数,但更规范的方法是使用梯形积分的系数,或者从网格节点相邻间距构造。在代码上,我通常用一个列向量 weights,长度等于节点数,然后内积就是:
matlab复制inner = sum(conj(u1) .* u2 .* weights, 1);
这里有一个容易忽略的维度问题:u1 和 u2 都是列向量,权重也是列向量,三者逐元素相乘后求和,得到标量。如果 COMSOL 导出的场矩阵是多能带多节点,需要按能带切片。在 Matlab 里可以这样实现:
matlab复制num_k = size(phc_data, 2);
num_bands = size(phc_data(1).field, 2);
ZakPhase = zeros(num_bands, 1);
for n = 1:num_bands
phase_sum = 0;
for ii = 1:num_k-1
u1 = squeeze(phc_data(ii).field(:, n));
u2 = squeeze(phc_data(ii+1).field(:, n));
% 去除每个本征向量的整体全局相位之前,先做一次重归一化和相位对齐
u1 = u1 / norm(u1 .* sqrt(weights));
u2 = u2 / norm(u2 .* sqrt(weights));
inner = sum(conj(u1) .* u2 .* weights);
phase_sum = phase_sum + angle(inner);
end
ZakPhase(n) = -mod(phase_sum + pi, 2*pi) + pi; % wrap 到 [-pi, pi)
end
这里用到 mod(phase_sum + pi, 2*pi) - pi 做范围包裹,确保输出是 ((-\pi, \pi])。如果你把 phase_sum 算出来后发现不是 0 就是 π,说明体系没有破坏某个关键对称性,Zak 相确实是量子化的。
4.2 全局相位对齐:让相邻内积不再随机跳变
前面提到 COMSOL 每次特征求解都会给本征向量加随机全局相位,这是数值计算 Zak 相最大的敌人。一个很常见的表现是:你算完 phase_sum 后得到一些乱串的数字,比如 1.2、-2.8、0.9,完全看不出量子化特征。这时候就要做相位规范修复。
最常用的修复策略是“平行输运规范”:对于每一条能带,从 (k_1) 开始,记录当前本征向量 (\psi_\text{ref}),当计算到下一个 k 点时,先计算 (\psi_{k_{i+1}}) 与 (\psi_\text{ref}) 的内积,然后把 (\psi_{k_{i+1}}) 整体乘以一个相位因子,使得这个内积变成实数且大于 0。也就是说,人为把相邻两个态的相对相位“拉平”。这样累积的 arg(inner) 就是相对平滑的几何相位,最后的闭合不匹配量才等于 Zak 相。
不是所有这些人为操作都会抹掉真实的几何相位。只要所有 k 点都使用同一套相位规则连续修复,最后到布里渊区末端的相位失配量仍然会保留真实的 Berry 相位模 (2π)。所以可以放心用。代码上可以这样加:
matlab复制u2 = u2 * exp(-1i * angle(inner));
或者更优雅一点,在整个循环之前对每个 k 的每一带,使用与相邻带重叠度最高的方式重新排序并修正相位。
4.3 能带交叉和简并点的模式追踪
纯单带模型不需要模式追踪,但实际光子晶体里能带交叉太常见了。比如高对称点处第二、第三带可能简并,或者某些参数下两条能带在 k 空间内部靠近但不交叉。如果在每个 k0 都按照频率升序排列,交叉点附近 COMSOL 返回的带序会出现交换,导致你追踪的“第 n 条带”在中途变成了物理上的第 m 条带,Zak 相自然全错。
解决方法是模式追踪。在每个 k0 步,计算所有当前能带候选 ((1,\dots,N_b)) 与上一步已排序好的能带集之间的场重叠矩阵 (O_{ij}=\langle \psi_i^{\text{prev}} | \psi_j^{\text{curr}} \rangle),然后做贪心匹配。由于相邻 k 点间距很小,本征态变化也小,所以重叠矩阵应该接近单位矩阵。出现某个元素异常时,说明带序发生了交换。
在 Matlab 中的实现骨架:
matlab复制prev_fields = phc_data(ii).field;
curr_fields = phc_data(ii+1).field;
overlap = abs(prev_fields' * (curr_fields .* weights));
perm = zeros(1, num_bands);
for b = 1:num_bands
% 选择上一带与当前带重叠最大的候选
[~, j] = max(overlap(b, :));
perm(b) = j;
overlap(:, j) = -inf; % 防止重复分配
end
phc_data(ii+1).field = curr_fields(:, perm);
依此逐 k 点扫描完所有参数步,能带顺序就连贯了。模式追踪做完之后最好画一个“场重叠模版”的图,看看对角线是否明显占优,如果有某一段非对角占优,就说明追踪逻辑还差一步。
4.4 从数值 Zak 相结果到物理结论
把 Zak 相数值算出来之后,可以沿着频率排序把每条带的 Zak 相标在能带图旁边。一个经典的一维双层原胞光子晶体,在第一条带往往是 Zak 相 0,第二条带是 π,第三条带是 0,这符合“带隙两侧带是否同相”来判断拓扑态的思路。如果你把多个带隙两侧能带的 Zak 相做差分,就能推测界面处是否有拓扑界面态存在:当两侧 gap 的“符号”相反时,界面态出现。也就是说,Zak 相计算的最终用途是判断界面态和拓扑频率区间。
我在实际项目中,通过扫描厚度占空比 (d_A/(d_A+d_B)) 从 0.1 到 0.9,画出一张 Zak 相随占空比变化的相图,发现 0/π 边界的走向与能带带隙边缘的位置完全吻合。这条相图拿来做后续参数设计非常有价值。如果你继续扩展到梯度结构,可以直接把不同占空比对应的 Zak 相当成设计地图,按需挑选界面结构的组合。
5. 常见问题与排查技巧实录
5.1 问题速查表
我把自己在 COMSOL 和 Matlab 两端跑项目时遇到的高频问题整理成了速查表,下面这些都是实际可复现的坑。
| 异常现象 | 可能原因 | 排查方法 |
|---|---|---|
| 能带图不闭合,边界处频率断开 | Floquet 边界条件里波矢表达式错误 | 检查 kx 是否使用了正确的晶格常数,扫描范围是否为 -0.5 到 0.5 |
| 能带出现单点跳变毛刺 | 网格太粗或对称性导致模式混杂 | 加密网格,检查边界处场分布 |
| Zak 相结果很乱,不是 0/π | 没有做相位规范修复 | 加相位对齐和平行输运规范处理 |
| 相邻 k 点内积接近 0 或变化剧烈 | 能带交叉,带序交换 | 使用模式追踪算法重新排序 |
| 计算时间过长 | 网格太密或导出节点数量过大 | 用均匀网格+采样点插值,减少节点数;COMSOL 研究可改用有效频率扫描 |
| 场导出后在 Matlab 中节点对不上 | 使用了不同数据集或插值坐标不一致 | 统一使用同一坐标数组,用 mphinterp 保持网格一致 |
排查时最重要的原则是分步检查:先检查能带结构,再检查场分布连续性,最后再看 Zak 相数值。如果最终 Zak 相不对,不要急着调公式,先画几个间隔较远的 k 点场分布,用肉眼看布洛赫函数的周期性和光滑性,很多时候问题出在导出阶段而不是计算阶段。
5.2 相位规范这个坑,值得单独写一段
我在第一次跑完整个流程时,算出来的 Zak 相从 0 到 π 到 2.7 什么都出现过,一度怀疑离散公式写错了。后来把每个 k0 下的本征场重新可视化,发现每个 k 点的场都发生了随机全局相移。COMSOL 对同一模式在不同参数步下给出的特征向量,允许有任意复相位,这在很多后处理中无所谓,但一旦参与相位累加就完全不同。
修复方法是在内积前做全局相位约束,让相邻 k 点的内积为实数正。准备写个通用函数:
matlab复制function u2_aligned = align_phase(u1, u2, weights)
inner = sum(conj(u1) .* u2 .* weights);
u2_aligned = u2 * exp(-1i * angle(inner));
end
如果这一步不加,后面的一切努力都会被随机背景相位抵消掉。别问我为什么写了三遍才想起这茬,这是经验。
5.3 网格、扫描步长和数值收敛性的平衡
Zak 相的数值误差来自几方面:场插值精度、内积积分精度、k 点离散粗度。通常网格最大尺寸取结构最小特征长度的 1/10 到 1/20,k 点步长取 0.005 就能做到 Zak 相误差小于 (10^{-3})。如果你需要更高精度,或者体系带隙很窄、模式接近简并,可以适当将 k 扫描步长降到 0.001,同时对场做更高阶插值。
但有个反面教训:网格太细时,COMSOL 的特征频率求解时间明显上升,而 Zak 相收敛对网格并不像对频率那样敏感。实际上一维问题网格做到 0.01 μm 和 0.04 μm 算出的 Zak 相差异不到 0.001,但求解时间差了几倍。所以不必盲目加密网格,找到“场分布光滑、频率稳定”的临界点即可。
说到扫描步长,需要特别提一句:布朗区的起点和终点必须是对布里渊区闭合路径上的同一点取极限,但 COMSOL 里直接取 (k=0.5) 和 (k=-0.5) 是同一个约化波矢点。只要你扫描列表里包含 -0.5 和 0.5 两个端点,它们就会得到同一物理模式。计算开路径时要注意不要把端点重复计算进内积,不然会多算一个和真实 Zak 相无关的相位。我在代码里一般让扫描列表不包含两个端点,而是取 (-0.5+\delta k) 到 (0.5-\delta k),然后再利用周期规范补上闭合路径的连续性。
5.4 如何验证计算结果是否可信
最后分享一个靠谱的自检方法。对一维双层光子晶体,当系统具有空间反演对称性时,你可以在理论上计算每个带边处布洛赫场分布的宇称,然后根据对称性公式推断各带 Zak 相的量子化值。具体做法是:把每个带边缘的场分布画出来,看原胞中心处是偶函数还是奇函数,然后套用经典的宇称乘积公式,得到 Zak 相应该为 0 或 π。如果 Matlab 数值得到的 Zak 相是 (0.999\pi) 或 (0.001),说明计算可信;如果是 0.37π 这种值,说明对称性破坏了或者你的相位处理还有问题。
另一个自检技巧:随便找一个能带,把扫描点数减少一半重新算一遍 Zak 相。如果结果几乎不变,证明离散足够密;如果变化超过 0.05π,就得加密 k 扫描。我在自己项目里做过这件事,从 200 个扫描点降到 100 个,Zak 相变化在 (0.01\pi) 以内,所以基本可以确定这个东西对数值参数不敏感,核心误差来源还是相位处理。
最后再分享一点关于整个流程的体会
这趟从 COMSOL 的 mph 文件到 Matlab 的算峰值,整体链路其实在方法上没有悬念,就是能带计算加离散 Wilson 线。真正影响成败的是工程细节,比如 Floquet 边界条件的波矢表达式、导出时是否去掉了平面波因子、以及相邻 k 点之间的相位对齐。我之所以坚持把最后一步做得跟前面一样认真,是因为 Zak 相这个量本身就敏感,它不是求和后看个大概,而是需要精确到 0 或 π 来支撑拓扑判断。数值上错过零点零几个 phase,整个物理结论就会反转。
如果你手头正好有类似的一维光子晶体模型,建议先不要急着往二维三维、拓扑优化、实验验证这些方向冲,先把 1D 的整条链路跑通、把几个自检方案过一遍。后面换了体系你会发现,Zak 相的计算套路依然是一样的,但坑可能会以新的形式出现。记录好每一次异常和修复方式,最后积累下来的问题清单会比任何公式手册都值钱。
