ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Lumerical FDTD脚本建模:从基础API到光栅耦合器参数化实战

Lumerical FDTD脚本建模:从基础API到光栅耦合器参数化实战 做FDTD仿真的人迟早会遇到同一个问题结构稍微复杂一点手动画图就变成纯粹的体力活。我第一次在Lumerical FDTD Solutions里搭建带光栅的波导结构时还老老实实在GUI里一个个拖矩形、改尺寸、设材料等把8个齿摆完半小时已经没了。更崩溃的是后面要改成16个齿又得从头再来一遍。从那天起我几乎所有结构创建都改成了脚本方式。这篇教程就从“用FDTD脚本创建结构”这个主题讲起内容包括最基础的对象创建API、属性设置方法、参数化与扫描思路最后用一个光栅耦合器作为完整案例跑一遍。想学脚本建结构的新手可以按顺序看已经会基础操作的可以直接跳到第三节和第四节看参数化方法。1. 为什么要用脚本创建结构手工建模的几个真实痛点1.1 重复操作与批量生成才是脚本的主场先说最直接的场景。你在设计亚波长光栅、超表面或者光子晶体时结构动不动就是几十上百个单元。手动画的话每一个单元都要重复一遍“添加矩形-设置尺寸-设置位置-设置材料”这个流程改一个参数全部重来。脚本的核心优势在于循环同样的代码加一个for8个齿、64个单元、几百个圆孔都是一次性生成而且不会出现手动点歪、漏点的问题。我第一次用循环生成一排圆孔阵列时脚本运行完模型树里齐刷刷地出现了64个圆孔那个感觉真的是“这才对”。1.2 参数扫描时的“改一个值重画一遍”其实没必要做FDTD仿真的人绕不开参数扫描。最痛苦的做法是手动改结构的宽度、跑一次仿真、记录结果、再改、再跑。如果直接在GUI里改对象属性还可能漏改或改错。用脚本之后扫描参数可以直接挂在对象属性或者结构组变量上一次把起始值、终止值、步长设好仿真自动跑完结果自动汇总。参数扫描是脚本化结构创建最刚需的理由没有之一。我见过不少同事用GUI手动扫描一个三参数扫描能折腾一整天换成脚本之后设置完下班走人第二天早上结果就齐了。1.3 可复现性与版本管理还有一个容易被低估的好处脚本让仿真模型具备可复现性。同一个结构三个月后回头再看鼠标点过的东西你可能忘了一大半而脚本还清清楚楚地写在那里每个尺寸、每层材料都有据可查。配合脚本文件版本管理组里任何人拿到脚本都能重建出完全一致的模型。这一点在写论文和团队协作时特别重要审稿人要求提供仿真文件时脚本就是最好的“实验记录”。我个人的习惯是每个仿真项目建一个scripts文件夹里面按日期保存脚本版本模型文件里的对象命名也和脚本一一对应后期回溯非常快。1.4 脚本化不等于放弃GUI要注意的是“用脚本创建结构”不等于所有步骤都写代码。我的习惯是初期调试和观察单个结构的几何形状时用GUI确认没问题后再把整个过程固化成脚本。Lumerical的脚本编辑器和布局窗口是实时联动的脚本运行完可以立刻在GUI里旋转视角、检查截面、核对尺寸。脚本负责效率和可复现GUI负责检查和直观理解两者是互补关系。有些新手容易走极端要么全程手点要么强行纯脚本不用界面其实都不太健康。2. 结构创建脚本的核心API从addrect到set属性2.1 基础结构原语矩形、圆柱、多边形、球体Lumerical脚本创建结构的API非常有规律一个结构对应一个add函数创建之后当前对象会自动处于“选中”状态紧接着用set函数设置属性即可。最常用的几个原语我列在下面脚本函数结构类型常见用途addrect矩形块波导、衬底、光栅齿、金属电极addcircle圆柱体圆孔、纳米柱、光纤端面addsphere球体纳米球、散射粒子addpoly多边形柱体锥形波导、任意截面结构addstructuregroup结构组封装多个结构配合扫描与参数化每个函数调用之后新建对象就处于选中状态直接在后面跟set就能修改属性。比如最简单的创建addrect; set(name, box);这行代码会在布局里创建一个默认尺寸的矩形块同时命名为box。如果你在脚本编辑器里按F5运行完切到Layout窗口就能看到模型树里多了一个叫box的对象。2.2 set方法定位、尺寸、旋转与材料在Lumerical里set是通用属性修改函数格式是set(属性名, 值)。结构创建最常用到的属性有这么几类位置属性x、y、z以及x span、y span、z span。其中x、y、z是结构中心的坐标span是沿轴的总长度。注意脚本里面所有长度单位都是米这是新手最容易踩的坑后面单开一节详细说。设置材料的属性名是material值是材料库里的字符串名称例如set(material, Si (Silicon) - Palik);材料名必须和材料库里的名称完全一致包括括号和空格。你可以在GUI的材料下拉框里直接复制避免手打出错。也可以用listmaterial命令把当前可用的材料全部打印到脚本提示区。旋转相关的属性是rotation x、rotation y、rotation z单位是度set(rotation z, 45);这个在设置倾斜结构时会用到比如MEMS反射镜阵列或者斜入射光栅。旋转轴默认经过结构中心如果需要绕其他轴旋转还可以配rotation x axis、rotation y axis、rotation z axis这几个属性指定旋转轴位置。2.3 对象命名与层级关系每个结构都应该有个有意义的名字方便后续select、sweep和排查。命名用set(name, ...)命名之后可以用select(name)重新选中它再get或set属性。比如select(waveguide); set(x span, 0.6e-6);就能把名为waveguide的对象宽度改成0.6微米。这里有个容易忽略的点select的字符串要和set(name, ...)时完全一致大小写、空格、下划线都不能错。Lumerical的模型树是按名称索引的名称冲突或拼写错误都会导致操作失败。层级管理主要靠结构组。addstructuregroup创建组之后用addtogroup把当前选中的结构加进去。批量生成的同类结构建议放进一个组这样在层叠关系、材料覆盖和扫描设置时会省很多事。分组还有一个好处是整体移动、整体隐藏几何检查起来清爽得多。2.4 用一条最小脚本跑通创建-设置-查看流程为了让新手对整个过程有体感这里给一个最小可运行示例switchtolayout; addrect; set(name, si_waveguide); set(x, 0); set(y, 0); set(z, 0); set(x span, 4e-6); set(y span, 0.5e-6); set(z span, 0.22e-6); set(material, Si (Silicon) - Palik);这段代码在脚本编辑器里按F5运行后切到布局窗口就能看到模型树里的矩形块和3D视图里的实体结构。你可以在脚本编辑器里把z span改成0.3e-6再运行结构会立即更新这就是脚本创建结构最直观的反馈。这一小段代码虽然简单但它包含了脚本建结构的所有关键要素切换到正确模式、创建原语、设置命名、设置位置、设置尺寸、设置材料。3. 让结构参数化变量、表达式与sweep扫描3.1 脚本变量与GUI的联动方式Lumerical脚本不是简单的录制回放它本质上是一种类似MATLAB的脚本语言支持变量、表达式、循环和函数。这让结构创建从“一次画一个”变成“定义规则”。比如period 0.7e-6; width 0.35e-6; addrect; set(name, bar); set(x span, width);这里period和width是脚本里的变量它们不会自动出现在GUI的某个输入框里但脚本运行时决定了对象属性。如果需要让变量在GUI中可调、可扫描就要结合结构组的本地变量local variables。这一步是很多初学脚本的人卡住的地方我单独在3.4节展开。3.2 表达式驱动的尺寸联动结构设计里经常需要“一个参数变了其他尺寸跟着变”。比如占空比duty等于线宽除以周期如果你想扫描周期同时保持占空比不变就得让线宽由period和duty共同决定duty 0.5; period 0.7e-6; line_width period * duty; addrect; set(x span, line_width);脚本在运行时会先计算line_width再创建结构所以改duty、改period最终都能自动得到对应的几何。这就是参数驱动建模的核心逻辑。用表达式链路把几何参数串起来之后做优化才不会被单一变量卡死。实际工程中衬底厚度、刻蚀深度、周期之间往往有耦合关系比如固定深宽比时凹槽宽度变了深度也得跟着变这种联动用表达式处理最方便。还有一个很有用的内置函数是num2str它能把数字转成字符串配合循环给对象命名。比如for(i 1:8) { addrect; set(name, tooth_ num2str(i)); }这样生成的8个对象名称分别是tooth_1、tooth_2到tooth_8。如果你不需要对象名带序号也可以统一命名成同一个名字但那样后面select的时候会把所有同名对象一起选中容易导致set操作作用范围超出预期所以还是建议用序号区分。3.3 参数扫描的挂载方式Lumerical提供sweep功能来做参数扫描脚本里对应的API是addsweep、setsweep和addsweepparameter。最简单的一种用法是选中一个对象把它的某个属性设为扫描参数select(si_waveguide); addsweep; setsweep(name, width_sweep); setsweep(sweep_type, parameter); addsweepparameter(x span, 0.3e-6, 0.7e-6);addsweepparameter的格式是“参数名或对象属性路径起始值终止值”。扫描任务会在仿真中自动生成多个case并依次运行。这里要注意的是addsweepparameter支持的是对象属性路径比如waveguide::x span或者是结构组里的参数名不要凭感觉乱写。如果你希望一次性扫多个参数可以连续多次调用addsweepparameter但要注意参数组合会成倍增加计算资源和时间消耗要提前评估。扫描完的结果会在sweep的result选项卡里可以用脚本提取也可以直接在GUI里画图。3.4 结构组在参数扫描中的关键作用假如你有8个光栅齿宽度都等于line_width能否通过sweep让它们一起变答案是可以但要注意方法。直接对每个齿单独添加sweep参数不仅繁琐而且扫描维度会爆炸。正确做法是把整个光栅封装在结构组里在结构组的本地变量中定义period、duty、thickness等参数结构组内部的几何创建脚本统一引用这些变量。之后sweep时只需要把period、duty作为扫描参数添加一组几何就会自动按变量变化。这也是Lumerical结构组在参数化建模里最重要的价值它不只是视觉上的分组更是一个参数封装和复用单元。你可以在结构组的脚本选项卡里编写几何生成逻辑把局部变量暴露给sweep也可以先用普通脚本建好结构再通过addtogroup把对象收进组里然后对组内属性做操作。两种方式都能用但如果你要做“多参数联动扫描”结构组脚本变量方案才是干净的路径。4. 实战用脚本构建一个光栅耦合器结构4.1 设计目标与几何参数这个案例的目标是创建一个典型的波导光栅耦合器结构底部是玻璃衬底上面一层硅波导波导表面刻蚀出一排空气凹槽形成光栅。所有几何通过脚本控制关键参数定义为变量方便之后做扫描。结构的基本几何参数如下参数含义初值sub_thickness衬底厚度0.5e-6米si_thickness波导层厚度0.22e-6米period光栅周期0.7e-6米duty光栅占空比0.5grating_depth刻蚀深度0.1e-6米width波导宽度0.5e-6米num_teeth光栅齿数8实际设计时光栅周期通常需要结合目标波长和有效折射率来估算这里为了演示脚本参数本身并不代表某个经过优化的最优值。脚本的价值在于给定一套参数几十行代码就能把结构完整重建出来后续调参只是改最上面几行赋值。4.2 创建衬底与波导层先在脚本里把衬底矩形和波导矩形建出来switchtolayout; # 衬底 addrect; set(name, substrate); set(x, 0); set(y, 0); set(z, 0); set(x span, 8e-6); set(y span, 8e-6); set(z span, 0.5e-6); set(material, SiO2 (Glass) - Palik); # 波导层 addrect; set(name, si_layer); set(x, 0); set(y, 0); set(z, 0.36e-6); set(x span, 8e-6); set(y span, 0.5e-6); set(z span, 0.22e-6); set(material, Si (Silicon) - Palik);这里z坐标的选择很关键。我习惯把衬底几何中心放在z0那么衬底顶面就在z0.25e-6的位置也就是衬底z span的一半。硅波导层的中心放在z0.36e-6z span是0.22e-6那么它的底面就是0.36e-6减去0.11e-6正好等于0.25e-6刚好和衬底顶面贴平。这个坐标推算的思路比死记数字重要因为当你把衬底厚度改成0.8微米时波导层的z中心也得跟着动否则就会悬空或者嵌入。4.3 用循环生成光栅齿光栅齿本质上是波导表面的一组空气凹槽。我的做法是在硅层上方加一组空气矩形块凹槽中心设在波导顶面以下grating_depth/2的位置period 0.7e-6; duty 0.5; grating_depth 0.1e-6; num_teeth 8; for(i 1:num_teeth) { addrect; set(name, tooth_ num2str(i)); set(x, -4e-6 (i - 0.5) * period); set(y, 0); set(z, 0.42e-6); set(x span, period * duty); set(y span, 0.6e-6); set(z span, grating_depth); set(material, air); }解释一下z坐标0.36e-6是硅层中心0.11e-6是硅层半厚度所以波导顶面在0.47e-6。空气槽的中心设在顶面往下grating_depth/2也就是0.47e-6减去0.05e-6得到0.42e-6。这里我已经把它写成了固定数值但更好的写法是写成一个表达式比如si_center 0.36e-6; si_half_thickness 0.11e-6; slot_center si_center si_half_thickness - grating_depth / 2;这样grating_depth变化时凹槽仍然会从波导顶面向下长不会飘走。这正是脚本建结构最舒服的地方坐标不靠手算靠表达式。如果用手工建模你每次改刻蚀深度都得手动重算一大堆坐标。x的取值为-4e-6加(i-0.5)乘period是为了让8个齿在-4微米到0微米之间均匀分布。如果你想从0开始往正方向排把-4e-6改成0就行。循环变量i的类型是整数但和浮点数运算后结果是浮点数这一点Lumerical会自动处理。4.4 用结构组封装并配置扫描为了让齿统一管理可以先创建结构组再把每一个齿加进去addstructuregroup; set(name, grating_group); for(i 1:num_teeth) { addrect; set(name, tooth_ num2str(i)); # ... 省略尺寸和材料设置 addtogroup(grating_group); }这里有个顺序问题我特意说一下必须先addstructuregroup然后再循环里建齿、addtogroup顺序反了会报“组不存在”的错。addtogroup执行时组必须已经存在且未被选中否则分组逻辑会乱。如果你要做完整的参数扫描尤其是让所有齿同步变化我更推荐在结构组脚本变量方案里做在结构组的属性窗口中找到本地变量选项卡定义period、duty、grating_depth然后在结构组自带的脚本编辑器中写循环建齿的代码。这样sweep参数直接绑定变量名运行时一组几何就会按变量自动变化效果远比给8个齿分别加sweep参数干净。4.5 完整脚本注释版把上面的内容整合一下给一个可直接运行的完整版本switchtolayout; # 参数定义 period 0.7e-6; duty 0.5; grating_depth 0.1e-6; num_teeth 8; width 0.5e-6; si_center 0.36e-6; si_half_thickness 0.11e-6; # 衬底 addrect; set(name, substrate); set(x, 0); set(y, 0); set(z, 0); set(x span, 8e-6); set(y span, 8e-6); set(z span, 0.5e-6); set(material, SiO2 (Glass) - Palik); # 波导层 addrect; set(name, si_layer); set(x, 0); set(y, 0); set(z, si_center); set(x span, 8e-6); set(y span, width); set(z span, 0.22e-6); set(material, Si (Silicon) - Palik); # 光栅结构组 addstructuregroup; set(name, grating_group); for(i 1:num_teeth) { addrect; set(name, tooth_ num2str(i)); set(x, -4e-6 (i - 0.5) * period); set(y, 0); set(z, si_center si_half_thickness - grating_depth / 2); set(x span, period * duty); set(y span, 0.6e-6); set(z span, grating_depth); set(material, air); addtogroup(grating_group); }这个脚本运行后布局窗口的模型树应该出现substrate、si_layer和grating_group三个顶层对象grating_group下面挂着8个tooth。建议运行完先在GUI里转一下视角检查凹槽是否从波导顶面向下挖以及有没有嵌入到衬底里。几何不对后面仿真精度无从谈起。4.6 扫描运行和结果查看如果要做周期扫描选中grating_group通过结构组sweep添加period变量扫描。如果只是单独测试某个齿的宽度变化选中该齿后select(grating_group::tooth_1); addsweep; setsweep(name, sweep_tooth_width); setsweep(sweep_type, parameter); addsweepparameter(x span, 0.2e-6, 0.5e-6);这里要特别提醒一个坑用这种属性扫描方式每次只有一个齿会变其他齿保持不变这在多数光栅设计中并不是你想要的效果。如果希望所有齿同步变化还是那句老话用结构组变量方案。这个细节我见过不少人在论坛里问“为什么我只扫一个参数别的结构也变了”或者“为什么扫了以后只有其中一个齿变了”根因基本都是没搞清楚对象属性和组变量的区别。5. 脚本调试与常见报错排查5.1 单位混乱最常见的“形体消失”原因我见过的脚本建结构问题里至少有一半是单位问题。Lumerical脚本的默认长度单位是米但GUI界面里的长度信息通常显示成微米。于是很多新手在脚本里写set(x span, 0.5)本意是0.5微米实际创建了一个0.5米宽的巨型结构。小尺寸在有限视野里根本看不到大尺寸可能会把仿真区域撑爆或者导致网格数爆炸。正确的写法是set(x span, 0.5e-6);我建议所有脚本里的长度量一律用科学计数法不要贪图省事写0.5或者500e-9。越是复杂的结构单位混乱越难排查。还有一个习惯是在脚本开头写一个注释块标明所有基本单位比如“所有长度单位米”提醒自己下次打开脚本的人。5.2 set属性失败与属性名拼写set函数报错的常见原因有两个属性名拼写错误或者当前选中的对象不是你想操作的对象。Lumerical的属性名是区分大小写的比如X span会报错正确的是x span。如果你在脚本里写错了属性名脚本提示区会直接报出找不到该属性的错误这时候先检查拼写不要急着怀疑软件。另一个问题是select的对象不对。比如模型里有两个名字相近的对象select(tooth_1)和select(tooth_01)是不同对象。建议在set操作之前先用get确认select(si_layer); ? get(x span);?是Lumerical脚本里打印变量的快捷方式get(x span)返回当前选中对象的x span值。如果打印出来的值和你预期不一致说明select到的不是你以为的对象。5.3 多边形顶点矩阵的格式坑addpoly是创建任意形状结构最常用的原语但它的顶点格式确实坑过不少人。polygon vertices属性要求是一个二维矩阵每一行代表一个顶点的x、y坐标行与行之间用分号分隔addpoly; set(name, tri_prism); set(polygon vertices, [0, 0; 1e-6, 0.5e-6; -1e-6, 0.5e-6]);如果你把顶点写成一维数组比如[0, 0, 1e-6, 0.5e-6, -1e-6, 0.5e-6]脚本很可能不会直接报错但画出来的多边形形状完全不对。顶点矩阵里的坐标是相对于结构中心的偏移坐标不是绝对坐标这一点也和addrect的坐标系一样。我在创建锥形波导时经常用addpoly配合表达式把多个顶点坐标用变量算好再填进去这样改长度、改锥角都只需要改几个变量。5.4 用print和get做断点式排查Lumerical脚本本身没有图形化断点调试功能但可以靠print语句做分段排查。在怀疑的位置插入printprint(si_center); print(get(x span));运行后脚本提示区会输出当前变量值和对象属性值。如果你发现某个变量输出的是0或者空说明赋值可能有误如果某个get返回值和预期不符说明select对象有问题。还有一个技巧是把大脚本拆成几个小节分多次运行。比如先运行参数定义和衬底创建检查衬底位置对不对再运行波导层创建检查贴合关系最后运行光栅循环。每次都确认无误后再继续往下写。这样做的好处是问题容易定位不会出现一个几百行脚本运行完报错信息指向最后一行实际错误却发生在第50行的情况。5.5 运行环境与保存习惯创建结构之前先执行switchtolayout确保当前处于布局模式。如果当前停在FDTD求解器分析模式或者网格模式addrect等结构创建指令可能会报错或行为不正常。这个命令本身很轻量我习惯在每个结构脚本的第一行都写上。再一个就是保存习惯。运行脚本之前先保存当前的模型文件。因为有些脚本错误会导致几何丢失、对象名冲突、甚至模型树混乱。我吃过大亏一个跑得正好的脚本改了一个参数后运行直接报错结果模型文件里所有结构全消失了CtrlZ都救不回来。从那以后我养成两个习惯一是运行脚本前手动保存模型二是脚本文件本身也复制一份备份。每次改完一轮参数模型另存为一个新文件名脚本同样按日期命名。虽然麻烦一点但相比重写脚本几十行代码的成本这点麻烦完全值得。最后一个个人体会是脚本建结构这件事入门门槛其实不高真正难的是建立“参数驱动”的思维。刚开始你可能只是为了省点鼠标操作写着写着就会发现把结构拆成参数、把位置关系写成表达式能让你对设计的理解上升一个层次。而且一旦你手里的模型全是脚本可生成的后续做优化、做统计分析、做批量仿真都会顺很多。这篇文章里的代码和思路都是我之前项目里实践过的照着敲一遍再套到你自己的结构上很快就能上手。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进