简介:这是一份面向光子学工程师与Python开发者的Lumerical Python API入门源码包,适合需要了解API基本架构、会话管理、数据传递以及Lumopt逆向设计流程的人群,可在光子逆向设计优化任务中作为起步参考。资源以可运行源码为主体,共3个文件,包含inscode配置入口、HTML说明页与gitignore工程配置,压缩包仅5KB,结构精简,便于快速导入Lumerical环境查看。结合描述可知,内容覆盖Lumerical Python API在数据分析、复杂工作流自动化、求解器脚本调用、优化与高质量图表生成方面的核心能力,并涉及2019a R3及以上版本的调用前提、Linux bash shell适配、GUI许可证交互等实践要点。已有119人学习。从预览看,包内提供了可直接调用的工程骨架与配套说明页面,读者可据此搭建自己的光子逆向设计实验,尤其在纳米线应用、光栅耦合器逆向设计等场景中快速验证Lumopt优化思路,节省环境配置和摸索时间。
1. Lumerical Python API 是什么:把 FDTD 仿真从「手点界面」变成「脚本驱动」
做纳米光子器件设计的人应该都有过这种体验:为了扫一个厚度参数,在 Lumerical 界面里改一次数、点一次 run、等一次仿真结束,再手动把结果抄进表格。一个参数扫二十个点,就是二十次重复劳动。Lumerical Python API 解决的就是这件事:它把 FDTD、MODE 等仿真引擎封装成 Python 的 lumapi 模块,让你用脚本替代手工操作,把参数扫描、优化迭代和结果后处理串成一条自动化流水线。这篇文章里的源码都是可运行的最小版本,适合正在评估要不要把仿真流程脚本化的工程师,也适合已经被重复操作折磨、想找个可靠替代方案的人。
2. 先跑通最小脚本:环境准备、lumapi 导入与三种打开工程的方式
2.1 版本匹配是第一道门槛:Python 解释器与 lumapi 模块位置
lumapi 不是 PyPI 上的独立开源包,它是随 Lumerical 套件一起安装到本地的模块。所以第一道门槛不是「pip install 一下就行」,而是确认你当前的 Python 解释器和 Lumerical 安装版本是否匹配。不同大版本自带的 Python 环境不一样,老版本可能对应 Python 3.6,新版本可能到 3.8 或更高;解释器选错,后面所有脚本都跑不起来。
我遇到过不止一次这样的现场:某同事在自己的 Anaconda 环境里装了一堆科学计算包,然后 import lumapi 直接 ModuleNotFoundError。这不是代码逻辑问题,而是模块根本没出现在当前解释器的搜索路径里。排查顺序应该是先确认 python 版本,再确认 lumapi 是否能加载,最后才谈仿真脚本怎么写。
import sys print(sys.version) try: import lumapi print("lumapi 加载成功:", lumapi.__file__) except ImportError: print("import lumapi 失败:当前解释器路径里没有这个模块")这段代码的作用很直接:sys.version 告诉你当前解释器版本,lumapi.file打印模块物理路径,一眼就能看出你加载的是哪个安装目录下的副本。如果 import 失败,常见做法是把 Lumerical 安装目录下 python 子目录里的 site-packages 路径手动加进 sys.path,或者直接换用该目录自带的 python.exe 建虚拟环境,而不是在自己原来的环境里硬凑依赖。注意不同安装版本的路径有差异,以你机器上的实际目录为准。
2.2 lumapi.FDTD 的三种打开方式:新工程、已有工程与后台模式
lumapi 的入口是按产品拆分的:FDTD 对应 lumapi.FDTD,MODE、INTERCONNECT 各有自己的类。用 FDTD 举例,打开一个工程有三种方式,对应三种不同的使用节奏。
import lumapi # 1. 新建空白工程 sim_new = lumapi.FDTD() # 2. 打开已有 .fsp 工程文件 sim_open = lumapi.FDTD("D:/projects/grating.fsp") # 3. 后台模式打开,不弹主界面 sim_hidden = lumapi.FDTD("D:/projects/grating.fsp", hide=True)新建工程适合从零搭仿真,参数完全由脚本控制;打开已有工程适合把参数扫描逻辑挂到现成的模型上,模型里已经配好的材料、边界条件可以直接复用;hide=True 是服务器批量跑仿真的关键,不渲染界面,窗口部件全部跳过,速度更快、资源占用也更省。三种方式拿到的 sim 对象用法完全一样,后面调 run、getresult 没有区别。
| 打开方式 | 适用场景 | 注意点 |
|---|---|---|
| lumapi.FDTD() | 从零搭建仿真,全部由脚本生成 | 对象多时要手动管理每步添加的命名 |
| lumapi.FDTD("xx.fsp") | 在已有模型上做参数修改与批量跑 | 确认原模型里的对象名,避免 setnamed 静默失败 |
| lumapi.FDTD("xx.fsp", hide=True) | 服务器批处理、长时间扫描 | 没有弹窗提示,排错要依赖日志和打印 |
还有一个习惯要养成:每个 sim 对象用完都要 close()。不 close 的话,句柄会一直占着,循环批量跑的时候机器会肉眼可见地变卡,后面章节会专门讲这个坑。
2.3 最小可运行脚本:加区域、加光源、加监视器、取结果一次走通
下面这个脚本是「加仿真区域 → 加光源 → 加监视器 → 运行 → 取结果 → 关闭」的最小闭环,也是后面所有章节的骨架。直接复制到本地,按你的需求改物理尺寸就能跑。
import lumapi # 新建 FDTD 工程 sim = lumapi.FDTD() # 1. 全局工作波长设为 1550 nm sim.setglobalmonitor("wavelength", 1550e-9) # 2. 加入 FDTD 仿真区域 sim.addfdtd() sim.setnamed("FDTD", "x span", 4e-6) sim.setnamed("FDTD", "y span", 4e-6) sim.setnamed("FDTD", "z span", 4e-6) sim.setnamed("FDTD", "mesh accuracy", 2) # 3. 加入平面波光源,沿 x 轴正向入射 sim.addplane() sim.set("name", "source") sim.setnamed("source", "injection axis", "x") sim.setnamed("source", "direction", "forward") sim.setnamed("source", "x", -1.5e-6) sim.setnamed("source", "wavelength start", 1500e-9) sim.setnamed("source", "wavelength stop", 1600e-9) # 4. 在出射端加功率监视器 sim.addpower() sim.set("name", "monitor_T") sim.setnamed("monitor_T", "monitor type", "Linear X") sim.setnamed("monitor_T", "x", 1.5e-6) sim.setnamed("monitor_T", "override global monitor settings", 1) sim.setnamed("monitor_T", "use wavelength spacing", 1) sim.setnamed("monitor_T", "wavelength center", 1550e-9) sim.setnamed("monitor_T", "wavelength span", 100e-9) # 5. 运行仿真 sim.run() # 6. 取回透射率结果并打印 result = sim.getresult("monitor_T", "T") print("波长(nm):", result["lambda"] * 1e9) print("透射率:", result["T"]) sim.close()这段脚本里有两个关键动作。一个是 set 与 setnamed 的配合:addplane() 会把新对象设为当前选中,紧接着的 set("name", "source") 给它改名,后面所有参数都用 setnamed 按名字寻址,不依赖当前选中状态。另一个是单位:所有数值都用国际单位制,尺寸用米,波长用米,这是 Lumerical 脚本语言的一贯约定,写成 1550e-9 就是 1550 nm。run() 是阻塞调用,仿真没结束不会返回;getresult 返回一个类似 dict 的结构,键名与脚本语言里的结果名一致。
提示:如果你用的套件版本比较新,个别参数名可能和旧文档不一致。排查时在脚本里用 getnamed("对象名", "属性名") 回读当前值,能快速确认参数有没有设置成功。
3. 用 setnamed 与 set 把仿真搭出来:光源、监视器与网格的参数设置
3.1 set 与 setnamed 的区别:最近对象与按名寻址
新手最容易混的就是这两个命令,区别用一句话就能讲清:set 作用于「当前被选中的对象」,setnamed 按对象名寻址。addxxx 类的命令都会把新添加的对象设为选中态,所以 addplane 之后立刻 set("name", "source") 给对象改名;改完名之后,再用 setnamed 就完全不关心当前选中了谁。凡是写函数、写循环,我一般只用 setnamed,因为函数体内你不知道外部选中状态是什么,按名寻址最不容易出错。
sim.addrect() sim.set("name", "slab") # 给刚加的矩形改名为 slab sim.setnamed("slab", "x span", 3e-6) # 按名字设 x 方向跨度 sim.setnamed("slab", "y span", 3e-6) sim.setnamed("slab", "z span", 0.5e-6) # 厚度 0.5 微米 sim.setnamed("slab", "material", "SiO2 (Glass) - Palik")这里 addrect() 创建的是一个矩形结构,默认材质来自材料库;如果你不指定 material,它就用 Lumerical 默认的折射率材料。实际项目里,我几乎每个对象都会先 set("name", ...) 起一个能看懂的名字,再加一组 setnamed 配置参数。这样脚本跑完、反过来读代码,每个对象是什么、设了什么参数,一目了然。set 这种「作用于最近对象」的行为虽然方便,但在复杂脚本里很容易因为添加顺序变化而出玄学问题,能不用就不用。
3.2 光源选型:平面波、高斯光与模式光源怎么进脚本
光源选型直接决定仿真结果有没有意义。平面波光源适合计算透射、反射这类远场量,边界条件干净;高斯光源适合波导和片上器件,光束有尺寸概念;模式光源则用于波导模式分析,需要配合模式求解器,参数最复杂。三者用脚本添加的方法一致,都是 add 一条命令再加参数。
# 平面波:计算透射反射最常用 sim.addplane() sim.setnamed("source", "injection axis", "x") sim.setnamed("source", "direction", "forward") # 高斯光束:适合片上耦合类器件 sim.addgaussian() sim.setnamed("source_g", "injection axis", "z") sim.setnamed("source_g", "waist size", 1.2e-6) sim.setnamed("source_g", "wavelength start", 1500e-9) sim.setnamed("source_g", "wavelength stop", 1600e-9) # 模式光源:先确保工程里有模式求解设置 sim.addmode() sim.setnamed("source_mode", "mode selection", "fundamental")光源参数里最常调的是 injection axis 和 direction。injection axis 决定光从哪个轴入射,direction 决定是 forward 还是 backward,这两个值写错,结果通常会对称地跑偏。wavelength start 和 wavelength stop 定义光谱范围,配合全局波长设置一起用。模式光源我不建议在没跑过模式求解的情况下直接上,参数多、报错信息又不够直观,先用平面波把流程跑通再换。
| 光源类型 | 典型用途 | 必调参数 |
|---|---|---|
| 平面波 addplane | 透射率、反射率、远场 | injection axis、direction、波长范围 |
| 高斯光 addgaussian | 波导耦合、片上器件 | waist size、injection axis |
| 模式光源 addmode | 波导模式追迹、模式匹配 | mode selection、模式阶数 |
3.3 监视器与网格覆盖区域:参数设置决定结果可靠性
监视器是取数的接口,选错类型等于白跑。功率监视器取 T 和 R,场监视器取 E 和 H 的空间分布,索引监视器取折射率分布。网格覆盖区域(mesh override region)则是控制局部精度的手段,光靠全局 mesh accuracy 一个参数,很难兼顾计算量和关键区域的精度。
# 功率监视器:获取透射率 sim.addpower() sim.setnamed("pow_m", "monitor type", "2D Z-normal") sim.setnamed("pow_m", "z", 0) # 场监视器:获取电场空间分布 sim.addfield() sim.setnamed("field_m", "x span", 2e-6) sim.setnamed("field_m", "y span", 2e-6) # 网格覆盖区域:在结构附近加密网格 sim.addmesh() sim.setnamed("mesh1", "dx", 10e-9) sim.setnamed("mesh1", "dy", 10e-9) sim.setnamed("mesh1", "dz", 10e-9)功率监视器的 monitor type 很关键,它决定监视器是线、面还是体。2D Z-normal 表示一个法线沿 z 轴的平面监视器,穿透率 T 是穿过整个平面的功率积分结果。场监视器的 x span、y span 决定记录区域的大小,区域越大内存占用越高。网格覆盖区域的 dx、dy、dz 是网格步长,10e-9 就是 10 纳米;步长越细,局部精度越高,跑得越慢。一个实用的经验是:先用 mesh accuracy 2 全局粗跑一次确认模型没问题,再在关键结构附近加 mesh1 加密,最后跑正式结果,不要一上来就全局限到最细。
3.4 把搭仿真封装成函数:一份可复用的 build_sim
脚本化优势的体现,是把「搭仿真」这个动作封装成函数。参数全放在一个 dict 里,函数体只做参数的翻译,换一组几何尺寸就是换一份 dict,不用改任何结构逻辑。
def build_sim(sim, params): # 仿真区域 sim.addfdtd() sim.setnamed("FDTD", "x span", params["xspan"]) sim.setnamed("FDTD", "y span", params["yspan"]) sim.setnamed("FDTD", "z span", params["zspan"]) sim.setnamed("FDTD", "mesh accuracy", params["accuracy"]) # 平面波光源 sim.addplane() sim.set("name", "source") sim.setnamed("source", "injection axis", "x") sim.setnamed("source", "wavelength start", params["lam_start"]) sim.setnamed("source", "wavelength stop", params["lam_stop"]) # 功率监视器 sim.addpower() sim.set("name", "monitor_T") sim.setnamed("monitor_T", "monitor type", "Linear X") sim.setnamed("monitor_T", "x", params["monitor_x"]) return sim params = { "xspan": 4e-6, "yspan": 4e-6, "zspan": 4e-6, "accuracy": 2, "lam_start": 1500e-9, "lam_stop": 1600e-9, "monitor_x": 1.5e-6, } sim = lumapi.FDTD() build_sim(sim, params)返回值直接返回 sim,方便链式操作。这个函数体里没有任何和具体项目相关的硬编码数字,所有物理量都从 params 读。后面做参数扫描,只需要在一个循环里修改 params 字典对应键,代码一行都不用动。这是把 Lumerical Python API 用出价值的第一步:先封装,再批量。
4. 把数据拿回来:getresult、getdata 与透射率计算的完整套路
4.1 run 之后有哪些取数命令
run() 跑完之后,取数命令按用途分。getresult 取的是监视器的计算量,比如功率监视器的 T、R;getdata 取的是原始场数据,比如场监视器的 E、H;还有几个专用命令,比如取电场的 getelectricfield,取折射率分布的 getindex。命令本身不难,难的是搞清楚每个监视器对应哪些结果名。
# 功率监视器取透射率 res_T = sim.getresult("monitor_T", "T") # 场监视器取电场分量 res_E = sim.getdata("field_m", "E") # 直接取监视器的透射率曲线 T_line = sim.gettransmission("monitor_T")getresult 和 getdata 返回的结构类似,都是带键名的数据容器,区别在于结果名的合法值:功率监视器认 T、R,场监视器认 E、H,索引监视器认 index。记不住没关系,但不要猜,猜错的结果是运行时报错或返回空数据,浪费一次完整的仿真时间。
| 命令 | 适用监视器 | 常见结果名 |
|---|---|---|
| getresult | 功率、场、索引 | T、R、E、H、index |
| getdata | 场、索引 | E、H、x、y、z |
| gettransmission | 功率 | 透射率曲线 |
| getindex | 索引 | 折射率分布 |
4.2 result 返回结构:dict 形式的键值对与 numpy 数组
getresult 返回的对象用起来和 dict 一样,键包括物理量本身,也包括坐标轴。新手最容易犯的错是直接打印整个返回值,然后被满屏的数字吓住;正确做法是先看它有哪些键,再看每个键对应的数组形状。
res = sim.getresult("monitor_T", "T") print(res.keys()) print("lambda 形状:", res["lambda"].shape) print("T 形状:", res["T"].shape)这段代码打印出的 keys 一般会包含 lambda 和 T,lambda 是波长轴,单位是米,T 是归一化透射率。拿到手之后要注意两个细节:一是 lambda 永远以米为单位,打印前要换算成纳米;二是如果结果是复数场,比如 E 分量,要用 abs 取模再平方才得到强度。曾经有人直接拿复数模值当强度用,结果曲线形态完全对不上,最后发现是忘了取平方,血泪教训。
4.3 算透射率并落盘:一个完整后处理脚本
仿真数据不落盘等于白跑。下面这段代码把透射率曲线转成常用单位、提取特定波长的值,最后存成 CSV。
import numpy as np res = sim.getresult("monitor_T", "T") lam = res["lambda"] * 1e9 # 转成纳米 T = res["T"] # 找 1550 nm 附近最近的数据点 idx = np.argmin(np.abs(lam - 1550.0)) print("1550 nm 处透射率: {:.4f}".format(T[idx])) # 全曲线落盘 np.savetxt( "transmission.csv", np.column_stack([lam, T]), delimiter=",", header="lambda_nm,T", comments="" )argmin 是取最近波长点下标,如果波长间隔比较粗,比如全局只设置了几个频率点,直接用 argmin 会有偏差,更严谨的做法是用 np.interp 做一次线性插值。savetxt 对一维光谱数据够用,如果后面要存场分布这种多维数据,再考虑 h5py。后处理脚本的通用套路就是:取数 → 换算单位 → 提取特征量 → 落盘,四步固定,换结构只换中间两步。
5. Lumerical Python API 避坑现场:5 个高发问题的现象与排查
5.1 import lumapi 直接报 ModuleNotFoundError
现象:Lumerical 套件明明装好了,但在自己的 Python 环境里执行 import lumapi,直接 ModuleNotFoundError。
原因:当前解释器和 Lumerical 安装不对应。lumapi 模块只存在于套件自带的 Python 环境或特定解释器路径里,你自己另装的 Anaconda、系统 Python 都不在搜索范围内。还有一类情况是环境变量被改过,模块路径没被注册。
解决:先按 2.1 的检查脚本确认 sys.version 和 lumapi.file。如果失败,直接改用 Lumerical 安装目录下的 python.exe 建虚拟环境,这是最简单可靠的路子;或者把该目录下 site-packages 的路径手动 append 到 sys.path。不要在原环境里硬装依赖,容易把 numpy 版本搞乱。
5.2 getresult 返回空数据或报错
现象:run() 正常结束,getresult("monitor_T", "T") 返回空,或者报 key 不存在的错。
原因:监视器名字写错、结果名写错、监视器平面位置在仿真区域之外导致没有采集到数据。还有一种常见情况是拿 getdata 去取功率监视器的 T,命令和监视器类型不匹配。
解决:run 之前先用 getnamed 回读监视器属性,确认对象名存在;run 之后先打印 res.keys() 看实际返回了哪些键。如果监视器位置在 PML 边界上,功率计算本身就没意义,把监视器往结构方向挪一挪再跑。检查顺序:名字 → 类型 → 位置,三步走完基本能定位。
5.3 setnamed 改了参数结果却没变
现象:脚本里 setnamed("slab", "z span", 1e-6),跑完结果和修改之前一模一样,仿佛参数没生效。
原因:对象名不对,setnamed 静默失败,不报错但也不生效;或者后面又用同类 add 命令创建了新对象,脚本语言里的重名规则把旧的顶掉了;再一种可能是两个结构重叠,被上层结构覆盖了显示但没覆盖材料属性。
解决:setnamed 之后立刻用 getnamed 回读同一属性做断言,值对不上就抛异常,不要默默往下跑。给每个对象起唯一名字,add 完马上 set("name", ...),避免重名覆盖。结构重叠问题需要检查几何位置,特别是从模板工程继承来的对象,先列一遍所有对象再动手改。
5.4 批量循环内存与句柄暴涨
现象:循环跑 50 组参数,跑到第 20 组机器明显变卡,后面直接卡死,进程结束不了。
原因:循环里每次 new 一个 lumapi.FDTD() 却没有 close,句柄和内存一点一点累积;或者用了默认的界面模式,每轮循环弹一个窗口,窗口越积越多。
解决:循环体里用 try/finally 包住 close(),保证报错时也能释放资源。批量场景统一用 hide=True。数据每轮落盘,不要攒在内存里最后统一写。写进度打印,每处理一组输出一行,至少能知道卡在哪一步。
5.5 后台模式卡死且没有日志
现象:hide=True 跑大批量,某一步没有输出,进程占着 CPU 但一直不结束,界面又看不到任何提示。
原因:网格过细导致内存不够,或仿真本身发散跑不出收敛结果。后台模式把错误弹窗吞掉了,异常没有任何可见出口。
解决:先用界面模式跑单次,确认模型本身能正常结束;批量时控制 mesh accuracy 上限和内存预算。循环里每轮打印进度并 flush,把 print 的 end 参数设置好,保证日志实时刷新。如果某个参数点跑不过,记录参数并跳过,不要让整个批处理死在一组数据上。
6. 把 API 用出效率:一个带容错和落盘的参数扫描模板
把前面的函数和排查经验合起来,就是一个可以直接投入使用的参数扫描模板。它的核心不是跑得快,而是跑得稳:单点失败不影响整体,数据随时落盘,进度随时可见。
import numpy as np import lumapi def build_sim(sim, params): sim.addfdtd() sim.setnamed("FDTD", "x span", params["xspan"]) sim.setnamed("FDTD", "y span", params["yspan"]) sim.setnamed("FDTD", "z span", params["zspan"]) sim.setnamed("FDTD", "mesh accuracy", params["accuracy"]) sim.addplane() sim.set("name", "source") sim.setnamed("source", "injection axis", "x") sim.setnamed("source", "wavelength start", params["lam_start"]) sim.setnamed("source", "wavelength stop", params["lam_stop"]) sim.addpower() sim.set("name", "monitor_T") sim.setnamed("monitor_T", "monitor type", "Linear X") sim.setnamed("monitor_T", "x", params["monitor_x"]) return sim # 扫描 slab 厚度 sweep_values = [0.15e-6, 0.20e-6, 0.25e-6, 0.30e-6, 0.35e-6] rows = [] for i, zspan in enumerate(sweep_values): print(f"[{i+1}/{len(sweep_values)}] 厚度 = {zspan*1e9:.0f} nm", flush=True) sim = lumapi.FDTD(hide=True) # 后台模式,避免窗口堆积 try: base = { "xspan": 4e-6, "yspan": 4e-6, "zspan": zspan, "accuracy": 3, "lam_start": 1500e-9, "lam_stop": 1600e-9, "monitor_x": 1.5e-6, } build_sim(sim, base) sim.run() res = sim.getresult("monitor_T", "T") T_at_1550 = np.interp(1550e-9, res["lambda"], res["T"]) rows.append([zspan * 1e9, T_at_1550]) # 每轮立即收集 except Exception as e: print(f" !! 厚度 {zspan*1e9:.0f} nm 失败: {e}", flush=True) rows.append([zspan * 1e9, float("nan")]) # 失败也记录,保持对齐 finally: sim.close() np.savetxt("sweep_result.csv", np.array(rows), delimiter=",", header="zspan_nm,T", comments="")这个模板比最小脚本多了三处关键设计。一是 try/except/finally:单组参数失败只影响这一行,不会让整个扫描停摆,finally 保证 sim 一定被关闭。二是 np.interp 替代 argmin,光谱点不够密时结果更准确。三是每轮立即收集并最终一次性落盘,既避免内存累积,又保证数据完整。扫描毛坯厚度、波导宽度、周期这类几何参数时,这个模板直接改 sweep_values 和 base 字典就能用。
把这个模板再往前推一步,就是优化算法的闭环:把 T_at_1550 作为目标函数返回值,外面套任意优化器迭代更新 zspan,Lumerical Python API 在其中只当一个可调用的仿真函数用。这也是这个方向最值得投入的地方——仿真本身不变,变的是驱动它的方式。这几年我用这个套路处理过不少参数扫描需求,翻车基本都栽在资源释放和参数名上,模板里这两点都已经堵死。现在我的习惯是每个批量脚本必带进度打印和失败兜底,先让它自己跑一晚,第二天看 CSV 就能判断趋势。希望帮到你。
本文还有配套的精品资源,点击获取