简介这是一套基于 Ceviche 有限差分频域FDFD模拟器的光子逆设计基准测试题集专门为拓扑优化算法研究者与集成光子器件设计者提供可复现的对比平台。借助 HIPS Autograd 自动微分代码可高效计算散射参数与电磁场分布覆盖波导分束器、模式转换器、波导弯曲和波分复用器等典型器件场景。压缩包共 43 个文件以 Python 源码为主体35 个 .py包含核心模型、参数定义、模式计算及散射/测试脚本另有 4 张效果图、2 份 Markdown 说明文档以及配置与许可证文件整体大小仅 772KB目录划分清楚。目前已有 368 人学习下载。下载后可直接调用标准 API 开展算法基准测试同时可参考 README 与单元测试快速理解复现流程是开展光子逆设计实验和论文复现的实用工具。1. 光子逆设计要想可复现先得有套标准考题做光子逆设计的人心里都有个数算法改进了效果到底好不好得有个能说话的基准。可问题是每个人仿真器不同、网格尺寸不同、目标函数定义不同论文里对比的基线常常是各跑各的复现时一步一个坑。这个 ceviche_challenges 模块就是冲着这个痛点来的——它把波导分束器、模式转换器、波导弯曲和波分复用器这四类经典集成光子器件统一封装成基于 FDFD有限差分频域模拟器的基准测试问题配好散射参数计算和自动微分接口直接能用来给不同拓扑优化算法打分。简单说它是一套标准考题不是仿真器本身但比仿真器更值钱——因为它把考题、答案和评分标准一起交到你手里了。如果你正在找自己的拓扑优化算法能跑出什么水平或者刚入坑光子逆设计想找个靠谱起点这套资源值得认真拆一遍。2. FDFD 与自动微分这套挑战题的底层是“可求导的麦克斯韦”2.1 为什么选 FDFD 而不是 FDTD拓扑优化最核心的需求是对目标函数求梯度然后沿着梯度方向更新介电常数分布。FDTD有限差分时域也能算梯度但那得走伴随方法还得自己处理时间步进和稳定性条件写起来非常痛苦。FDFD 不一样它直接在频域求解亥姆霍兹方程把问题变成一个线性方程组[ (\nabla \times \nabla \times - k_0^2 \varepsilon) E -j \omega \mu_0 J ]这个形式天然适合矩阵求逆而矩阵求逆的过程可以被自动微分框架完整记录。ceviche_challenges 用的是 HIPS autograd它对 numpy 代码做算子级重载前向传播算完场分布反向传播自动把梯度算出来。对比 FDTD 需要手动推导伴随场FDFD 的做法大幅降低了实现成本也让基准测试更关注算法本身而不是仿真器的数值细节。选 FDFD 的另一层原因在器件尺度这套基准里的器件尺寸在微米量级工作波长在 1.55 微米附近网格数量万级别FDFD 求解一次只需要几秒到几十秒做上百轮迭代优化是可行的。如果是 FDTD时间维度的迭代会让单次仿真代价翻一个量级。2.2 代码骨架四层职责分明的模块结构解压 ceviche-challenges-main.zip 之后第一层映入眼帘的是四个器件文件夹beam_splitter、mode_converter、waveguide_bend、wdm。往下翻根目录下的 python 模块才是核心。模块文件职责params.py各器件的物理参数与优化参数定义primitives.py基础几何原语用于生成初始折射率分布scattering.py散射参数计算与目标函数定义modes.py模式展开与模式投影计算ops.py顶层 API把仿真、梯度、目标函数串起来units.py无量纲化处理避免单位换算错误用一句话概括这套模块的协作逻辑params.py 定义“物理世界长什么样”primitives.py 生成“初始猜测长什么样”scattering.py 和 modes.py 负责“光在里面怎么走”ops.py 把这些内容整合成对外的标准接口。第一次跑通代码时我最意外的点是 ops.py 里大量使用了 autograd 的自定义函数包装FDFD 求解器被当作一个可微分的黑匣子嵌入到整个计算图里这让梯度计算完全透明化不需要手动干预。2.3 梯度怎么算下来从 S 参数到折射率更新一个典型的拓扑优化迭代流程长这样import autograd.numpy as np from autograd import grad from ceviche_challenges import scattering # 初始化介电常数分布eps_vec 是设计区域的像素值数组 eps_vec np.ones(num_pixels) * eps_wg # 定义目标函数以透射率为目标最大化到指定端口的传输 def objective(eps_vec): _, S_params scattering.simulate(eps_vec, wavelength1.55e-6) transmission np.abs(S_params[port_out, port_in]) ** 2 return -transmission # 最大化透射率等价于最小化负值 # 自动微分求梯度 grad_objective grad(objective) # 迭代更新 for iteration in range(200): eps_vec eps_vec - learning_rate * grad_objective(eps_vec) print(fiter {iteration}: transmission {-objective(eps_vec):.4f})这段代码最关键的逻辑在第 9 行grad(objective)不是数值差分而是 autograd 的自动微分。它对整个仿真链做反向传播从 S 参数的实部虚部一路把梯度传回每个像素的介电常数上。第 13 行的更新规则是最简单的梯度下降实际使用中你完全可以换成 Adam、MMA 或者移动渐近线法只需要把eps_vec - learning_rate * grad替换成你自己的优化器步进逻辑。参数选择上wavelength1.55e-6是电信 C 波段的标准波长num_pixels取决于网格划分密度设计区域每个波长方向至少需要 10 个像素这是 FDFD 方法的基本分辨率要求。学习率初始值我一般取在电场强度绝对值的量级附近跑十几个迭代观察目标函数是否震荡再决定放大还是缩小。这套 API 的设计哲学是“仿真细节被封装优化自由度完全开放”。算法研究者不需要懂 FDFD 的差分格式只需要定义目标函数、调用 simulate、拿梯度、喂给优化器。这和我之前用过的其他仿真平台很不一样——那些平台通常把优化流程写死想换算法得改源码。ceviche_challenges 把“问题”和“求解器”分离得干净利落。3. 四大经典器件benchmark 题目到底考什么3.1 波导分束器功率配比与 S 参数终点第一个基准问题是 1×2 波导分束器。输入端一个 TE 模式输出端两个波导目标是让光功率均匀分配到两个出口。设计变量是中间一个矩形区域的折射率分布初始值是波导材料的均匀折射率。目标函数的定义非常直观transmission_1 np.abs(S_params[port_1, port_in]) ** 2 transmission_2 np.abs(S_params[port_2, port_in]) ** 2 objective -min(transmission_1, transmission_2) penalty第 3 行用min而不是均值这是在惩罚“只优化好一个端口”的投机行为。很多新手第一次跑的时候会发现目标函数下降很快但两个端口的功率并不均衡——那是因为他们用了均值目标函数算法找到了一个“把光全送到一个口”的捷径。这个min的设定让基准题有真正的区分度算法必须在两个端口之间做权衡这才能测出优化器的真实能力。penalty项是二值化约束。代工厂要求最终设计只能是“硅或者空气”两种状态不能有中间介电常数。常见做法是在目标函数里加一个正则项惩罚偏离二值分布的连续值。céviche_challenges 的 benchmark 里这些细节都提前配好了你不需要自己写惩罚项但如果要做消融实验知道惩罚项的存在很重要。3.2 模式转换器TE0 到 TE1 的模式投影模式转换器考的是另一件事不只关心功率大小还关心模式纯度。输入是 TE0 模目标输出是 TE1 模如果输出端口的光场仍然是 TE0 占主导那传输功率再高也不算分。这里就轮到 modes.py 出场了。它做的事情是把输出端的电场投影到目标模式的场分布上得到一个模式重叠积分from ceviche_challenges import modes # 计算输出端口的场分布 fields scattering.get_fields(eps_vec, wavelength1.55e-6) # 投影到 TE1 模式上得到模式转换效率 mode_overlap modes.project(fields, port_out, mode_index1, directionforward) conversion_efficiency np.abs(mode_overlap) ** 2第 6 行的mode_index1指定了目标模式是 TE1。这个接口的设计让我想起一个常见误用场景有人直接把 TM 模式的光也算进传输效率里导致优化结果看着功率高实际代工出来完全不可用。正确的做法是把所有模式的功率分离只取目标模式的分量作为目标函数。这个基准题的全部意义就在于检验算法能不能理解“模式纯度”这个物理量而不是简单地最大化功率。3.3 波导弯曲和 WDM弱损耗与多端口隔离波导弯曲看似简单实际是检验算法对反射的敏感度。90 度弯折的辐射损耗很难完全消除好的拓扑优化算法能找到一种“渐变折射率透镜”式的结构让光平滑转向而不是硬拐弯。这个基准题的难点在于目标函数不能只看透射功率还要看回波损耗——S 参数矩阵中入射端口本身的反射系数。WDM波分复用器是这四个题目里最难的它是一个多端口、多波长的设计问题。两个输入波长分别路由到两个不同输出端口这就需要算法同时处理两个频率点的约束。具体实现里散射问题在多个波长下分别求解目标函数是各个波长透射率的加权和。这里有个值得注意的参数波长的间隔不能太近否则两个通道的响应会互相干扰导致优化不收敛。céviche_challenges 里默认的波长间隔是几十纳米量级这个设定和典型的硅光子 WDM 设计一致。器件设计变量数目标函数核心典型难度波导分束器中双端口功率均衡入门模式转换器中模式重叠积分中等波导弯曲小透射率回波损耗中等WDM大多波长多端口串扰高四个器件的复杂度梯度安排得很清楚这也是为什么我推荐做算法评估时四个题目全跑而不是选一个。单个题目的结果有很大偶发性算法 A 在分束器上胜过算法 B在 WDM 上可能完全反过来。四个题目覆盖了不同维度的优化挑战合在一起才是公平的算法擂台。4. 跑通基准套件从源码安装到验证接口4.1 环境准备与依赖清单把代码拉下来之后第一步是准备 Python 环境。这套代码的核心依赖有这么几个numpy 是基础数值计算autograd 提供自动微分scipy 用于稀疏矩阵求解。安装过程我建议直接用 pippip install numpy autograd scipy # 进入项目根目录安装本地模块 cd ceviche-challenges-main pip install -e .第 2 行pip install -e .是把当前目录的包以可编辑模式安装这样你在本地修改代码后不需要重新安装就能生效。如果这一步报错先检查 Python 版本——autograd 在 Python 3.10 以上可能会遇到兼容性问题我一般在 3.8 或 3.9 的虚拟环境里跑这套代码。依赖装完之后最直接的验证方式是运行 ceviche_challenges 自带的测试文件。项目里有两个测试文件特别值得留意scattering_test.py和modes_test.py它们分别验证散射参数计算和模式投影的正确性。跑一遍测试比什么都说明问题python -m pytest ceviche_challenges/scattering_test.py -v python -m pytest ceviche_challenges/modes_test.py -v这两行命令会逐个测试 S 参数的数值正确性和模式投影的物理正确性。如果测试全绿说明你的环境配置没问题可以放心跑优化。我拆这个包的时候第一次跑scattering_test.py挂了三个用例排查半天发现是 scipy 版本太新导致稀疏矩阵求解器接口变了——这种事在开源项目里太常见了所以先跑测试能帮你快速定位是环境问题还是代码问题。4.2 用散射问题验证 S 参数的正确性sacttering_test.py 的核心逻辑是构造一个简单的直波导然后检查 S 参数矩阵是否满足物理规律。直波导的透射率理论上接近 1反射率接近 0用这个已知结论作为基准来验证仿真器是否正确是最高效的冒烟测试。from ceviche_challenges import scattering import numpy as np # 构造一个直波导设计区域填充波导材料 eps_vec np.ones(num_pixels) * eps_wg # 计算 S 参数矩阵 S_params scattering.simulate(eps_vec, wavelength1.55e-6) # 物理检验透射率应该接近 1反射率应该接近 0 T np.abs(S_params[1, 0]) ** 2 R np.abs(S_params[0, 0]) ** 2 assert T 0.9, f透射率过低: {T} assert R 0.05, f反射率过高: {R}第 8 行的S_params[1, 0]表示从端口 0 入射、从端口 1 出射的传输系数第 9 行的S_params[0, 0]是端口 0 的反射系数。这两个断言保证了仿真器的基本物理正确性。如果你把 eps_vec 全填成空气ε1透射率会完全不同——那种情况下光根本不会在波导里传输。这个简单的对比测试能帮你建立对仿真结果的直觉什么样的折射率分布对应什么样的场分布。4.3 用模式问题验证模式投影的接口modes_test.py 检查的是模式展开的正确性。它会调用 modes.project 把任意场分布投影到波导模式上然后验证投影系数的总功率守恒——所有模式投影的功率之和应该等于场本身的总功率这个性质叫 Parseval 定理。from ceviche_challenges import modes # 输入一个 TE0 模式的场分布 fields_te0 modes.get_mode_field(port0, mode_index0, wavelength1.55e-6) # 投影回模式基 coefficients modes.project(fieldsfields_te0, port0, mode_indices[0, 1, 2]) total_power sum(np.abs(c) ** 2 for c in coefficients) # Parseval 定理验证总功率守恒误差应该在 1e-6 量级 assert abs(total_power - 1.0) 1e-6, f功率不守恒: {total_power}在这个测试里mode_indices[0, 1, 2]表示取前三个模式做投影。如果只取 TE0 一个模式total_power会等于 TE0 的功率占比这个数值本身就是模式纯度的一种度量。我第一次跑这个测试的时候发现total_power不等于 1差了几个百分点——原因是模式展开的数量不够高阶模式没有被包含进来。把mode_indices的范围扩大之后功率守恒就成立了。这个细节在跑模式转换器基准题时要特别注意目标函数里的模式投影如果只取了一个模式梯度计算是会出偏差的。5. 避坑指南跑 ceviche-challenges 踩过的五个坑5.1 autograd 张量拒绝 in-place 操作现象梯度计算时报错提示无法修改 autograd 追踪的数组整个迭代在第一步就崩溃。原因autograd 对 numpy 操作做了包装设计了计算图来记录每次运算。如果你对eps_vec使用eps_vec[10,10] 1.5这类 in-place 赋值计算图会被破坏反向传播无从谈起。解决每次更新都用np.array()创建新数组或者直接使用np.copy()复制一份再修改。养成“不可变数据”的习惯会让 autograd 的体验顺畅很多。5.2 网格间距设置过大导致模式计算失真现象基准测试的测试用例能跑通但自己设计的器件优化结果很差透射率始终上不去。原因céviche_challenges 默认的网格间距是 50 纳米量级对 1.55 微米的波长来说每个波长大约 31 个像素这是够用的。但如果你把设计区域改大网格间距跟着变大模式场分布的采样精度会下降模式重叠积分算不准梯度就带上了系统性误差。解决网格间距保持每个波长至少 20 个像素设计区域变大时等比例增加像素数。别为了省内存把网格变粗不然优化出来的是“数值伪影结构”不是能代工的结构。5.3 PML 边界层数不足导致反射干扰现象S 参数的反射率不管怎么优化都下不去而且不同迭代之间反射率剧烈震荡。原因仿真区域边界的完美匹配层PML太薄边界反射没有完全吸收反射波和器件内的光场形成干涉污染了 S 参数的计算结果。解决检查仿真区域的 PML 层数通常需要 10 层以上才能把反射压到 -60 dB 以下。céviche_challenges 的某些配置里 PML 默认只有 5 层我一般会把它加大到 15 层虽然增加了一点内存开销但换来的是梯度计算的稳定性。5.4 优化器学习率设置不当导致目标函数发散现象迭代开始后目标函数一路下降看起来一切正常但到了 50 轮左右突然暴涨再也没收敛回来。原因梯度下降的学习率是常数但设计区域的介电常数梯度在不同迭代阶段尺度差别很大。前期大梯度步进没问题后期接近最优解时固定学习率让参数在最优解附近震荡甚至发散。解决使用带自适应步长的优化器比如 Adam或者给梯度下降加 step decay 策略每 50 轮把学习率乘以 0.5。我一般倾向用 Adam初始学习率设为 1e-3跑 200 轮大部分情况下都能稳定收敛。5.5 目标函数里忘了加电磁场归一化常数现象不同波长下跑 WDM 基准题某个波长的透射率一直偏高换另一个波长反而偏低看起来像是能力不对称。原因FDFD 求解器输出的电场幅度是任意标度的没有归一化到入射功率。不同波长下的模式场分布归一化常数不同直接比较透射率就产生了偏差。解决在目标函数计算前先对每个波长下的场做功率归一化确保入射功率在频率维度是一致的。scattering.py 里自带归一化逻辑但如果自己定义目标函数绕过了这个步骤坑就来了。我后来每次定义新目标函数都会先跑一遍直波导算 S 参数确保透射率接近 1 再继续优化。6. 把 benchmark 改造成自己的算法擂台跑通基准套件只是第一步真正有价值的是把这套题目拿来横向比较不同的拓扑优化算法。这里说一个我常用的进阶流程把默认的梯度下降换掉接入自己的优化器然后用同样的初始条件、同样的迭代次数、同样的目标函数做对比实验。这三个“同样”缺一不可否则变量太多了根本说不清算法优势来自哪里。具体操作上我会在objective函数外面包一层计时和记录逻辑把每一轮的目标函数值、梯度范数、设计区域的二值化程度都存下来。这些曲线画在一张图上A 算法和 B 算法的收敛速度和最终性能一目了然。保存中间设计也很重要——比如每 10 轮存一次eps_vec的快照这样如果最后一轮结果不理想还能回溯到前面某个局部最优解相当于给自己买了“后悔药”。有一个参数特别值得做敏感性分析就是目标函数里透射率和二值化惩罚的权重比。默认配置下惩罚项权重很小主要靠透射率驱动结构演化。但如果把惩罚权重调到过大算法会为了满足二值化约束而牺牲透射率最终得到的结构看着整整齐齐、性能却一塌糊涂——这种情况模拟器不会报错S 参数看起来也有模有样但拿到代工厂流片出来的实际测试结果对不上仿真。做可靠性设计这个权重比的调节就是避不开的玄学环节每个器件的最优比例都不太一样靠实跑实验来标定别信别人的经验值直接抄。还有一条但第一次跑完一遍四个基准题目之后一定要做的检查输出的设计是否能通过最小线宽约束。代工厂的工艺能力决定了几何尺寸低于某个阈值比如 100 纳米的结构无法加工普通梯度优化出来的连续折射率分布偶尔会出现比一个像素还细的“孤岛结构”后处理加个腐蚀或膨胀操作性能就变了。我在自己的流程里加入了一个强制约束每次迭代后对设计区域的介电常数做一次最小线宽过滤把太细的结构直接抹掉。这个做法牺牲了一点最终性能但换来了可制造性。从那以后我每次跑基准题都会把“二值化之后的最小线宽”当作一项硬指标达不到就调惩罚权重重新跑不再只盯着透射率数字好看。希望这个习惯也能帮到你少走弯路。本文还有配套的精品资源点击获取