
简介这份资源面向从事光子器件逆向设计与拓扑优化研究的开发者与研究生提供一套基于开源 ceviche 有限差分频域FDFD模拟器的光子逆设计挑战问题集。它把散射参数与场的计算封装成标准 API并借助 HIPS autograd 实现梯度与自动微分可用于对波导分束器、模式转换器、波导弯曲及波分复用器等集成光子组件的不同拓扑优化算法进行基准测试相关代码曾用于「具有严格代工制造约束」论文的逆向设计结果复现。资源包共 43 个文件以 35 个 Python 源码为主体另含 4 张 png 示意图、2 个 md 说明文档、1 个 yml 配置与 1 份 license压缩包约 772KB目录按组件与测试模块划分结构清晰。目前已有 368 人学习下载。读者可据此获得完整赛题定义、可运行的仿真与优化脚本、单元测试及 README 使用说明快速搭建基准环境并对比算法表现。1. 光子逆设计基准测试这套挑战集到底解决什么问题做集成光子器件的人大多有过这种体验自己写的拓扑优化脚本跑出来的分束器仿真透过率看着不错但换一个算法、换一组超参数结果就完全对不上也没法判断到底是算法不行还是实现有 bug。ceviche-challenges 这套代码就是冲着这个痛点来的——它把波导分束器、模式转换器、波导弯曲、波分复用器这几个典型集成光子组件打包成统一的挑战问题每个问题都配了标准的散射参数计算 API 和场分布输出底层跑的是 ceviche 有限差分频域FDFD模拟器梯度由 HIPS autograd 自动微分提供。换句话说你拿到的不只是一堆示例脚本而是一个可以横向对比不同拓扑优化算法的基准测试台。适合已经了解 FDFD 基本概念、想验证自己逆设计流程是否靠谱的从业者也适合刚接触光子逆设计、想找一个能跑通的最小完整案例的新手。这套代码对应的是那篇讨论严格代工制造约束下光子器件逆设计的论文所以它的器件定义和约束条件不是随便拍的有明确的工程背景。2. 环境搭建与依赖安装从零把 ceviche-challenges 跑起来2.1 为什么选 conda 而不是裸 pipceviche 依赖里有一批科学计算库对底层线性代数后端比较敏感尤其是 FDFD 求解器用到的稀疏矩阵运算。裸 pip 装出来的 numpy/scipy 在某些 BLAS 实现下会出现求解器不收敛或者结果抖动的情况这不是玄学是链接库版本不一致导致的。我一般会先用 conda 建一个干净环境把 numpy、scipy 这些基础件锁在 conda 渠道里再用 pip 装 ceviche 本身和 autograd。这样能避开大部分「装完了跑不通」的问题。# 创建独立环境python 版本建议 3.8~3.10 conda create -n ceviche-chal python3.9 -y conda activate ceviche-chal # 先装科学计算底座锁 conda 渠道 conda install numpy scipy matplotlib -y # 再装 ceviche 和自动微分 pip install ceviche autograd # 验证核心依赖是否可用 python -c import ceviche; import autograd; print(ok)这里 python 版本不建议上 3.11因为 autograd 对较新版本的 numpy 类型系统跟进有延迟容易出现ArrayBox相关的报错。conda 装 numpy/scipy 是为了拿到 MKL 或 OpenBLAS 的预编译版本pip 装 ceviche 是因为它不在 conda 主渠道里。最后那行验证命令能跑通说明 FDFD 求解器和自动微分链路基本就绪。2.2 下载代码包与目录结构确认代码包解压后根目录下能看到ceviche_challenges这个包目录里面按器件类型分了子模块beam_splitter、mode_converter、waveguide_bend、wdm每个子目录下有defs.py定义器件几何和仿真参数params.py放可调参数。顶层还有scattering.py、modes.py、primitives.py、ops.py、units.py这些公共模块测试文件以_test.py结尾。img目录里是各器件的示意图.github/workflows里是 CI 配置能看出作者是用自动化测试保证各器件定义一致性的。# 解压后进入项目根目录 unzip ceviche-challenges-main.zip cd ceviche-challenges-main # 以可编辑模式安装方便改参数后直接生效 pip install -e . # 跑一遍测试确认环境没问题 python -m pytest ceviche_challenges -x -qpip install -e .的好处是你改params.py里的参数后不用重新安装直接跑脚本就生效。pytest加-x是遇到第一个失败就停-q是精简输出。如果测试全过说明 FDFD 求解、模式求解、散射参数计算这几条链路都正常。常见失败是modes_test.py里的模式求解对网格分辨率敏感如果报错先检查params.py里的resolution是不是被改过。2.3 跑通第一个挑战波导分束器波导分束器是最直观的入门器件输入一个波导输出两个波导目标是让指定波长的光按目标比例分配。下面这段代码加载分束器定义、跑一次正向仿真、输出散射参数。import numpy as np from ceviche_challenges import beam_splitter # 加载分束器默认参数 params beam_splitter.defs.BeamSplitterParameters() # 构建仿真模型 model beam_splitter.defs.beam_splitter_model(params) # 用初始介电常数分布跑一次正向仿真 # 这里先用全波导材料填充看基线散射参数 design np.ones(model.design_shape) * params.background_epsilon scattering model.simulate(design) # 输出各端口的散射参数幅度 for name, value in scattering.items(): print(f{name}: {np.abs(value):.4f})BeamSplitterParameters()里封装了波导宽度、输入输出端口位置、仿真区域尺寸、网格分辨率这些参数改这些值就能调整器件规格。model.simulate(design)接收的是设计区域的介电常数分布矩阵返回的是各端口散射参数。第一次跑建议先用均匀背景跑一遍看看基线透过率是多少后面优化时才有对比。如果这一步就报求解器不收敛多半是resolution设得太粗把网格加密到 20 以上再试。3. 拓扑优化算法接入用 autograd 做梯度逆设计3.1 挑战问题的目标函数怎么定义每个挑战问题的核心是一个目标函数输入是设计区域的介电常数分布输出是一个标量损失值。以分束器为例目标是让两个输出端口的功率分配接近 50:50同时总透过率尽量高。ceviche-challenges 里把这类目标封装在scattering.py和ops.py里你不需要自己从场分布里手算散射参数直接调 API 就行。import autograd.numpy as npa from autograd import value_and_grad from ceviche_challenges import beam_splitter params beam_splitter.defs.BeamSplitterParameters() model beam_splitter.defs.beam_splitter_model(params) def loss_fn(design): # 跑正向仿真拿到散射参数 scattering model.simulate(design) # 目标两个输出端口功率尽量相等且总透过率高 s21 npa.abs(scattering[s21])**2 s31 npa.abs(scattering[s31])**2 # 平衡项 透过率项 balance (s21 - s31)**2 throughput 1.0 - (s21 s31) return balance throughput # 用 autograd 拿到损失和梯度 loss, grad value_and_grad(loss_fn)(design) print(floss{loss:.4f}, grad_norm{npa.linalg.norm(grad):.4f})这里用autograd.numpy替代普通 numpy 是关键因为梯度要穿过 FDFD 求解器反向传播。value_and_grad一次调用同时返回损失值和梯度省一次正向仿真。balance项惩罚两个端口功率不平衡throughput项惩罚总透过率不足。实际调的时候这两项的权重可以调如果发现优化后分束比对了但总损耗很大就把throughput的系数加大。3.2 用梯度下降跑一轮完整逆设计拿到梯度后最朴素的优化就是梯度下降。下面这段代码跑 50 轮迭代每轮更新设计变量并记录损失。import numpy as np from autograd import value_and_grad from ceviche_challenges import beam_splitter params beam_splitter.defs.BeamSplitterParameters() model beam_splitter.defs.beam_splitter_model(params) # 初始设计背景介电常数 小随机扰动打破对称 design np.ones(model.design_shape) * params.background_epsilon design 0.01 * np.random.randn(*design.shape) loss_fn ... # 同上节定义 grad_fn value_and_grad(loss_fn) lr 0.05 for step in range(50): loss, grad grad_fn(design) design design - lr * grad # 投影回制造约束范围 design np.clip(design, params.background_epsilon, params.material_epsilon) if step % 10 0: print(fstep {step}: loss{loss:.4f})lr是学习率光子逆设计里一般取 0.01 到 0.1 之间太大容易震荡太小收敛慢。np.clip那一步是把设计变量限制在背景材料和波导材料之间这是最基础的制造约束——你不能让介电常数跑到负值或者超过材料上限。50 轮只是演示实际跑收敛通常要 200 轮以上。如果发现损失下降几轮后就卡住检查一下初始扰动是不是太小对称结构在对称目标下梯度可能为零。3.3 不同拓扑优化算法的对比方法这套挑战集的核心价值就是让你在同一套器件定义和同一套散射参数 API 下对比不同算法。常见做法是固定器件参数和仿真网格只换优化器记录达到目标损失所需的迭代次数和最终器件性能。下面是一个对比框架的骨架。def run_optimizer(optimizer_name, model, params, steps200): design np.ones(model.design_shape) * params.background_epsilon design 0.01 * np.random.randn(*design.shape) history [] if optimizer_name sgd: lr 0.05 for _ in range(steps): loss, grad grad_fn(design) design np.clip(design - lr * grad, params.background_epsilon, params.material_epsilon) history.append(loss) elif optimizer_name adam: m np.zeros_like(design) v np.zeros_like(design) beta1, beta2, eps 0.9, 0.999, 1e-8 lr 0.02 for t in range(1, steps 1): loss, grad grad_fn(design) m beta1 * m (1 - beta1) * grad v beta2 * v (1 - beta2) * grad**2 m_hat m / (1 - beta1**t) v_hat v / (1 - beta2**t) design np.clip(design - lr * m_hat / (np.sqrt(v_hat) eps), params.background_epsilon, params.material_epsilon) history.append(loss) return design, history对比时要注意不同优化器对学习率敏感度不同SGD 通常需要更大学习率Adam 用小学习率更稳。记录history后可以画损失曲线看哪个算法收敛更快、最终损失更低。但别只看损失还要把优化后的设计代回model.simulate看实际散射参数因为损失函数里的近似项可能让损失很低但实际器件性能不达标。这也是这套基准测试集比单跑一个脚本靠谱的地方——它逼你把优化结果放回统一标准里验证。4. 避坑与排查跑这套代码最容易翻车的几个地方4.1 现象仿真跑完散射参数全是 NaN原因通常是网格分辨率设得太粗FDFD 求解器在离散化时产生了奇异矩阵或者设计区域里有介电常数突变导致数值不稳定。解决方法是把params.py里的resolution从默认值往上调一般调到 20~30 之间同时检查design矩阵里有没有超出[background_epsilon, material_epsilon]范围的值。如果调了分辨率还不行把仿真区域边界条件从默认的 PML 改成周期性边界试试有时候是吸收边界和器件模式不匹配。4.2 现象autograd 报 ArrayBox 类型错误这个报错基本是因为在损失函数里混用了普通 numpy 和 autograd.numpy。比如你写了import numpy as np然后在loss_fn里用np.absautograd 追踪不到梯度就会抛ArrayBox相关异常。解决方法是把损失函数里所有 numpy 调用换成autograd.numpy包括abs、sum、sqrt这些。如果某个操作 autograd 不支持用autograd.scipy里的替代实现或者把那段逻辑用primitive自定义梯度。4.3 现象优化后器件性能比初始设计还差常见原因是学习率太大导致设计变量在迭代中跳出了合理范围虽然每步都做了clip但梯度方向可能已经把结构推到了局部极值。另一个原因是初始扰动太小设计从对称结构出发梯度在对称点为零优化器原地不动。解决办法是把学习率降到 0.01 再跑同时把初始扰动幅度从 0.01 加到 0.05 左右。如果还不行换 Adam 优化器它对初始阶段的小梯度更敏感。4.4 现象测试通过但自己写的脚本跑不出结果pytest跑的是作者预设的参数组合你自己写脚本时如果改了params.py里的默认值可能触发了测试没覆盖的边界情况。比如把波导宽度改到小于网格步长模式求解器就找不到导模。排查方法是先把params.py恢复默认跑通一个最小示例再逐项改参数每改一项跑一次仿真定位到具体哪个参数导致失败。这套代码的参数之间耦合比较紧不建议一次性改多个。4.5 现象WDM 器件仿真时间异常长波分复用器的仿真区域比单波长器件大得多因为要覆盖多个波长。如果直接跑默认参数一次正向仿真可能要几分钟。优化时每轮迭代都要跑正向加反向时间会成倍增长。实用做法是先用粗网格跑几十轮看趋势收敛到大致结构后再用细网格精调。另外可以把scattering.py里不需要的端口输出关掉只保留目标波长对应的散射参数能省不少计算量。5. 进阶技巧把挑战集用成自己的算法验证流水线这套代码最值钱的用法不是跑通示例而是把它改造成你自己的算法验证流水线。具体做法是固定defs.py里的器件几何和params.py里的仿真参数只替换优化循环部分把不同算法、不同超参数组合跑出来的结果统一记录到一张表里。我一般会记录这几个量达到目标损失所需迭代次数、最终损失值、优化后器件的实际散射参数、单次迭代平均耗时。下面是一个记录表的字段定义。字段含义记录方式algorithm优化器名称字符串如 sgd/adam/lbfgslr学习率浮点数steps_to_target达到目标损失的迭代数整数未达标记 -1final_loss最终损失值浮点数s21_actual优化后实际散射参数从 model.simulate 重新计算time_per_step单步平均耗时秒用 time.time 差值跑对比实验时有个细节容易忽略不同优化器的停止条件要统一。如果 SGD 跑 200 轮、Adam 跑 100 轮就停对比就不公平。我一般会设一个目标损失阈值谁先达到谁停同时设一个最大迭代上限防止死循环。另外每次实验前用固定随机种子重置初始设计否则初始扰动不同会导致结果不可比。还有一个进阶用法是把制造约束加进优化循环。论文里强调的严格代工约束包括最小特征尺寸、最小间距这些基础版代码只用clip做了介电常数范围约束你可以自己在每轮迭代后加一个形态学开运算或者卷积滤波把小于最小特征尺寸的结构抹掉。这样跑出来的设计更接近可流片状态而不是只在仿真里好看的理想结构。从那以后我每次接新的逆设计算法都强制先在这套挑战集上跑一遍分束器和弯曲两个器件记录基线数据再上自己的器件。这样至少能保证算法本身没问题后面出问题就只可能是器件定义或参数设置的事排查范围小很多。希望帮到你。本文还有配套的精品资源点击获取