
我们平时用 FDTD 仿真比如 Lumerical FDTD Solutions做光学设计绝大多数人上手都是从图形界面GUI拖拽结构开始的。鼠标点一点画个矩形、圆柱设个材料好像也挺方便。但一旦你开始做参数扫描、批量优化、模型复用或者需要几十上百个结构单元时纯 GUI 操作会让人崩溃——重复拖拽、重复设置、重复导出既慢又容易出错。这时候脚本就是唯一靠谱的出路。这篇教程我会聚焦在“用 FDTD 脚本创建结构”这件事上从最基础的命令语法到一个可以直接抄作业的纳米柱阵列建模案例再讲参数扫描和排错经验。内容面向已经能用 GUI 建简单模型、但想转向脚本自动化建模的人也适合刚接触 FDTD 脚本、被语法和单位折腾得头疼的新手。1. 为什么要用脚本创建结构——当 GUI 建模不够用的时候1.1 脚本的真正应用场景我先说说我个人的使用习惯。并不是说 GUI 建模不好而是脚本建模解决的痛点完全不一样。GUI 适合“一次性、简单结构”的验证比如你想快速看一个单层薄膜的透射率拖个矩形、设个光源就能跑了。但现实中稍微实际一点的需求GUI 都很难受。第一种典型场景是参数扫描。比如说你想研究纳米柱直径从 100 nm 到 200 nm、步长 5 nm 的情况下透射光谱会怎么变化。用 GUI 的话你需要手动改 21 次直径每次都要点开属性面板、输入数值、运行、记录结果。一次两次还能忍规模一大或者变量变成两三个直径、周期、高度手动操作就完全不可行了。而脚本里只需要一个for循环就能搞定跑完之后结果还能自动导出去洗杯咖啡回来数据就齐了。第二种场景是复杂周期性阵列。超表面、光子晶体、衍射光栅这类器件往往需要在模拟区内排布成百上千个单元结构。你不可能手动拖几百个六边形、圆柱到 FDTD 里就算你有这个耐心后面改一个参数也要全部重新来一遍。而脚本建模时一个嵌套循环就能轻松生成整个阵列而且修改参数只需要改一个变量名后边的循环和设置全部自动跟进。第三种场景是模型复用和交接。脚本本质上就是一个可复现的记录文件。半年前建的模型三个月前改的设置写得规不规范一看脚本就全都清楚了。你不需要去翻仿真文件里的哪个隐藏属性被改过也不需要担心发给别人之后对面看不懂你的边界条件和网格设置。一份写清楚注释的脚本就是一份最标准的建模说明书。我自己在团队里做项目时能明确感受到脚本交付比.fsp文件交付靠谱得多——接收方哪怕没有 GUI 操作基础也能照着脚本理解你的建模逻辑。1.2 脚本与 GUI 建模的边界与取舍有些人会有种误解觉得用了脚本就等于放弃了图形界面。其实不是。Lumerical 的脚本模型和 GUI 模型是同源的脚本里创建的每个对象都会实时出现在布局编辑器和结构树中。反过来你在 GUI 里手动建好的模型也能通过保存脚本或者导出命令把建模过程转成脚本。两边是联动的关键看你在这个环节用哪个更顺手。我的一个习惯是“GUI 看结构脚本管流程”。建模型的过程中我通常会穿插使用先用脚本批量生成主体结构然后用 GUI 手工微调个别材料参数或者检查几何是否重合再回到脚本里设置扫描和优化流程。这样做不仅比纯 GUI 快也比纯脚本试错更直观。毕竟脚本创建完结构之后立刻可以通过select命令选中对象再在布局编辑器里看它的具体位置和形状完全不耽误可视化。另外要说清楚一个认知FDTD 脚本不是一种需要先系统学完“编程”才能上手的语言它本质上和 Python 有点像但比 Python 简单得多。你只需要掌握创建对象的命令、设置属性的语法、循环和条件判断的基础写法再加上几个常用的文件操作函数就可以覆盖 90% 的日常建模脚本需求。真的不用怕我最早开始学的时候也就花了半天时间能用的核心命令二三十个封顶了。还有一个我踩过的坑别一上来就追求“全脚本化”。非要让一个不规则形状的 3D 模型完全用代码生成那是跟自己过不去。Lumerical 脚本擅长的是参数化规则结构比如矩形、圆柱、球、圆环、多边形以及这些基本原语的组合和阵列。真正不规则的复杂模型通常的做法是用 CAD 或者建模软件导入.step、.stl文件这个在脚本里一条import命令就能搞定。两者结合起来用效率和精度都能保证。2. 从零开始脚本创建结构的基础语法与常用命令2.1 结构原语的创建命令Lumerical FDTD Solution 的脚本官方叫 Lumerical Script简写有时候也叫 LSF建结构使用的一组命令核心就是“创建对象”和“设置属性”。创建原语的命令很直白addrect加矩形、addcircle加圆、addsphere加球、addcylinder加圆柱、addpoly加多边形、addring加圆环以及addstructuregroup加结构组。这些命令执行之后会在结构树里新增一个对象紧接着就要用set来设置它的属性。例如你创建一个硅材料的矩形柱子addrect; set(name, Si_pillar); set(x, 0); set(y, 0); set(z, 0); set(x span, 0.2); set(y span, 0.2); set(z span, 0.5); set(material, Si (Silicon) - Palik);这段脚本执行后一个中心位于原点、横截面 0.2 μm × 0.2 μm、高度 0.5 μm 的硅矩形柱就出现了。这里我要重点提醒一个新手最容易踩的坑——Lumerical 的默认单位是微米μm。如果你脑子里想的是纳米直接填 100那建出来的结构就是 100 μm等于在你看不见的尺度上放了个庞然大物。所以写脚本之前一定要先明确单位或者干脆通过set(x span, 100e-3)这么写用科学计数法强行转成微米。2.2 属性设置与单位陷阱set命令的通用格式是set(属性名, 值)属性名必须是对象属性面板里存在的标准名称。常用的几何属性包括位置x、y、z、尺寸x span、y span、z span材料属性就是material名字和 GUI 材料库里显示的名字保持一致就行。还有index这类渲染优先级参数之前在多层结构里需要指定哪个物体盖在哪个物体上面时就要手动改。另外还有一个很实用但容易被忽略的命令是set(override color, 1)和set(color, 某个RGB值)方便你区分同一模型里的不同部件。虽然不设置也不影响仿真结果但脚本建模的后期结构数量一多不同结构用不同颜色显示对排查几何重叠问题特别有帮助。单位陷阱我再展开说。Lumerical 里位置x、y、z和尺寸x span等的单位是微米但有些属性用的不是几何单位比如角度用的是度degree材料折射率是纯数值薄膜层层的厚度单位反而要根据介质材料属性而定。我见过有人写光栅周期时填了 0.7其实他想表达 700 nm结果仿真出来的衍射波长完全不符合物理预期。排查了半天最后发现是单位少了一个数量级。所以每次运行脚本前我都会在脚本开头写一个clear;紧接着输出一行所有关键尺寸的微米值确认数字没问题再继续。2.3 用 for 循环批量创建阵列结构脚本建模最爽的地方就是可以用循环批量生成结构。我拿二维光栅举例。假设要在 5 μm × 5 μm 的区域里做一个周期为 0.7 μm 的正方形柱阵列直径 0.3 μm、高度 0.6 μm。手写几十个addcylinder显然不现实用循环写就非常简洁# 周期和尺寸 period 0.7; diameter 0.3; height 0.6; Nx 7; # x 方向柱子数量 Ny 7; # y 方向柱子数量 for (i -floor(Nx/2); i floor(Nx/2); i) { for (j -floor(Ny/2); j floor(Ny/2); j) { addcylinder; set(name, pillar_ num2str(i) _ num2str(j)); set(x, i * period); set(y, j * period); set(z, 0); set(radius, diameter / 2); set(z span, height); set(material, Si (Silicon) - Palik); set(override color, 1); set(color, [1, 0.5, 0]); # 橙色显示 } }这段代码里我用了floor、嵌套循环、字符串拼接num2str这些都是 Lumerical 脚本自带的基础功能和 Python 的语法风格非常像。跑完之后结构树里会出现 49 个圆柱布局编辑器里可以看到整个阵列。这里还有个小细节set(name, ...)时尽量把循环索引加到名字里后面做参数扫描、选择性修改某个柱子或者单独选中检查时会方便得多。还有一个和循环配合很高的命令是select。循环里创建完对象之后如果要给某些局部结构单独设置属性可以用select(pillar_1_-1)精确选中它再set属性。这种“按名字操作”的方式比用鼠标在结构树里点来点去快得多尤其当结构数量多到结构树都卡的时候命令行反而更流畅。3. 实战案例用脚本搭建纳米柱阵列模型3.1 建模目标与参数设定这一节我会完整演示一个纳米柱阵列建模案例这个案例几乎涵盖了脚本创建结构的所有核心操作。目标在 FDTD 模拟区内创建一个硅纳米柱阵列中心在原点结构基底是二氧化硅衬底上方是周期排列的硅柱。我们需要扫描硅柱高度对透射率的影响高度从 300 nm 到 500 nm步长 20 nm。先明确基本参数模拟区尺寸x 方向 1.4 μmy 方向 1.4 μmz 方向 3 μm含 PML 边界阵列周期0.7 μm柱子半径0.15 μm直径 0.3 μm柱子材料Si (Silicon) - Palik衬底SiO2 (Glass) - Palik厚度 1 μm光源平面波从上方垂直入射波长范围 400 nm 到 800 nm这里要特别提一下模拟区边界条件的设置。对于周期性结构x 和 y 方向应该用周期性边界条件periodicz 方向用 PML。这样模拟区只需要包含一个周期就可以等效无限阵列极大地节省计算资源。脚本里设置边界条件的方式是set(x bc, periodic);等命令我们后面会用到。3.2 完整脚本解析这个案例完整脚本如下每一段我都加了注释说明意图# 纳米柱阵列建模脚本 clear; switchtolayout; # ---------- 基本参数 ---------- period 0.7; # 周期单位 μm radius 0.15; # 圆柱半径单位 μm h_si 0.4; # 硅柱高度单位 μm (初始值) h_glass 1.0; # 衬底厚度单位 μm z_glass_top -0.5; # 衬底上表面 z 坐标 z_si_bottom z_glass_top; # 硅柱底部贴着衬底 # ---------- 创建衬底 ---------- addrect; set(name, SiO2_substrate); set(x, 0); set(y, 0); set(z, z_glass_top - h_glass / 2); set(x span, 5); # 衬底在 x、y 方向做大一些模拟无限大衬底 set(y span, 5); set(z span, h_glass); set(material, SiO2 (Glass) - Palik); # ---------- 创建硅柱阵列 ---------- N 3; # 单方向柱子数量3×3 阵列 for (i -floor(N/2); i floor(N/2); i) { for (j -floor(N/2); j floor(N/2); j) { addcylinder; set(name, Si_pillar_${i}_${j}); # 注意这里的字符串模板写法 set(x, i * period); set(y, j * period); set(z, z_si_bottom h_si / 2); set(radius, radius); set(z span, h_si); set(material, Si (Silicon) - Palik); set(override color, 1); set(color, [0.8, 0.4, 0.2]); } } # ---------- 设置模拟区域 ---------- select(FDTD); set(x span, 1.4); set(y span, 1.4); set(z span, 3); set(x bc, periodic); set(y bc, periodic); set(z bc, PML); set(z min, -1.5); set(z max, 1.5);这里面有几个地方值得详细解释。首先是switchtolayout;这个命令会把 FDTD 切换到布局设计模式layout mode而不是仿真计算模式。建模阶段一定要先执行这个命令否则你后续添加的结构可能不会正确显示在布局环境中。这个命令我几乎每个建模脚本都会写算是个基础习惯。其次是set(name, Si_pillar_${i}_${j})这种写法它利用的是 Lumerical 脚本对字符串模板的支持比Si_pillar_ num2str(i) _ num2str(j)更简洁易读。需要注意的是模板里的变量会自动转换类型但有些很老版本的 Lumerical 可能不完全支持${}写法如果不支持就退回用num2str拼接。第三是模拟区边界的设置。set(z min, -1.5)和set(z max, 1.5)定义了 FDTD 模拟区在 z 方向的范围配合z span的数值这三个需要保持一致。否则你设置了z span是 3但z min和z max的差不是 3Lumerical 会以z span优先还是z min/z max优先不同版本行为不太一样容易造成边界条件设置和预期不符。我写脚本时习惯于显式设置z min和z max避免歧义。3.3 结构组 dry-run 与 GUI 联动建完结构之后千万别急着直接跑仿真。Lumerical 脚本里常用的一个技巧是“先 dry-run”——也就是只建模、不运行仿真在布局编辑器里目视检查所有几何结构的位置是否正确。我通常是这么做的脚本跑完之后打开布局编辑器从正上方xy 平面和侧方向xz 平面各看一眼确认柱子间隔均匀、和衬底贴合、没有明显重叠。这里就要用到overlap检查了。Lumerical 脚本里可以逐对选中物体然后点击右键的“check overlap”功能也可以在命令行用相关函数检查结构重叠。但我个人更倾向于用目视检查加上一个“渲染顺序”的技巧——多个物体在同一位置时后创建的物体会遮挡先创建的。如果你想看衬底和柱子的交界面可以把衬底的透明度调高。设置透明度的方法是在结构属性面板里找“render type”或者通过脚本set(alpha, 0.5)调整。另外一个重要的联动操作当你确认模型正确后可以执行save(nanopillar_array.fsp);把当前布局保存为仿真文件。以后打开这个文件所有用脚本创建的结构都还在而且你可以继续用脚本修改、运行、提取数据。再配合 GDS 导入或者.stl扩展Lumerical 可以和其他 EDA/CAD 工具无缝衔接。还有一个小技巧如果你需要把脚本创建的几何导出到其他软件查看可以选中结构后用菜单栏的 File - Export - Geometry 导出为.stl或者.step格式。这些文件可以直接导入到 SolidWorks、COMSOL 或者自家画图工具里做进一步处理。在高频电磁仿真和光学设计中几何文件交换是很常见的需求。4. 进阶技巧用脚本驱动参数扫描与优化4.1 参数关键字的定义与使用结构建好了下一步就是让脚本帮我们批量跑仿真分析不同结构参数下的光学响应。Lumerical 脚本提供了强大的参数扫描功能sweep你不需要自己写循环去一次次修改结构并运行——虽然那样也是可行的——更规范的做法是定义参数关键字parameter和扫描任务。参数关键字本质上是一个模型里的全局变量脚本、结构属性和扫描任务都可以引用它。例如我们想扫描硅柱高度可以这样定义addglobalsweep; setsweep(sweep_1, sweep type, parameter); addglobalsweepparameter(sweep_1, h_si_scan, 0.3, 0.5, 11);这里addglobalsweep创建了一个参数扫描任务addglobalsweepparameter定义了扫描的变量h_si_scan从 0.3 到 0.5、均匀取 11 个点。然后你还需要告诉 Lumerical在扫描过程中要用这个变量值来更新结构中“z span”属性。这一步需要在set(z span, h_si_scan)里引用同名的参数关键字。实际脚本中更稳妥的流程是先把关键几何属性指向一个中间变量再用这个变量去设置属性。比如h_si h_si_scan; # 将扫描参数赋值给几何变量 select(Si_pillar_0_0); set(z span, h_si);addglobalsweepparameter中的数值会自动按等差序列生成不需要你手动构造循环。扫描任务建好之后在 GUI 的优化和扫描窗口里就能看到这个任务直接运行即可。4.2 自动批处理与结果导出参数扫描跑完之后我们通常需要把每个参数下的结果汇总分析。Lumerical 脚本有专门的数据提取和保存命令。比如我们只需要透射率可以这样写select(T); # 假设监视器名为 T T getresult(T, T); transmission T.T; # 保存数据到文件 write(transmission_vs_height.txt, transmission);但这只是在一次仿真结束后的数据导出。配合扫描任务后我们可以把数据保存放到每次仿真结束时自动触发的地方比如在“sweep”设置里的“post processing”回调函数中写入文件。我这里建议一个更直觉的做法在扫描之前写好一个用于保存多个结果数据的初始化文件然后每次扫描循环里用matlabsave或者write导出文件名带上当前参数值例如write(T_data_h_ num2str(h_si_scan*1000) nm.txt, transmission);批量跑完所有仿真之后你就能得到一个带参数的文档集合后续用任何数据处理软件Python、MATLAB、Origin导入即可。相比手动记录或者跑完一次截一次图这种方式数据可追溯、格式统一做论文或者工程报告时写“数据可用性声明”都要方便得多。4.3 效率优化心得脚本自动化建模很长时间之后我才真正意识到“效率优化”的本质不是一味缩短单次 FDTD 仿真时间而是减少人机交互的开销。很多新手会花大把时间手动监视器调位置、等每次仿真结束再去看结果图结果一天下来没跑几个点。而脚本化之后人只需要在开始前把参数范围和输出路径定义好剩下交给计算机跑自己的时间可以拿去做物理分析或者准备下一轮的模型。还有一个很实在的小技巧在跑大批量扫描之前先用一个粗网格、较小的模拟区做快速试跑验证脚本和数据提取代码没有 bug。等确认逻辑没问题了再恢复高精度网格和完整模拟区正式跑。这样一次就能省下好几个小时的无效等待。这个“小规模试跑”的脚本完全可以复用只要加一个mesh_accuracy和模拟区大小的开关变量就行。比如在我的工作流里脚本开头常会有这种结构quick_test 1; # 1 表示快速验证0 表示正式仿真 if (quick_test) { set(x span, 0.7); set(y span, 0.7); set(mesh accuracy, 2); } else { set(x span, 1.4); set(y span, 1.4); set(mesh accuracy, 4); }这样同样的脚本既能快速验证又能正式跑省去维护两套脚本的麻烦。5. 常见问题与排查技巧实录5.1 脚本报错排查速查表脚本报错是家常便饭关键是快速定位。我把这些年遇到过的高频报错整理成一个速查表方便大家对照。报错现象常见原因排查与解决办法set设置无效属性名属性名拼写错误或对象不支持该属性用get查看当前对象属性列表核对属性名代码块未闭合少写}或括号使用编辑器缩进和对括号功能逐层检查创建对象后没出现在结构树忘了switchtolayout或没刷新在脚本开头加switchtolayout;单位错误导致结构尺寸异常默认微米被误填为纳米用?查看对象位置和尺寸确认数值量级材料名不对提示找不到版本不一致或材料路径改了查看材料库中的标准名称如Si (Silicon) - Palik循环索引异常floor、ceil使用错误打印索引值确认是整数必要时num2str转字符串数据写不出来路径不存在或文件名称含非法字符先cd切换工作路径文件名不用特殊字符监视器读取不到结果监视器没仿真完或名字不对确认模拟完成后才读取检查监视器名一致这张表里我发现最坑的是“属性名拼写错误”因为它不一定报错只是设置了等于没设置。比如你想设置x span结果输入成了xspan少了空格Lumerical 可能不会直接报错但也不会生效。这时候你选中对象看属性面板会惊讶地发现尺寸没变。所以我在脚本里凡是设置完关键属性都会加一句查询select(Si_pillar_0_0); ?get(x span);?命令可以在脚本控制台直接输出表达式的结果多写几行就能精确定位是哪一步的问题。5.2 单位、精度与几何重叠问题单位问题我在前面提过这里再补充一个和网格相关的精度问题。FDTD 仿真是基于网格的结构边缘会被网格阶梯化处理staircasing。如果你的结构尺寸和网格尺寸不匹配仿真结果会有不小的误差。脚本建模时可以通过设置网格精度和添加网格覆盖区域mesh override region来控制局部网格大小。比如对纳米柱阵列通常会在结构所在区域加一个细网格覆盖网格步长设置为最小特征尺寸的 1/10 到 1/20。几何重叠的问题则是另一个经常被忽视的坑。脚本创建结构时如果你不小心让两个不同的结构在空间上重叠FDTD 会默认后创建的对象优先显示但实际计算时两个物体都会参与材料平均或折射率混合取决于网格材料算法。这可能导致材料的等效折射率出乎意料。检查几何重叠可以这样做# 选中两个物体检查重叠 select(Si_pillar_0_0); select(SiO2_substrate); ?overlap(Si_pillar_0_0, SiO2_substrate);不过 Lumerical 里的重叠检查更多依赖 GUI 中的“check overlap”按钮脚本命令不一定在所有版本都支持。我个人常用的方式是目视检查加上显式控制 z 坐标。比如在本案例中我设置硅柱底部z_si_bottom z_glass_top就让接触面严格重合而不是交叉重叠这样既避免了材料混合带来的意外也让模型更符合真实加工情况。5.3 性能和稳定性问题最后聊一下脚本跑大批量仿真时的稳定性和效率问题。最常见的一个坑是内存泄漏或者长时间跑批导致软件卡死。我的做法是每跑完一个仿真点主动清理不需要的数据clear; # 清理内存中的临时数据但要注意clear也会把结构体定义和对象清掉所以如果有需要保留的对象设置可以用select先保存到变量或者直接不要频繁clear。更稳的做法的脚本里定期重启 FDTD。不过这通常涉及到外部调度一般用户不需要做得这么极端。还有一个大坑是循环跑仿真时不要用run;而是用run;前面的保存步骤。我见过有人每跑一次扫描点就手动点一下 run结果一个下午就在点鼠标中过去了。正确做法是脚本里循环执行run;然后立刻读结果读完后自动进入下一个参数点全程不需要人工介入。我在大量实践中发现如果中途需要人工干预99% 是脚本没有写好——不是边界条件没设对就是结果提取部分有 bug。稳定性方面一个很好的习惯是“每步写日志”。我的脚本末尾通常会加几行logfile scan_log.txt; write(logfile, height num2str(h_si_scan) completed at time());哪怕是简单的字符记录也能帮你在几百个仿真点里快速定位哪一步出了问题。不要小看这个习惯我至少两次靠这种日志文件在两三千行的批量仿真里准确找到了异常点的参数值省去了无数重新仿真的时间。最后再分享一个小技巧在我做 FDTD 建模的这些年里脚本带给我的帮助远不止是“省时间”。它真正的价值是让建模过程变得可重复、可共享、可追溯。以前我还在 GUI 里手动添加长方体的时候总担心某个参数忘了改导致整轮结果作废。自从切换到“参数变量 循环 扫描任务”的模式之后这种担心的比例降低了很多因为所有参数都在脚本顶部一目了然改一个变量名整个模型和所有输出数据都会跟着动想错都难。如果你刚开始接触 FDTD 脚本我建议你从今天这个案例入手先跑通一个最简单的参数扫描感受一下“一键改参、自动出图”的流程。上手之后你会觉得以前在 GUI 里一个一个拖结构的做法真的就像是回到了手工时代。