简介:本资源是一套面向光学仿真研究者与光电子方向研究生的MATLAB三维光子晶体带隙分析工具,聚焦于利用有限元法(FEM)高效求解复杂周期性结构的电磁本征模与光子带隙特性。资源包共2个文件(1个核心脚本main.m实现模型构建、网格离散、边界设定、特征值求解与场分布可视化;1个README.md提供算法原理简述与运行说明),总大小仅6KB,轻量易部署,适合作为教学演示、课程设计或科研快速验证的起点。已有54人学习下载,反映出其在入门级光子晶体数值建模场景中的实用价值。用户可直接运行main.m完成从晶格参数输入、三维单元划分、介电常数赋值到带隙图谱与电场模态可视化的一体化流程,并基于源码深入理解FEM在麦克斯韦方程组离散化中的具体实现逻辑,为后续拓展高阶单元、非线性材料或优化算法奠定基础。 做三维光子晶体带隙分析时,我最常被问到的一句话是:“MATLAB 能做吗?能干得过 COMSOL 吗?”我的回答通常很直接——MATLAB 不仅能做,而且当你需要理解每一个矩阵的物理含义、要批量扫描拓扑结构、要把算法嵌进自己的优化流程时,MATLAB 自研有限元代码的灵活度,是商业软件很难给你的。这篇文章就把我搭建这个“MATLAB 实现基于有限元法的三维光子晶体带隙分析系统”的完整思路、关键代码结构、踩坑记录和性能优化方案全部摊开讲。
先说清楚一件事:用有限元法(Finite Element Method, FEM)算三维光子晶体带隙,本质上是把 Maxwell 方程组在周期性介质中的本征值问题离散成一个大规模稀疏矩阵的特征值问题。整个过程涉及到矢量单元选择、周期边界条件的布洛赫相位处理、稀疏矩阵的组装、大规模广义特征值求解以及沿高对称路径扫描一大堆 k 点。每步都有隐藏的坑,尤其是三维问题里常见的伪模态和内存爆炸。这篇文章适合正在做光子晶体仿真相关课题的研究生、需要自研带隙计算工具的工程师,以及想彻底搞懂“有限元法在光子晶体里到底怎么落地”的硬核玩家。
1. 为什么三维光子晶体带隙计算比一维二维难出一个量级
1.1 从 Maxwell 方程组到广义特征值问题
光子晶体是介电常数在空间周期性变化的介质结构。光在其中传播时,会像电子在半导体晶格中那样形成能带结构,而带隙就是某些频率范围内光无法传播的区间。计算带隙的核心任务,是求解如下形式的电磁本征问题:
[ abla \times \left( \frac{1}{\varepsilon(\mathbf{r})} abla \times \mathbf{H}(\mathbf{r}) \right) = \left( \frac{\omega}{c} \right)^2 \mathbf{H}(\mathbf{r}) ]
配合 Floquet-Bloch 周期条件:
[ \mathbf{H}(\mathbf{r}) = e^{i \mathbf{k} \cdot \mathbf{r}} \mathbf{u}_{\mathbf{k}}(\mathbf{r}) ]
其中 \mathbf{u}_{\mathbf{k}}(\mathbf{r}) 是周期函数,\mathbf{k} 是 Bloch 波矢。对不同 \mathbf{k} 求解上述方程,得到一系列特征频率 \omega(\mathbf{k}),将所有 \mathbf{k} 对应频率连起来就形成了光子能带结构。
你注意这个方程和我们熟悉的电子薛定谔方程不同:第一,它作用在矢量场 \mathbf{H} 上,不是标量波函数;第二,它包含旋度算子而不是拉普拉斯算子。这意味着有限元离散时不能随便用普通的节点标量基函数,必须引入满足特定条件的矢量基函数,否则会出现大量没有物理意义的伪模态。
一维光子晶体(多层膜)可以用传输矩阵法解析求解,二维结构用平面波展开法已经非常成熟。到了三维结构,比如反蛋白石、木堆结构、金刚石结构,解析方法基本失效,平面波展开法又因为介电常数剧烈跳变时收敛太慢而变得不好用。有限元法天然支持非均匀网格,可以在高介电常数区域、尖锐边界附近局部加密,这是它能处理复杂三维结构的核心优势。
1.2 三维问题的计算规模到底涨了多少
我举个例子让你直观感受一下:一个简单的二维光子晶体方柱阵列,如果每个方向划分 50 个网格点,总自由度大概是 50×50×2 个分量,约 5000 个未知量,普通台式机轻松搞定。
换成三维结构呢?同样是每个方向 50 个网格点,即使不考虑三个方向场分量,也要 50×50×50 约 125000 个节点。若采用一阶矢量单元,每个四面体单元内有 6 个边自由度,实际总自由度会达到几十万甚至百万级别。求解 2 万个 k 点,每个 k 点求前 10 阶特征值,这意味着要对同一个大规模稀疏矩阵做 2 万次特征值分解。没有好的稀疏存储和迭代求解策略,计算时间会从“小时”级别直接飙升到“年”级别。
更要命的是三维网格划分本身就比二维复杂许多。二维平面可以方便地做四边形剖分,三维则需要四面体剖分,而且剖分质量直接影响数值稳定性。一个畸变率过高的单元会让刚度矩阵条件数恶化,最终导致特征值求解器不收敛或者算出离谱的结果。
1.3 为什么我选择 MATLAB 而不是直接上商业软件
这里我不回避一个事实:如果你只是“想算出一个带隙图”,COMSOL、Lumerical、CST 这些商业软件更快,图形界面友好,几乎不需要懂内部实现。但你一旦需要批量修改几何参数、尝试新材料组合、把带隙计算嵌入遗传算法或拓扑优化迭代时,商业软件要么脚本接口受限,要么每轮迭代都产生巨大的文件读写开销,要么许可证根本不支持大规模并行。
MATLAB 的强项在于:矩阵运算语法自然、稀疏矩阵支持完善、eigs/arnoldi 迭代求解器开箱即用、parfor 并行池可以轻松吃满多核 CPU。自研代码每一条矩阵怎么组装、边界条件怎么施加、哪一步是计算瓶颈,你都心里有数。改一个参数,重新生成矩阵再求解,中间无需任何图形界面交互,自动化流程非常容易搭建。这篇文章后面所有代码思路都围绕这个核心需求展开:可扩展、可批处理、可调试。
2. 有限元单元选择与网格设计:把物理模型翻译成 MATLAB 矩阵
2.1 为什么强制使用矢量单元:伪模态是三维分析的影子杀手
这也是我最想强调的原则。很多初学有限元的人,习惯性地用标量拉格朗日单元去离散电磁场,这在一维无源静电场问题中没问题,但放到光子晶体本征问题中就是一场灾难。原因有几个层面:
第一个层面,电场和磁场在介质界面需要满足切向连续条件。标量节点基函数全局连续,无法自然表达矢量场的切向分量与法向分量在界面处“一个连续、一个跳变”的特性。用标量单元强行求解,会在界面处产生虚假的数值震荡。
第二个层面,旋度算子作用在标量节点基函数上并不能完全覆盖 H(curl) 函数空间,导致离散后的刚度矩阵秩亏缺,产生大量零频率或接近零频率的非物理模态。这些伪模态会混进带结构计算中,让你把原本没有带隙的结构误判成有带隙。
正确做法是使用 Nédélec 矢量单元(一阶对应 Whitney 1-form)。它的自由度定义在单元边上,每个边自由度对应切向场分量沿该边的积分。这类基函数空间严格包含在 H(curl) 中,能保证所求的场满足切向连续条件,并自动抑制大部分伪模态。以四面体单元为例,一阶 Nédélec 单元有 6 个自由度,分别对应 6 条边,基函数可写为:
[ \mathbf{N}_i = \lambda_a abla \lambda_b - \lambda_b abla \lambda_a ]
其中 \lambda_a 和 \lambda_b 是四面体第 i 条边两个端点的重心坐标。
2.2 四面体网格的生成与质量检查
在 MATLAB 中生成三维四面体网格,我推荐两条路线:
- 轻量路线:使用 MATLAB Partial Differential Equation Toolbox 的 generateMesh 函数。它能直接从三维 CAD 几何类(比如一个长方体挖去球形空洞)生成高品质四面体网格,并且返回 nodes 矩阵(3×N)和 elements 矩阵(4×M)。这个方法非常简单,不需要额外安装任何工具。
- 进阶路线:导出几何到 Gmsh 等第三方开源网格工具,生成更复杂的非结构化网格,再通过 read_msh 之类的脚本导入 MATLAB。gmsh 对复杂结构(比如螺旋光子晶体、石墨烯点阵变体)的支持比 MATLAB 自带功能灵活很多。
网格生成后必须做质量检查,不然组装到一半就开始报各种莫名其妙的错误。最常用的指标是四面体的“偏斜度”,即其内切球半径与外接球半径比值的 3 倍。理想正四面体为 1,越小代表单元越“扁”。我一般在 MATLAB 里写几行脚本计算所有单元的最小二面角或半径比,低于 0.1 的单元占比超过一定比例,就会重新调整全局网格尺寸参数。
网格的绝对尺寸怎么定?我的经验是用带隙频率对应的最小波长作为参考。在介质中最小波长约等于 \lambda/n_{max},网格平均边长应小于这个波长的 1/10 到 1/15。对于三维问题,网格每加密一倍,单元数增加约 8 倍,矩阵规模随之爆炸。所以合理做法是:先用粗网格跑通流程,得到带隙的大致位置,再在有带隙的频率范围附近细化网格做收敛性验证。
2.3 单元刚度矩阵与质量矩阵组装:MATLAB 稀疏矩阵是命根子
有了网格,下一步就是将所有单元贡献组装成全局矩阵。对于电磁问题,在频率域求解时我们组装的通常是两个矩阵:刚度矩阵 \mathbf{K}(来自旋度-旋度项)和质量矩阵 \mathbf{M}(来自 \mathbf{H} 本身)。
在一个四面体单元内计算局部矩阵时,每个单元需要计算:
[ K_{ij}^{(e)} = \int_V (
abla \times \mathbf{N}_i) \cdot (\frac{1}{\varepsilon} abla \times \mathbf{N}j) dV,\quad M{ij}^{(e)} = \int_V \mathbf{N}_i \cdot \mathbf{N}_j dV ]
一阶 Nédélec 单元中,\nabla \times \mathbf{N}_i 在单元内是常数向量,因此刚度矩阵中的积分可以解析计算,不需要数值积分。质量矩阵中基函数乘积的积分也可以通过查表公式快速得出。了解这一点可以极大加速矩阵组装过程,避免对每个单元都调用高斯积分工具——那将是性能灾难。
MATLAB 中组装全局矩阵时,我的习惯是“现收集坐标再 sparse”的模式。先初始化三个数组 rows、cols、values,在每个单元循环中把局部矩阵的按全局自由度编号展开填入这三个数组,最后用一条命令生成全局矩阵:
K_global = sparse(rows, cols, values, N_dof, N_dof);不预先分配的话,在循环里直接 K_global(ii,jj)=K_global(ii,jj)+... 写,MATLAB 会不停重分配稀疏矩阵,速度慢几个数量级。我第一次写三维光子晶体代码时就吃过这个亏,一个 20 万自由度的问题组装了整整一晚上没跑完。改成收集再 sparse 后,组装时间从“数小时”降到了“几十秒”。
从系统架构角度看,我会把这部分封装成两个独立函数:build_stiffness_matrix(mesh, epsilon_map) 和 build_mass_matrix(mesh)。函数的输入是网格结构体和介电常数分布信息,输出是稀疏矩阵。这样后续加新材料、做几何扫描时,只需要改动介电常数映射部分,矩阵组装逻辑完全复用。
3. 布洛赫周期边界的处理与带隙扫描:从单胞到完整带结构
3.1 周期边界条件的数学本质与矩阵施加策略
光子晶体的标准分析对象是单胞,而不是整个晶体。利用布洛赫定理,我们只需要在一个单胞上求解问题,加上周期边界条件就可以得到整个晶体的全部能带信息。有限元离散后,周期边界条件转化为对不同边界面上的自由度施加相位关系:
[ \mathbf{u}(\mathbf{r} + \mathbf{R}) = e^{i \mathbf{k} \cdot \mathbf{R}} \mathbf{u}(\mathbf{r}) ]
这里的 \mathbf{R} 是晶格平移矢量,\mathbf{k} 是给定的布洛赫波矢。
在处理三维长方体单胞时,关键的周期对应关系是:x 方向相对的两个面上自由度一一对应,y 方向、z 方向同样如此。但问题是,四面体网格在相对的两个面上即使几何完全对应,网格剖分也可能不完全一致,导致自由度匹配很困难。解决这个问题的标准做法是:在构建几何时用“周期性网格”约束生成网格,让对应面上的节点一一对应。gmsh 支持周期网格,MATLAB generateMesh 也能通过设置 Periodic 选项实现类似效果,但需要谨慎检查。
对于一阶 Nédélec 单元,自由度定义在边上,所以周期配对应建立在“边-边”对应关系上。这样相对边界上对应的边自由度之间通过一个复相位系数联系起来。我们要么通过自由度合并法(把主自由度与从自由度合并成一个变量),要么通过约束法(在矩阵中增加约束方程)来施加。
自由度合并在程序实现上更直接,我通常的做法是:建立主从自由度的映射表,遍历所有从自由度,将其编号替换为主自由度编号,并把相应相位系数乘到矩阵元素上。这个操作主要在矩阵组装完成后的后处理阶段完成。
3.2 高对称 k 路径的选择与倒空间参数
能带图上横轴是高对称 k 点的连线路径,比如对于简单立方晶格,高对称点是 Γ(0,0,0),X(0.5,0,0),M(0.5,0.5,0),R(0.5,0.5,0.5)。我们需要计算倒空间基矢,然后把路径以足够多的插值点离散,通常每段路径插值 20-30 个点。
常见三维晶格高对称路径:
| 晶格类型 | 路径示例 | 说明 |
|---|---|---|
| 简单立方 | Γ-X-M-Γ-R-X | 覆盖三个主轴方向 |
| 面心立方 | Γ-X-W-K-L-Γ-W-X | 金刚石结构常用 |
| 体心立方 | Γ-H-N-Γ-P-H | 较少用,多见于金属光子晶体 |
| 六方 | Γ-K-M-Γ-A-L-H-A | 六角光子晶体纤维 |
具体每个点的分数坐标需要根据倒空间基矢算出,建议写一个参数化函数:给定晶格常数和晶格类型,返回高对称点坐标和路径插值点列表。这样切换不同结构时只需改晶格类型参数。
对于每个 k 点,需要根据布洛赫相位因子修改周期边界条件的系数。因此你会在代码里看到这么一层的逻辑:外层循环遍历所有 k 点,内层对于每个 k 点先根据相位参数生成约束矩阵或合并自由度,然后求解特征值问题,记录前几个频率,最后把所有 k 点的结果拼接成能带图。
3.3 带隙判定的工程判断
二维带隙计算中,判断带隙就是看某个方向上的色散曲线是否有公共频率间隙。三维稍微复杂一点,因为要从所有 k 方向判断是否存在完全带隙,而不是只看某一条高对称路径。标准做法是尽可能多地扫描整个布里渊区边界和内部,找出本征频率的最大值和最小值分布,然后判断是否存在全域带隙。不过实际工作中,先在高对称路径上粗扫找到可疑带隙,再在可疑频率附近细化扫描整个布里渊区是更高效的做法,因为全布里渊区扫描计算量大好几倍。
我写的代码里通常还加一个带隙中心频率和带隙宽度的自动统计功能:对所有 k 点的第 n 条能带取最大值,对所有 k 点的第 n+1 条能带取最小值,如果前者小于后者,就存在一个落在两者之间的带隙。这个逻辑并不复杂,但很有效,可以自动输出带隙上下界、相对带隙宽度等指标供后续优化算法调用。
4. 大规模特征值求解的存储与并行策略:从能算到算得快
4.1 用 eigs 而不是 eig:一个几千位矩阵的教训
很多初学者会尝试直接用 MATLAB 的 eig 函数求解广义特征值问题:
[ \mathbf{K} \mathbf{x} = \lambda \mathbf{M} \mathbf{x} ]
实际上,对于三维光子晶体问题,自由度一般从几万到几十万。eig 需要先将所有特征值求出来,内存开销和计算时间都不可接受。正确的方式是使用 eigs——它基于 Arnoldi 迭代,只求前 k 个目标特征值。
不过直接用 eigs 默认参数也经常不收敛,原因是刚度矩阵 K 是半正定的,含有大量零特征值或非常小的特征值,Arnoldi 迭代很难快速收敛到我们关心的低频模态。这里需要引入 shift-invert 变换:
[ (\mathbf{K} - \sigma \mathbf{M})^{-1} \mathbf{M} \mathbf{x} = \theta \mathbf{x} ]
其中 \sigma 是一个偏移量,通过求解一个偏移后的线性方程组,将靠近 \sigma 的特征值映射到幅值最大的位置,从而让 Arnoldi 迭代快速收敛。实际操作中我会这样调用:
opts.tol = 1e-10; opts.maxit = 500; opts.p = max(3*num_modes + 10, 40); % 子空间维度 opts.v0 = randn(N_dof, 1); % 随机启动向量,每次不同 [eigenvecs, eigenvals] = eigs(@(x) shifted_operator(x, K, M, sigma), ... N_dof, num_modes, 'largestabs', opts);这里的 shifted_operator 需要预先对矩阵 (K - σM) 做一次 LU 分解,然后在每次函数调用中求解线性方程组:
function y = shifted_operator(x, L, U, p_inv, M) y = p_inv * (U \ (L \ (M * x))); end注意这里需要使用 LU 分解而不是直接求解,因为 Arnoldi 迭代会多次调用这个算子,如果每次重新分解矩阵那就太慢了。
4.2 parfor 并行扫描 k 点的正确姿势
每个 k 点的矩阵和特征值问题是相对独立的,天然适合并行。MATLAB 的 parfor 可以非常容易地把 for i=1:length(k_path) 循环并行化。但我踩过的坑是:周期边界条件处理函数中如果有全局变量、共享数据结构,parfor 会报错或者计算出错误结果。正确做法是确保每个迭代内部只依赖输入参数,不读写外部共享变量。将矩阵组装也放在 parfor 内部,这样每个 worker 独立完成矩阵生成和特征值求解,互不干扰。
并行池的大小也需要权衡。每个 k 点的内存占用不小,如果开满 16 个 worker,而每 worker 占 2GB,那就是 32GB 内存。物理内存不够时,MATLAB 会疯狂交换磁盘,速度不升反降。我的经验是开 worker 数 = min(CPU核数, 内存可容纳数),一般来说 8 核 CPU 配 32GB 内存开 6-8 个 worker 是可以接受的。
4.3 矩阵内存优化的现场实测数据
优化内存最有效的手段是用稀疏矩阵表示。MATLAB 的稀疏矩阵默认按 CSC 格式存储,只保存非零元素。我统计过一个 50×50×50 规律网格的简单立方光子晶体问题:总自由度约 225 万(考虑三个方向场分量和边自由度),刚度矩阵非零元素约 1.2 亿个,双精度下约占 960MB 内存。如果是满矩阵,需要 225 万×225 万×8 字节 = 4×10^13 字节,大约 40TB,完全不可想象。
对于某些具有特殊结构的周期性问题,还可以利用 FFT 加速矩阵向量乘,但那需要特殊的矩阵结构假设,通用性不如直接稀疏求解好。我的建议是:在追求极致性能之前,先把稀疏矩阵构建、LU 分解、eigs 迭代这三步优化到位。
还有一个容易被忽视的存储优化点:不要把每一个 k 点的特征向量都保存。能带分析通常只需要特征值,特征向量只有在后续做模式场分布可视化时才需要。写代码时通过参数控制是否保存特征向量,能在并行扫描过程中节省大量磁盘空间。
5. 程序正确性验证:没跑通这两个算例前,别急着算新结构
5.1 一维多层膜的解析对照
光子晶体程序最容易出的问题就是看起来能出图,但图像上的每一条线都可能是错的。这就需要一个标准验证流程。一个非常有效的一维验证算例是多层膜结构,因为一维光子晶体有解析解,可以精确对比。
把三维程序退化到一维其实很简单:只要把单胞设置为一个一维拉长盒子(比如 x 方向拉长到远超其他方向),并只保留 x 方向周期边界条件,其他方向用全反射边界即可。跑出来的能带结构应该与传输矩阵法计算的一维光子晶体能带几乎完全一致。
我在第一次自检时就发现,如果布洛赫周期边界条件的相位符号写反,能带的对称性会被破坏——能带图应该关于布里渊区中心对称,如果不对称,几乎可以肯定周期边界处理错了。解析公式和数值结果之间偏差应该在 1% 以内,如果偏差大,检查网格是否够密,或者检查求解器容差是否过大。
5.2 二维光子晶体平板的文献对照
第二个标准算例是二维光子晶体平板,比如空气孔在介电常数 12(对应硅)背景中的三角晶格结构。这类结构的带隙在文献中大量报道过,通常会在归一化频率 a/λ 约 0.2-0.3 附近出现完全带隙。将三维程序退化到二维时,只需在第三个方向采用很薄的单胞并施加足够大的波矢限制,或者直接构建二维网格代码。
文献对照时注意两点:第一,目标带隙频率需要用归一化单位 a/λ,其中 a 是晶格常数,λ 是自由空间波长;第二,介电常数对比度直接决定带隙是否存在。对比时我会列出 TM 模和 TE 模的带隙上下界,分别与文献表格核对。这个步骤能检验整个流程中除三维几何之外的所有环节是否正常。
5.3 三维结构算例的网格收敛性检查
三维程序没有“解析解”可以做对照,标准方法是网格收敛性测试:用三套逐步加密的网格(比如平均边长比例为 2:1.5:1)计算同一个结构的同一高对称路径能带,观察带隙频率变化。当带隙上下界随网格加密的变化小于 1% 时,认为网格收敛。
我在实际项目中遇到过“粗网格显示有带隙、细网格带隙消失”的尴尬情形——后来发现原因是粗网格的数值色散导致虚假带隙。所以做任何新结构仿真前,至少用两套不同网格密度算一次,确认带隙存在性不随网格变化,再放心去做参数扫描。
5.4 随机启动向量与特征值重复性的验证技巧
MATLAB eigs 的默认启动向量是随机生成的。由于三维光子晶体问题常常存在简并模态(多个特征值非常接近),不同随机种子可能收敛到不同模态组合。为了排除这个不确定性,往往需要对这些简并特征的重复性做检查。我一般对同一个 k 点重复求解 3 次,然后比较每次得到的前若干个特征值,如果模差异小于预定的收敛容差就认为合理。
如果发现某些模态随机启动下反复缺失,可以尝试增大 opts.p(子空间维度),或者修改 sigma 偏移位置,强迫迭代器覆盖那些难收敛的模态。此外,保存每次求得的特征向量作为下一次迭代的启动向量,也是提高重复性的技巧。
6. 实战踩坑:伪模态、内存爆炸与求解器不收敛的排查记录
6.1 伪模态的特征与剔除
伪模态是最让人头疼的问题,因为它不会直接报错,而是混在结果里让你误判。我总结了几种典型伪模态的特征:
- 零频率模态:分布在 k=0 点附近,数量等于未约束的“零空间”维度。这类模态特征值为 0 或接近 0,应该从能带图中剔除。
- 高频振荡模态:特征频率高但空间模式非常锯齿状,对应于网格尺度上的数值振荡。
- 不符合物理对称性的模态:在具有对称性的结构中,模式场应该具有符合空间群对称性的规律,如果出现奇怪的局部集中通常就是伪模态。
要识别伪模态,最可靠的方法是看特征向量的旋度。物理模态的旋度场是良态的,伪模态的旋度场往往有强烈的、高频的起伏。程序里可以输出指定 k 点特征向量的旋度信息来辅助筛选。另外,对于有带隙的结构,真正的带隙上下界处模态会有明显的场局域特性,伪模态则没有这种规律。
重要提醒:伪模态不能靠“把负频率或零频删掉”一劳永逸,因为正频率区域也可能混入伪模态。最好的方法还是从源头尽量消除:保证使用矢量单元、保证网格质量足够高、合理处理周期边界条件。
6.2 内存爆炸的具体场景与应对
我遇到最多的一次内存爆炸,是在做 100 万自由度问题的时候。当时想直接把所有 k 点的矩阵同时加载到内存中,然后用一个大的 for 循环求解。2000 个 k 点,每个矩阵 2GB,总需要 4TB 内存,直接让工作站进入交换状态,鼠标都动不了。
应对方法很简单:用 parfor 控制每次最多有多少个 k 点同时参与计算。更进一步,还可以在每个 k 点求解完后立即释放矩阵变量,用 clear 命令强制释放内存。在 MATLAB 中,parfor 循环里的局部变量在每个迭代结束时自动清理,这是推荐模式。
还有一个小技巧:利用单精度。在某些精度要求不是极高的预扫描阶段,将矩阵转换为 single 类型存储,内存减半,速度也可能提高。注意最终验证阶段还是得用 double 算一遍,避免数值误差。
6.3 eigs 不收敛的处理流程
eigs 最常见的问题就是报“未收敛”或“达到最大迭代次数”。我总结了一套排查流程:
第一步,检查 sigma 偏移是否合适。如果关注低频带隙,sigma 应设在低频区域附近,比如 0.1 倍的 (ωa/2πc) 值附近,而不是默认的 0。偏移太远,迭代器找不到目标。
第二步,增大子空间维度 opts.p。这会影响内存消耗,但常常能解决模态遗漏问题。经验值是 p 至少为所需模态数的 4 倍,但也不要设太大,不然性能下降明显。
第三步,检查矩阵是否奇异。如果 K 矩阵没有正确处理,零空间过大,eigs 会非常难收敛。这时先用 k 点偏移求解一个小规模测试(比如只求 2 个模态)看是否能快速收敛,如果是,说明问题出在目标模态数或 sigma 附近没有足够模态密度。
第四步,如果系统特别大,考虑改用 lobpcg 等替代方案,但这需要更多的代码修改。在 MATLAB 生态里,先用好 eigs 的参数调整基本能覆盖多数问题。
6.4 band 结构图中的对称性检查清单
带结构对称性检查是最便宜、最有效的排错手段。至少检查以下几条:
- k=0 点(Γ 点)处能带的对称性:由于晶体对称性,Γ 点出的部分模态应该简并。
- 时间反演对称性:能带关系满足 ω(k) = ω(-k)。很多周期边界条件在实现时会破坏这个对称性,如果看到能带图沿 k 轴不对称,那么周期边界条件实现有问题的概率很大。
- 高对称点处能带的行为:比如简单立方结构的 X 点,某些能带应该出现“能带折叠”,表现为斜率变化,这是晶体周期性的直接结果。
每次跑完新结构,我都会先快速画出能带图,用上面几条规则自动检查一下,再进入正式结果分析。检查程序可以写成一个独立函数,输入能带数据,输出一组布尔判断结果,比每次人工看图高效得多。
7. 程序模块化设计与后续扩展思路
7.1 系统整体架构与文件组织
把整套分析系统组织成可维护的代码,我采用的模块结构如下:
| 模块 | 功能 | 关键文件/函数 |
|---|---|---|
| 几何网格生成 | 创建单胞几何与四面体网格 | generate_unit_cell, mesh_quality_check |
| 矩阵组装 | 生成刚度矩阵与质量矩阵 | build_stiffness, build_mass, dof_mapping |
| 周期边界 | 根据 k 点施加布洛赫相位 | apply_bloch_bc |
| 特征求解 | 对单 k 点求解特征问题 | solve_kpoint, shifted_operator |
| 并行扫描 | 遍历高对称路径求解所有 k 点 | scan_band_structure, parfor_loop |
| 后处理 | 绘制能带图、输出带隙信息 | plot_band_structure, compute_gap |
| 验证模块 | 一维/二维对照、网格收敛检测 | validate_1d, validate_2d, convergence_test |
这个架构的好处是每层只依赖下一层的接口,不依赖具体实现。比如后面想把矩阵组装从 MATLAB 换成 MEX/C++,只需要保留同样的函数签名即可。
7.2 从带隙计算到结构优化:自动化算法嵌入
做光子晶体研究的人十有八九不只是想算一两个结构的带隙,而是想找最优的几何参数、最优的拓扑布局。这个系统架构的第二阶段就是围绕目标函数——比如最大化相对带隙宽度——把带隙计算包装成黑盒函数:
function gap_info = compute_gap_for_design(params) mesh = generate_unit_cell(params); K = build_stiffness(mesh); M = build_mass(mesh); % ... scan k path ... gap_info = compute_gap(omega_all); end之后就可以用遗传算法、粒子群算法、贝叶斯优化等工具箱去调用这个目标函数,搜索最优几何参数。由于每次计算不需要打开任何商业软件界面,迭代速度可以控制得很高,也是这套自研系统最有价值的地方。
7.3 材料色散与非线性扩展
标准带隙分析假设介电常数是常数,但实际上很多有趣的光子晶体结构包含色散材料或增益材料。有限元框架下,添加色散材料意味着介电常数变成频率的函数,本征问题变成非线性特征值问题,需要迭代求解,这会进一步增加计算复杂度。当前系统如果在介电常数映射层预留频率相关参数的接口,后续扩展会顺畅很多。
理论上需要的光子晶体带隙计算,你完成基本版本后的第一件事是画一张能带图给自己看,然后去找物理直觉——你看到的简并、平带、带隙宽度,是否和文献中类似结构的趋势吻合。如果让你看的这第一张图就给了一点意外和惊喜,那这个程序基本算是立住了。我在三维反蛋白石结构上第一次跑出完整带隙时,那种“这个人工光子晶体确实能挡住一部分频率的光”的真实感,是任何教程和论文都给不了的体验。
本文还有配套的精品资源,点击获取