
1. 为什么平面实体值得单独拎出来讲做FDTD仿真的人绕不开结构搭建这一步。不管你是做光栅耦合器、微环谐振腔、还是超表面透镜最终都要落到“在仿真区域里放一个什么东西”这个最朴素的问题上。Lumerical的FDTD Solutions提供了图形界面画结构的方式但真正做过几个项目之后你会发现纯靠鼠标拖拽建出来的模型改一个参数就得重新画一遍效率低得让人抓狂。这时候脚本语言就成了分水岭——会用脚本的人改一个变量就能批量扫描几十组参数不会的人只能一遍遍手动调。平面实体Planar solid是Lumerical脚本里最基础也最常用的结构类型之一。所谓“平面实体”你可以把它理解成一块有厚度、有形状的平板——它在某一个方向上延伸出厚度另外两个方向由你定义的几何轮廓决定。听起来简单但实际用起来从坐标系的选取、厚度的方向、到材料属性的赋值每一步都有坑。我见过不少新手结构画出来了仿真跑起来结果不对排查半天发现是厚度方向搞反了或者材料覆盖错了区域。这篇内容适合两类人一类是刚接触Lumerical脚本、想从“会点鼠标”过渡到“会写脚本”的FDTD使用者另一类是用了一段时间脚本但对Planar solid的理解还停留在“能用就行”、想搞清楚底层逻辑和最佳实践的人。我会从脚本语言的基本结构讲起把平面实体的搭建过程拆开揉碎配上可直接复现的代码和参数计算过程最后分享一些我实际踩过的坑和排查技巧。2. Lumerical脚本语言与结构搭建的整体思路2.1 脚本语言在FDTD工作流中的位置Lumerical的脚本语言本质上是一种面向仿真对象的命令式语言它的语法风格接近Lua但又不完全是Lua。你在FDTD的脚本编辑器里写的每一行最终都会被解释成对仿真对象属性的操作。比如addfdtd是添加FDTD仿真区域addrect是添加矩形结构set是设置属性setnamed是按名称设置属性。这些命令组合起来就构成了整个仿真模型的搭建过程。为什么强调“面向仿真对象”因为Lumerical的脚本不是通用编程语言它没有复杂的控制流和数据结构虽然支持循环和数组它的核心价值在于把你手动操作GUI的每一步用文本命令的形式固定下来。这样做的好处是显而易见的——可复用、可参数化、可版本控制。你写好的脚本换一台机器、换一个项目改几个参数就能继续用。从工作流的角度看一个完整的FDTD仿真脚本通常包含这几个阶段初始化清空旧模型、设置单位、定义参数波长、尺寸、材料等、搭建结构添加各种实体、设置仿真区域和边界条件、添加光源和监视器、运行仿真、提取和分析结果。平面实体的搭建属于第三个阶段但它承上启下——上面的参数定义决定了它的尺寸和位置下面的仿真设置又依赖于它的存在。2.2 平面实体的几何逻辑与坐标系约定在讲具体命令之前必须先搞清楚Lumerical的坐标系约定。FDTD Solutions默认使用右手坐标系X、Y、Z三个轴的方向遵循右手定则。对于平面实体来说最关键的是“厚度方向”的确定。Lumerical里的平面实体比如addplanarsolid允许你指定一个平面和厚度平面由法线方向和平面上的一个点确定厚度则沿着法线方向延伸。这里有一个容易混淆的地方很多人以为“平面实体”就是二维的其实不是。它是有厚度的三维物体只是它的几何形状由一个二维轮廓加上一个厚度方向上的拉伸来定义。你可以把它想象成一块用模具压出来的饼干——饼干的形状由模具决定二维轮廓饼干的厚度由面团的厚度决定拉伸方向。Lumerical提供了几种创建平面实体的方式。最直接的是addplanarsolid命令它接受一个结构名称和一组属性参数。另一种方式是用addrect或addcircle等命令创建二维形状然后通过设置Z方向的跨度来赋予厚度。两种方式各有适用场景addplanarsolid更适合需要精确控制法线方向和平面位置的场合而addrect加Z跨度更适合简单的矩形平板。2.3 为什么选择脚本而非GUI这个问题我被问过很多次。GUI画结构直观所见即所得为什么还要费劲写脚本答案在于“规模”和“重复性”。当你只需要画一个简单的波导时GUI确实快。但当你需要画一个由几十个周期组成的光栅、或者需要扫描上百组几何参数时GUI的效率就崩溃了。脚本的优势在于一次编写无限次运行参数化定义批量扫描版本可控团队协作方便。还有一个容易被忽视的好处脚本强迫你思考结构的数学定义。用GUI画图时你可能凭感觉拖拽尺寸差不多就行。但写脚本时你必须明确写出每个坐标、每个尺寸的数值这迫使你回到设计本身想清楚每个几何参数的意义。这种“被迫的严谨”长期来看对仿真能力的提升是巨大的。3. 平面实体搭建的核心命令与参数详解3.1 addplanarsolid命令的完整语法addplanarsolid是Lumerical脚本中创建平面实体的核心命令。它的基本语法是addplanarsolid; set(name, 结构名称); set(x, x坐标); set(y, y坐标); set(z, z坐标); set(nx, 法线x分量); set(ny, 法线y分量); set(nz, 法线z分量); set(thickness, 厚度); set(material, 材料名称);这里有几个关键点需要展开。首先addplanarsolid本身不带参数它只是创建一个空的平面实体对象后续用set命令来配置属性。这种“先创建、后配置”的模式在Lumerical脚本中很常见好处是你可以分步骤设置也方便调试。其次x、y、z定义的是平面上的一个参考点而不是结构的中心。这个参考点的具体含义取决于你后续如何定义轮廓。对于最简单的矩形平面实体这个点通常取矩形的中心。但对于复杂轮廓它可能只是平面上的任意一点。法线向量(nx, ny, nz)决定了平面的朝向。如果你想要一个水平放置的平板厚度沿Z方向法线就是(0, 0, 1)。如果你想要一个垂直放置的平板厚度沿X方向法线就是(1, 0, 0)。法线向量不需要归一化Lumerical会自动处理但为了清晰起见建议使用单位向量。厚度thickness是沿着法线方向从参考点向两侧延伸的总厚度。注意是“向两侧延伸”也就是说参考点位于厚度的中间位置。如果你设置厚度为0.2微米法线为(0, 0, 1)参考点Z坐标为0那么结构会从Z-0.1延伸到Z0.1。这一点和很多人的直觉不同——他们以为参考点是底面或者顶面。我当初就因为这个搞错过结构位置偏了半个厚度仿真结果怎么都对不上。3.2 轮廓定义从矩形到任意多边形平面实体的“平面形状”由轮廓决定。Lumerical提供了几种定义轮廓的方式第一种是使用内置的简单形状。比如你可以在创建平面实体后用set(shape, rectangle)来指定矩形轮廓然后用set(x span, ...)和set(y span, ...)来定义矩形的长和宽。这种方式最简单适合规则形状。第二种是使用顶点列表定义任意多边形。命令是set(vertices, ...)后面跟一个N×2的矩阵每一行是一个顶点的(x, y)坐标。这种方式灵活度最高可以定义任何凸多边形或凹多边形。但要注意顶点必须按顺序排列顺时针或逆时针不能交叉否则Lumerical会报错或者生成错误的形状。第三种是使用数学表达式定义曲面。这种方式用得相对少一些适合需要精确控制轮廓曲线的场合。比如你可以用set(shape, equation)然后定义一个函数来描述边界。对于大多数应用场景矩形轮廓已经够用了。光栅的每个周期可以是一个矩形平板波导的芯层可以是一个长条形矩形微环的耦合区域也可以用矩形近似。真正需要复杂多边形的时候往往是做超表面或者光子晶体的时候每个单元的形状不是简单的矩形。3.3 材料属性的赋值与优先级材料赋值是平面实体搭建中另一个容易出问题的地方。Lumerical的材料系统分为几个层级内置材料库如Si、SiO2、Au等、用户自定义材料、以及直接指定折射率数值。当你用set(material, Si)时Lumerical会去材料库里查找名为“Si”的材料。如果找不到它会报错。这里有一个实用技巧如果你只是做快速验证不想花时间定义材料模型可以直接用set(index, 3.5)来指定折射率。这种方式简单粗暴但只适合色散不明显的场合。对于需要精确色散建模的场景还是老老实实用材料库或者自定义材料模型。材料赋值的优先级也需要注意。如果你先设置了material然后又设置了index后者会覆盖前者。反过来如果你先设置了index再设置material材料库的定义会覆盖折射率数值。所以设置顺序很重要建议在脚本里把材料相关的设置集中放在一起避免混乱。还有一个坑当多个结构重叠时Lumerical默认使用“后添加的结构覆盖先添加的结构”的规则。也就是说如果你先画了一个大平板又在上面画了一个小方块小方块区域会使用小方块的材料其余区域使用大平板的材料。这个规则在搭建多层结构时非常有用但如果不注意添加顺序可能会得到意想不到的结果。4. 从零搭建一个平面实体完整实操流程4.1 环境准备与脚本框架在开始写具体结构之前先建立一个干净的脚本框架。我习惯在脚本开头做这几件事清空当前模型、设置单位、定义全局参数。清空模型用newproject或者switchtolayout加deleteall前者会重置所有设置后者只删除结构。对于大多数情况我推荐用newproject确保从一个完全干净的状态开始。单位设置用set(units, micron)把长度单位统一为微米。FDTD仿真中微米是最常用的单位波长、尺寸都在这个量级。统一单位可以避免很多换算错误。全局参数的定义是脚本可读性的关键。我习惯把所有的几何参数、材料参数、仿真参数都放在脚本开头用变量名代替具体数值。比如-- 全局参数定义 lambda0 1.55; -- 中心波长单位微米 wg_width 0.5; -- 波导宽度 wg_height 0.22; -- 波导厚度 sub_thickness 2.0; -- 衬底厚度 si_index 3.48; -- 硅的折射率近似值 sio2_index 1.44; -- 二氧化硅的折射率这样做的好处是当你需要修改参数时只需要改一处整个脚本都会跟着变。而且变量名本身就是注释读脚本的人一眼就能看出每个数值的含义。4.2 创建衬底与波导的平面实体假设我们要搭建一个最简单的硅波导结构二氧化硅衬底上有一层硅波导。衬底是一个大平板波导是一个长条形平板。先创建衬底addplanarsolid; set(name, substrate); set(x, 0); set(y, 0); set(z, -sub_thickness/2); set(nx, 0); set(ny, 0); set(nz, 1); set(thickness, sub_thickness); set(material, SiO2 (Glass) - Palik); set(shape, rectangle); set(x span, 10); set(y span, 10);这里我把衬底的中心放在Z-sub_thickness/2的位置这样衬底的上表面正好在Z0处。法线是(0,0,1)厚度是2微米所以衬底从Z-2延伸到Z0。x span和y span都设为10微米足够覆盖大多数仿真区域。接下来创建波导addplanarsolid; set(name, waveguide); set(x, 0); set(y, 0); set(z, wg_height/2); set(nx, 0); set(ny, 0); set(nz, 1); set(thickness, wg_height); set(material, Si (Silicon) - Palik); set(shape, rectangle); set(x span, 10); set(y span, wg_width);波导的中心在Zwg_height/2厚度是0.22微米所以波导从Z0延伸到Z0.22。波导的下表面和衬底的上表面正好接触没有间隙也没有重叠。这种“刚好接触”的设置在仿真中很常见但要注意如果两个结构的表面完全重合Lumerical可能会在网格划分时产生一些数值上的模糊。更稳妥的做法是让它们稍微重叠一点点比如波导的下表面延伸到Z-0.01这样确保材料覆盖没有缝隙。4.3 参数计算厚度、位置与网格匹配平面实体的厚度和位置不是随便设的它们需要和仿真网格匹配。FDTD的网格是离散的如果结构的边界不在网格线上Lumerical会用“共形网格”技术来近似但这会引入一定的误差。为了减少误差一个实用的技巧是让结构的厚度是网格尺寸的整数倍。假设你设置的网格尺寸是0.02微米在硅波导中为了精确解析模式通常需要这么细的网格那么波导厚度0.22微米就是11个网格刚好是整数倍。如果你设置厚度为0.225微米那就不是整数倍Lumerical需要做共形网格处理虽然也能算但精度会打折扣。位置的计算也是同理。波导的中心Z坐标是wg_height/20.11微米如果网格在Z方向的起点是-2微米网格尺寸是0.02微米那么0.11除以0.02等于5.5不是整数。这意味着波导的中心不在网格线上。更好的做法是调整网格起点或者波导位置让关键界面落在网格线上。我通常的做法是先确定网格尺寸然后反推结构的尺寸和位置确保关键界面如波导上下表面都在网格线上。这个过程在脚本里可以用变量和表达式自动完成不需要手动计算。4.4 验证结构可视化与脚本检查结构搭建完成后不要急着跑仿真。先做两件事可视化检查和脚本检查。可视化检查用plot命令或者直接在GUI里看。Lumerical的脚本可以调用plot来显示结构但更直观的方式是运行脚本后在GUI的“Layout”视图里查看。检查的内容包括结构的位置是否正确、尺寸是否匹配、材料是否覆盖了正确的区域。脚本检查主要是看有没有语法错误和逻辑错误。Lumerical的脚本编辑器会高亮语法错误但逻辑错误需要你自己排查。一个常用的技巧是在关键步骤后加?命令来打印变量值比如?wg_height会输出波导厚度的当前值。这在你用表达式计算尺寸时特别有用可以确认计算结果是否符合预期。还有一个检查项结构的名称是否唯一。Lumerical允许同名结构存在但setnamed命令会操作第一个匹配的结构这可能导致意外的结果。我习惯给每个结构起一个描述性的名字比如“substrate”、“waveguide_core”、“cladding”等避免用“rect1”、“rect2”这种无意义的名字。5. 常见问题与排查技巧实录5.1 结构位置偏移法线与参考点的陷阱这是新手最容易踩的坑。很多人以为set(z, 0)就是把结构的底面放在Z0处实际上参考点位于厚度的中间。如果你想要底面在Z0参考点应该设在Zthickness/2。如果你想要顶面在Z0参考点应该设在Z-thickness/2。更隐蔽的问题是法线方向。如果你设置法线为(0,0,-1)厚度为0.2参考点在Z0那么结构会从Z-0.1延伸到Z0.1——和法线为(0,0,1)时一样。因为厚度是向两侧延伸的法线的正负只影响平面的“正面”朝向不影响厚度的延伸范围。但如果你后续用到了依赖法线方向的操作比如某些边界条件或者材料各向异性法线方向就很重要了。排查方法在GUI里查看结构时注意看Z方向的跨度。Lumerical会在属性栏里显示结构的实际范围。如果范围和预期不符检查参考点和厚度的设置。5.2 材料未生效名称匹配与覆盖顺序材料设置不生效通常有两个原因名称不匹配或者覆盖顺序不对。名称不匹配的情况很常见——你写了set(material, Si)但材料库里实际的名字是Si (Silicon) - Palik。Lumerical的材料库命名有固定的格式通常是“材料名 (全称) - 来源”。你可以用?getmaterial(Si)来查询材料是否存在或者用?getmaterial列出所有可用材料。覆盖顺序的问题更隐蔽。假设你先添加了一个大平板材料设为SiO2然后又添加了一个小方块材料设为Si。如果小方块和大平板有重叠区域重叠部分的材料取决于添加顺序——后添加的覆盖先添加的。如果你先添加小方块再添加大平板那么大平板会覆盖小方块重叠区域变成SiO2。这显然不是你想要的结果。排查方法用select命令选中结构然后用?get(material)查看当前材料。如果材料不对检查添加顺序和名称拼写。5.3 网格与结构不匹配导致的仿真异常结构尺寸和网格不匹配轻则精度下降重则仿真不收敛。我遇到过一次波导厚度设了0.22微米网格尺寸设了0.03微米0.22除以0.03等于7.33不是整数。仿真跑起来后模式分布看起来正常但传输率比预期低了10%。后来把网格尺寸改成0.02微米传输率就对了。原因是共形网格在处理非整数倍厚度时引入了额外的数值反射。排查方法在仿真设置里查看网格尺寸然后手动计算关键尺寸是否是网格尺寸的整数倍。如果不是调整网格尺寸或者结构尺寸。Lumerical也提供了“mesh override region”功能可以在关键区域强制使用更细的网格但这是另一个话题了。5.4 脚本调试的实用技巧Lumerical的脚本调试功能不算强大但有几个技巧很实用。第一用?命令打印变量值这是最直接的调试方式。第二用try...catch结构捕获错误避免脚本因为一个错误就完全停止。第三把脚本分成小段运行每段运行后检查结果而不是一次性跑完整个脚本。还有一个技巧在脚本开头设置set(script mode, true)这样脚本运行时不会弹出GUI窗口速度更快。调试完成后再关掉这个设置用GUI查看结果。6. 进阶技巧参数化扫描与结构复用6.1 用循环批量创建周期结构平面实体的真正威力在于批量创建。假设你要做一个由20个周期组成的光栅每个周期的宽度是0.5微米间距是0.2微米。用GUI画的话你得复制粘贴20次。用脚本的话一个循环就够了period 0.7; -- 周期 width 0.5; -- 光栅条宽度 N 20; -- 周期数 for i 1, N do addplanarsolid; set(name, grating_ num2str(i)); set(x, (i - (N1)/2) * period); set(y, 0); set(z, wg_height/2); set(nx, 0); set(ny, 0); set(nz, 1); set(thickness, wg_height); set(material, Si (Silicon) - Palik); set(shape, rectangle); set(x span, width); set(y span, 10); end这段代码的关键是(i - (N1)/2) * period这个表达式它让光栅条以原点为中心对称分布。num2str(i)把数字转换成字符串用于生成唯一的名称。这种模式在搭建超表面、光子晶体等周期结构时非常常用。6.2 将常用结构封装为函数如果你经常搭建类似的结构可以把它们封装成函数。Lumerical的脚本支持自定义函数语法是function add_waveguide(name, x, y, z, width, height, material) addplanarsolid; set(name, name); set(x, x); set(y, y); set(z, z); set(nx, 0); set(ny, 0); set(nz, 1); set(thickness, height); set(material, material); set(shape, rectangle); set(x span, 10); set(y span, width); end然后调用add_waveguide(wg1, 0, 0, 0.11, 0.5, 0.22, Si (Silicon) - Palik)就能快速创建一个波导。这种方式不仅减少了重复代码还让脚本的结构更清晰修改起来更方便。6.3 结构复用与参数继承在复杂的仿真中很多结构共享相同的参数。比如所有的硅结构都用同一种材料所有的波导都有相同的厚度。这时候可以用“参数继承”的方式把公共参数定义在全局然后在创建结构时引用这些全局变量。这样当你需要修改材料或者厚度时只需要改全局变量所有结构都会自动更新。更进一步你可以把结构定义写在一个单独的脚本文件里然后在主脚本中用include命令引入。这样不同的项目可以共享同一套结构定义避免重复劳动。7. 我个人的实操体会平面实体看起来简单但真正用好需要理解几个核心概念参考点的位置、法线的方向、厚度的延伸方式、材料覆盖的优先级。这些概念在GUI里是隐式的你拖拽鼠标时不需要想这些但写脚本时必须明确。我刚开始用脚本时经常因为参考点位置搞错导致结构偏移排查半天才发现是厚度延伸方向的问题。后来养成了一个习惯每创建一个结构就在脚本里加一行注释写明参考点的位置和厚度的范围。这样即使过了几个月回头看也能快速理解当时的意图。另一个体会是脚本的可读性比简洁性更重要。我见过一些脚本写得非常紧凑一行命令搞定好几个设置但过两周自己都看不懂了。相比之下把每个设置单独写一行加上注释虽然脚本长一点但维护起来轻松得多。特别是当你要把脚本交给别人或者过一段时间自己再看时可读性的价值就体现出来了。最后分享一个小技巧在脚本开头定义一个debug变量设为true时打印详细的调试信息设为false时静默运行。这样你可以在调试阶段打开详细输出正式运行时关掉既方便排查问题又不影响运行速度。这个习惯我从写第一行Lumerical脚本时就养成了到现在还在用。