1. 从脚本到光机为什么需要COM与ZOS-API如果你在光学设计领域工作尤其是使用Zemax OpticStudioZOS那么你大概率经历过这样的场景为了优化一个简单的单透镜你需要在软件界面里反复点击“优化”按钮手动调整曲率、厚度然后查看MTF、点列图再根据经验微调操作数权重。这个过程对于探索性设计或者一次性任务尚可接受但一旦涉及到参数扫描、批量分析、或者将光学设计流程嵌入到一个更大的自动化系统中比如与机械设计、热分析联动纯手动操作就显得力不从心效率低下且容易出错。这正是Python与ZOS-APIApplication Programming Interface结合的价值所在。ZOS-API是Zemax OpticStudio提供的一套编程接口它允许外部程序如Python脚本以编程方式控制OpticStudio实现几乎所有的软件功能。而COMComponent Object Model是微软开发的一种跨应用通信技术ZOS-API正是通过COM接口暴露其功能。简单来说你可以把OpticStudio想象成一个功能强大的“服务器”你的Python脚本就是“客户端”通过COM这条“数据线”客户端可以发送指令让服务器执行任何操作并获取结果。使用Python驱动ZOS-API意味着你可以将光学设计流程代码化、自动化。对于构建和优化一个单透镜你可以编写脚本来自动完成创建新镜头文件、设置波长和视场、添加表面并赋予初始结构、定义变量、设置评价函数Merit Function、运行优化、分析结果并输出报告。整个过程无需人工干预脚本可以7x24小时运行尝试成千上万种初始条件组合找到全局最优解或者集成到CI/CD流程中确保每次设计迭代的可重复性。2. 环境搭建与连接初始化迈出第一步在开始编写构建单透镜的脚本之前我们需要先搭建好Python与ZOS-API通信的桥梁。这个过程虽然基础但有几个关键点直接决定了后续脚本的稳定性和可移植性。2.1 核心依赖与版本匹配首先你需要确保安装了正确版本的Python和Zemax OpticStudio。ZOS-API对版本有严格要求通常建议使用Python 3.7至3.9的64位版本这与当前主流OpticStudio版本如20.3的兼容性最好。你可以通过命令行输入python --version来确认。Python与ZOS-API交互的核心库是win32com它提供了在Python中调用COM组件的能力。这个库通常通过pywin32包来安装。最稳妥的安装方式是使用pippip install pywin32安装完成后一个常见的验证方法是尝试导入win32com.client如果没有报错说明环境基本就绪。注意有时在复杂的Python环境如Anaconda中可能会遇到路径或权限问题。如果导入失败可以尝试以管理员身份运行命令行进行安装或者检查环境变量。2.2 建立与Zemax的连接连接Zemax OpticStudio有两种基本模式独立模式Standalone和交互模式Interactive。对于自动化构建和优化任务我们强烈推荐使用独立模式。独立模式脚本会启动一个“看不见”的Zemax OpticStudio后台进程。这个进程没有图形界面完全由你的脚本控制。它的优点是运行速度快、资源占用相对较少且不会干扰你前台可能正在进行的其他手动设计工作。脚本结束后该进程会自动关闭。交互模式脚本会连接到你已经打开的一个Zemax OpticStudio桌面程序实例。你可以实时看到脚本对镜头文件所做的修改。这常用于调试或者需要人工介入观察的复杂场景。但对于自动化流程界面刷新会带来不必要的性能开销且容易因人为操作导致状态不一致。以下是建立独立模式连接的经典代码段import win32com.client def create_standalone_connection(): 创建与ZOS-API的独立模式连接。 返回连接对象和TheApplication实例。 # 生成ZOS-API连接器 connection win32com.client.Dispatch(ZOSAPI.ZOSAPI_Connection) if connection is None: raise RuntimeError(无法创建ZOSAPI连接。请检查Zemax OpticStudio是否正确安装。) # 创建独立应用实例 app connection.CreateNewApplication() if app is None: raise RuntimeError(无法创建ZOSAPI应用实例。) # 获取顶层的API接口 zos_api app.AsIZOSAPI # 获取TheApplication对象它是大多数操作的起点 the_application zos_api.TheApplication the_application.NewApplication() # 确保是一个新的应用会话 # 获取透镜数据编辑器LDE的接口 lde the_application.PrimarySystem.LDE return app, the_application, lde # 使用示例 try: app, the_app, lde create_standalone_connection() print(成功连接到Zemax OpticStudio独立实例。) except Exception as e: print(f连接失败: {e})这段代码是几乎所有ZOS-API脚本的起点。win32com.client.Dispatch是关键它通过COM系统定位并激活ZOSAPI库。CreateNewApplication()方法触发了独立进程的启动。获取到的the_application对象是你的“总控台”通过它可以访问当前光学系统、文件、工具等一切资源。lde对象则是对透镜数据编辑器的直接引用后续添加表面、设置参数都要通过它。实操心得务必在脚本的最后加入资源清理的代码即调用app.CloseApplication()。否则独立模式下的Zemax后台进程可能会残留积累多了会占用大量内存。一个好的实践是使用Python的上下文管理器with语句或try...finally块来确保连接被正确关闭。3. 单透镜的自动化构建从零到初始结构有了连接之后我们就可以开始“无中生有”用代码构建一个单透镜。我们假设要构建一个经典的平凸单透镜材料为N-BK7用于可见光波段焦距约100mm。3.1 创建系统与设置全局参数首先我们需要一个干净的系统并设置一些全局参数如孔径、波长和视场。def setup_system(the_app, focal_length100.0): 设置光学系统的基本参数。 the_app: TheApplication 对象 focal_length: 期望的近似焦距mm # 获取当前主系统 system the_app.PrimarySystem # 1. 设置系统孔径以入瞳直径为例 sys_data system.SystemData aperture sys_data.Aperture aperture.ApertureType sys_data.ApertureType.ENtrancePupilDiameter # 孔径类型入瞳直径 # 一个粗略估计对于f/4的单透镜入瞳直径约为焦距/4 est_epd focal_length / 4.0 aperture.ApertureValue est_epd print(f系统孔径设置为入瞳直径: {est_epd:.2f} mm) # 2. 设置波长 wavelengths sys_data.Wavelengths wavelengths.SelectWavelengthPreset(sys_data.WavelengthPreset.dVisible) # 选择可见光预设 # 也可以手动添加/修改波长这里使用预设简化操作 print(波长数据已设置为可见光预设。) # 3. 设置视场 fields sys_data.Fields fields.Clear() # 清空现有视场 # 添加一个轴上视场0度 field1 fields.AddField() field1.X 0.0 field1.Y 0.0 field1.Weight 1.0 # 添加一个轴外视场例如2度 field2 fields.AddField() field2.X 0.0 field2.Y 2.0 # Y方向2度 field2.Weight 0.5 print(视场已设置轴上(0,0)和轴外(0,2 deg)。) return system这里有几个细节值得注意孔径设置我们选择了“入瞳直径”作为孔径类型。在自动化脚本中这是一个稳妥的选择因为它不依赖于镜头具体结构。我们根据期望的F数这里假设f/4反推了一个初始入瞳直径。在实际优化中这个值本身也可以作为变量。波长预设SelectWavelengthPreset方法非常方便它直接加载了Zemax预定义的“可见光”波长F, d, C线。如果你有特殊需求可以遍历wavelengths集合并手动设置每个波长的值和权重。视场设置对于单透镜的初步优化设置一个轴上视场和一个较小的轴外视场通常就够了。权重用于在优化时平衡不同视场的像差贡献。轴上权重高确保近轴性能轴外权重较低开始控制彗差、像散等。3.2 构建透镜表面结构接下来是核心步骤在透镜数据编辑器LDE中创建表面并设置属性。一个最简单的平凸单透镜需要3个面物面、透镜前表面球面、透镜后表面平面、像面。def create_singlet_lens(lde, materialN-BK7, thickness5.0, curvature-0.02): 在LDE中创建一个平凸单透镜结构。 lde: 透镜数据编辑器对象 material: 透镜材料 thickness: 透镜中心厚度 (mm) curvature: 前表面的曲率 (1/mm)。负值表示曲率中心在表面左侧对于光从左向右传播这是凸面。 # 确保从干净状态开始通常新系统已有物面、光阑面、像面 # 我们将在第1面光阑面和第2面之间插入透镜的前后表面。 # 假设初始LDE结构0-物面1-光阑面2-像面 # 我们需要在面1和面2之间插入两个面。 # 1. 插入新面作为透镜前表面将在当前面1之后插入成为新面2 lde.InsertNewSurfaceAt(2) # 在索引2处插入0-based原面2及以后的面序号会1 surf1 lde.GetSurfaceAt(2) surf1.Comment Lens Front Surface surf1.Radius 1.0 / curvature if curvature ! 0 else 0.0 # 半径 1/曲率 surf1.Thickness thickness surf1.Glass material # 设置材料Zemax会自动识别材料库中的名称 # 设置面型为标准球面通常默认就是 surf1.SurfaceType lde.SurfaceType.Standard # 2. 插入新面作为透镜后表面在新面2之后插入成为新面3 lde.InsertNewSurfaceAt(3) surf2 lde.GetSurfaceAt(3) surf2.Comment Lens Rear Surface surf2.Radius 0.0 # 平面曲率半径为无穷大用0表示 surf2.Thickness 50.0 # 后表面到像面的空气间隔给一个初始估计值 surf2.Glass # 空气空字符串表示 # 3. 更新像面位置现在像面是面4 # 通常我们会将最后一个面的厚度设为变量在优化中确定最佳像面位置。 image_surf lde.GetSurfaceAt(4) image_surf.Thickness 0.0 # 像面厚度固定为0 image_surf.Comment Image Plane print(f单透镜结构创建完成。前表面曲率半径: {surf1.Radius:.2f} mm, 材料: {material}.) return surf1, surf2这段代码揭示了ZOS-API操作LDE的核心逻辑通过索引来定位和操作每一个面。InsertNewSurfaceAt(index)是关键方法它会在指定的索引位置插入一个新面。需要注意的是插入操作会改变后面所有面的索引因此编写代码时顺序很重要。我们通过GetSurfaceAt(index)获取特定面的对象然后修改其属性如Radius半径、Thickness厚度、Glass材料、Comment注释等。踩坑提醒Glass属性赋值时必须使用Zemax材料库中确切的名称字符串例如“N-BK7”。如果拼写错误或材料不存在Zemax会将其视为“模型玻璃”并可能引发后续计算错误。建议在脚本中维护一个常用材料列表或先通过API查询材料库。4. 定义优化目标与执行自动化优化有了初始结构接下来就是定义我们想要透镜达到什么目标评价函数并设置变量让程序自动调整。4.1 配置评价函数编辑器MFE评价函数是Zemax优化的“指挥棒”它通过一系列操作数Operands及其目标值和权重来量化系统性能的好坏。def setup_merit_function(the_app, target_efl100.0): 设置评价函数以优化单透镜。 the_app: TheApplication 对象 target_efl: 目标有效焦距 (mm) mfe the_app.PrimarySystem.MFE # 获取评价函数编辑器 mfe.Clear() # 清空现有操作数 # 1. 控制有效焦距EFFL操作数 oper1 mfe.AddOperand() oper1.ChangeType(mfe.OperandType.EFFL) # 操作数类型有效焦距 oper1.Target target_efl oper1.Weight 1.0 oper1.GetCellAt(11).Integer 1 # 波长相通常为1主波长 # 2. 控制透镜中心厚度避免太薄或太厚CTGT操作数 oper2 mfe.AddOperand() oper2.ChangeType(mfe.OperandType.CTGT) # 操作数中心厚度大于 oper2.Target 3.0 # 目标大于3mm oper2.Weight 1.0 oper2.GetCellAt(9).Integer 2 # 面1前表面 # 注意由于我们插入了面需要确认面序号。这里假设透镜前表面是当前LDE的第2面。 # 更稳健的做法是根据面的Comment或类型来动态确定索引。 # 3. 控制边缘厚度避免边缘太尖ETGT操作数 oper3 mfe.AddOperand() oper3.ChangeType(mfe.OperandType.ETGT) # 操作数边缘厚度大于 oper3.Target 2.0 # 目标大于2mm oper3.Weight 1.0 oper3.GetCellAt(9).Integer 2 # 面1 oper3.GetCellAt(10).Integer 3 # 面2 # 4. 优化像质使用点列图RMS半径RSCH操作数 oper4 mfe.AddOperand() oper4.ChangeType(mfe.OperandType.RSCH) # 操作数点列图RMS半径质心参考 oper4.Target 0.0 # 理想情况下为0 oper4.Weight 2.0 # 给予较高的权重 oper4.GetCellAt(9).Integer 0 # 视场0表示所有视场需要查证。更常用的是为每个视场单独添加。 oper4.GetCellAt(11).Integer 1 # 波长相主波长 # 更精细的做法为每个视场添加独立的RSCH操作数 # for field_idx in range(num_fields): # oper mfe.AddOperand() # oper.ChangeType(mfe.OperandType.RSCH) # oper.Target 0.0 # oper.Weight 1.0 # oper.GetCellAt(9).Integer field_idx 1 # 视场编号从1开始 # oper.GetCellAt(11).Integer 1 print(f评价函数已设置目标焦距: {target_efl} mm.) return mfe配置MFE时最大的挑战是正确设置每个操作数的参数即操作数编辑器中的各个单元格。每个操作数类型都有其特定的参数含义。例如EFFL操作数的第11个参数通过GetCellAt(11)访问通常代表“波长相”。这些索引数字在Zemax手册或API文档中有详细说明但在脚本中容易出错。一个实用的调试方法是先在Zemax图形界面手动添加并配置好你需要的操作数然后通过API读取这些操作数的属性来学习正确的参数索引和值。4.2 设置变量与执行优化现在告诉优化器哪些参数是可以改变的。def set_variables_and_optimize(lde, the_app): 设置变量并执行优化。 lde: 透镜数据编辑器对象 the_app: TheApplication 对象 # 1. 设置变量 # 假设透镜前表面是面2后表面是面3 front_surf lde.GetSurfaceAt(2) rear_surf lde.GetSurfaceAt(3) # 将前表面的曲率半径设为变量 front_surf.RadiusCell.MakeSolveVariable() # 更API化的方式 # 或者使用旧式APIfront_surf.GetCellAt(lde.SurfaceColumn.Radius).MakeSolveVariable() # 将前表面的厚度透镜中心厚度设为变量 front_surf.ThicknessCell.MakeSolveVariable() # 将后表面的厚度透镜到像面的空气间隔设为变量 rear_surf.ThicknessCell.MakeSolveVariable() print(变量已设置前表面半径、透镜中心厚度、后空气间隔。) # 2. 执行优化 tool the_app.PrimarySystem.Tools.OpenLocalOptimization() if tool is None: print(无法打开局部优化工具。) return # 配置优化算法通常使用默认的阻尼最小二乘法即可 # tool.Algorithm tool.OptimizationAlgorithm.DampedLeastSquares # 运行优化 print(开始优化...) result tool.Run() print(f优化完成。状态: {result}) # 检查优化结果可以读取评价函数值 mfe the_app.PrimarySystem.MFE current_mf mfe.GetOperandAt(0).Value # 获取第一个操作数的当前值仅供参考 print(f当前评价函数值部分: {current_mf}) tool.Close()MakeSolveVariable()方法是设置变量的标准方式。优化工具LocalOptimization提供了多种算法对于大多数成像镜头优化默认的“阻尼最小二乘法”是一个很好的起点。tool.Run()是阻塞调用脚本会等待优化完成。优化结果result通常是一个状态码表示优化是否成功收敛。重要经验优化不是一次性的。上述代码只进行了一次局部优化。在实际工作中复杂的优化流程可能包括锤形优化Hammer Optimization在局部优化基础上加入全局搜索有助于跳出局部极小值。可以通过Tools.OpenGlobalOptimization()来调用。多次循环交替进行局部优化和变量微调。优化边界检查在优化循环中加入代码检查透镜厚度、曲率等是否在物理可加工范围内如果超出则自动调整边界条件或操作数权重。 脚本化的优势就在于你可以轻松地将这些步骤组合成一个复杂的、自适应的优化流程。5. 结果分析与脚本健壮性提升优化完成后我们需要评估结果并将整个流程封装得更健壮、更通用。5.1 关键性能指标的提取与分析优化后不能只看评价函数值还需要提取关键的光学性能指标来判断透镜设计是否合格。def analyze_system_performance(the_app): 分析当前系统的关键性能指标。 system the_app.PrimarySystem analysis system.Analyses # 1. 获取系统数据焦距、F数等 sys_data system.SystemData paraxial_data sys_data.ParaxialData efl paraxialData.EffectiveFocalLength # 有效焦距 fno paraxialData.ImageSpaceFNumber # 像空间F数 print(f系统参数 - 有效焦距: {efl:.3f} mm, F数: {fno:.2f}) # 2. 执行并读取点列图分析 # 首先创建或获取一个点列图分析 # 注意NewAnalysis() 可能会创建新窗口在独立模式下可能不支持图形。我们使用API计算。 # 更可靠的方式是使用“标准分析”Standard Analysis功能进行计算。 # 这里以获取RMS点列图半径为例通过快速聚焦Quick Focus和计算特性来实现。 # 这是一个简化示例实际分析可能需要更复杂的设置。 from win32com.client import constants as const # 使用ZOSAPI.Analysis.Settings.RayFan类的API进行计算较为复杂。 # 更实用的方法直接读取评价函数中我们之前设置的RSCH操作数的值。 mfe system.MFE # 假设我们之前为每个视场添加了RSCH这里读取最后一个或某个RSCH操作数的值 num_operands mfe.NumberOfOperands for i in range(num_operands): oper mfe.GetOperandAt(i) if oper.TypeName RSCH: print(f视场 {oper.GetCellAt(9).Integer} 的RMS点列图半径: {oper.Value:.4f} mm) # 3. 检查透镜形状和合理性 lde system.LDE front_surf lde.GetSurfaceAt(2) rear_surf lde.GetSurfaceAt(3) center_thickness front_surf.Thickness edge_thickness system.LDE.GetSurfaceAt(2).EdgeThickness # 可能需要计算 # 注意EdgeThickness属性可能不直接暴露需要通过Solve或序列计算得到。 # 更简单的方法使用TTHI操作数在评价函数中计算并读取。 print(f透镜中心厚度: {center_thickness:.3f} mm) # 打印材料 print(f透镜材料: {front_surf.Glass}) # 4. 可以在这里添加更多分析如MTF、畸变等。 # 创建MTF分析 # mtf_settings analysis.New_Analysis_SettingsFirst(constants.AnalysisIDM.Mtf) # mtf_results analysis.ApplyAndWaitForAnalysis(mtf_settings) # 独立模式下可能有限制在独立模式下直接进行图形化分析如弹出MTF图可能受限或效率不高。更常见的做法是通过评价函数获取像RMS半径、MTF值等都可以通过对应的操作数如RSCH,MTFT,MTFS在优化后直接读取其Value属性。这是最高效、最程序化的方式。使用ZOS-API的分析接口ZOS-API提供了诸如I_Analysis_RayFan、I_Analysis_SystemViewer等接口可以以编程方式计算并返回数据数组而不是渲染图形。这需要更深入的API知识。输出ZMX文件并调用外部工具脚本可以将优化后的系统保存为.zmx文件然后由其他支持Zemax文件格式的专业分析工具或另一个脚本进行深度分析。5.2 构建容错与可配置的完整脚本将上述所有步骤整合并加入错误处理和配置选项形成一个实用的脚本。import win32com.client import sys import traceback class SingletLensOptimizer: def __init__(self, zemax_version20.3): self.app None self.the_app None self.lde None self.connected False def connect(self): 建立与ZOS的独立连接 try: connection win32com.client.Dispatch(ZOSAPI.ZOSAPI_Connection) self.app connection.CreateNewApplication() self.the_app self.app.AsIZOSAPI.TheApplication self.the_app.NewApplication() self.lde self.the_app.PrimarySystem.LDE self.connected True print( 成功连接到Zemax OpticStudio。) return True except Exception as e: print(f!!! 连接失败: {e}) traceback.print_exc() return False def disconnect(self): 关闭连接 if self.app: self.app.CloseApplication() self.app None self.the_app None self.lde None self.connected False print( 连接已关闭。) def run_design_cycle(self, config): 执行完整的设计周期。 config: 包含设计参数的字典例如 { target_efl: 100.0, material: N-BK7, initial_curvature: -0.02, initial_thickness: 5.0, min_center_thickness: 3.0, min_edge_thickness: 1.5 } if not self.connected: print(未连接到Zemax请先调用connect()。) return False try: # 1. 系统设置 print(\n--- 步骤1: 设置系统参数 ---) system self.the_app.PrimarySystem sys_data system.SystemData # 设置孔径、波长、视场 (代码略同上文) # ... # 2. 创建透镜结构 print(\n--- 步骤2: 创建初始透镜结构 ---) # (代码略同上文) # ... # 3. 设置评价函数 print(\n--- 步骤3: 配置评价函数 ---) self._setup_merit_function(config[target_efl], config[min_center_thickness], config[min_edge_thickness]) # 4. 设置变量 print(\n--- 步骤4: 设置优化变量 ---) self._set_variables() # 5. 执行优化循环 print(\n--- 步骤5: 执行优化 ---) self._run_optimization_cycle() # 6. 分析结果 print(\n--- 步骤6: 分析最终设计 ---) performance self._analyze_results() return performance except Exception as e: print(f\n!!! 设计过程中出现错误: {e}) traceback.print_exc() return None def _setup_merit_function(self, target_efl, min_ct, min_et): 内部方法设置评价函数 mfe self.the_app.PrimarySystem.MFE mfe.Clear() # 添加EFFL, CTGT, ETGT, RSCH等操作数 (代码略) # ... def _run_optimization_cycle(self, max_cycles3): 内部方法执行优化循环可能包含局部和全局优化 tool_local self.the_app.PrimarySystem.Tools.OpenLocalOptimization() for cycle in range(max_cycles): print(f 优化循环 {cycle 1}/{max_cycles}...) result tool_local.Run() if not result: # 假设优化成功返回True print(f 警告第{cycle1}次局部优化未完全收敛。) # 可以在这里加入一些判断如果评价函数下降不明显则提前退出 tool_local.Close() # 可选加入锤形优化 # print( 执行锤形优化...) # tool_global self.the_app.PrimarySystem.Tools.OpenGlobalOptimization() # tool_global.Algorithm tool_global.GlobalAlgorithm.Hammer # tool_global.Run() # tool_global.Close() # 使用示例 if __name__ __main__: config { target_efl: 100.0, material: N-BK7, initial_curvature: -0.02, initial_thickness: 5.0, min_center_thickness: 3.0, min_edge_thickness: 1.5 } optimizer SingletLensOptimizer() if optimizer.connect(): try: result optimizer.run_design_cycle(config) if result: print(\n 设计完成) # 可以在这里保存文件 # optimizer.the_app.PrimarySystem.SaveAs(rC:\MyLens.zmx) else: print(\n!!! 设计失败。) finally: optimizer.disconnect()这个类封装了完整流程并提供了基本的错误处理。在实际项目中你可以进一步扩展它例如从配置文件如JSON/YAML读取设计规格使脚本更通用。实现多线程或并行优化同时探索多个不同的初始结构。集成更复杂的分析如公差灵敏度分析、热分析等。自动生成设计报告将关键参数、性能指标和系统图输出为PDF或HTML。通过这样的脚本你将单透镜的设计从依赖于个人经验和手动操作的艺术转变为一个可重复、可验证、可集成的自动化工程流程。这不仅极大提升了效率也为更复杂的光学系统自动化设计奠定了坚实的基础。
Python自动化光学设计:基于ZOS-API的单透镜构建与优化
1. 从脚本到光机为什么需要COM与ZOS-API如果你在光学设计领域工作尤其是使用Zemax OpticStudioZOS那么你大概率经历过这样的场景为了优化一个简单的单透镜你需要在软件界面里反复点击“优化”按钮手动调整曲率、厚度然后查看MTF、点列图再根据经验微调操作数权重。这个过程对于探索性设计或者一次性任务尚可接受但一旦涉及到参数扫描、批量分析、或者将光学设计流程嵌入到一个更大的自动化系统中比如与机械设计、热分析联动纯手动操作就显得力不从心效率低下且容易出错。这正是Python与ZOS-APIApplication Programming Interface结合的价值所在。ZOS-API是Zemax OpticStudio提供的一套编程接口它允许外部程序如Python脚本以编程方式控制OpticStudio实现几乎所有的软件功能。而COMComponent Object Model是微软开发的一种跨应用通信技术ZOS-API正是通过COM接口暴露其功能。简单来说你可以把OpticStudio想象成一个功能强大的“服务器”你的Python脚本就是“客户端”通过COM这条“数据线”客户端可以发送指令让服务器执行任何操作并获取结果。使用Python驱动ZOS-API意味着你可以将光学设计流程代码化、自动化。对于构建和优化一个单透镜你可以编写脚本来自动完成创建新镜头文件、设置波长和视场、添加表面并赋予初始结构、定义变量、设置评价函数Merit Function、运行优化、分析结果并输出报告。整个过程无需人工干预脚本可以7x24小时运行尝试成千上万种初始条件组合找到全局最优解或者集成到CI/CD流程中确保每次设计迭代的可重复性。2. 环境搭建与连接初始化迈出第一步在开始编写构建单透镜的脚本之前我们需要先搭建好Python与ZOS-API通信的桥梁。这个过程虽然基础但有几个关键点直接决定了后续脚本的稳定性和可移植性。2.1 核心依赖与版本匹配首先你需要确保安装了正确版本的Python和Zemax OpticStudio。ZOS-API对版本有严格要求通常建议使用Python 3.7至3.9的64位版本这与当前主流OpticStudio版本如20.3的兼容性最好。你可以通过命令行输入python --version来确认。Python与ZOS-API交互的核心库是win32com它提供了在Python中调用COM组件的能力。这个库通常通过pywin32包来安装。最稳妥的安装方式是使用pippip install pywin32安装完成后一个常见的验证方法是尝试导入win32com.client如果没有报错说明环境基本就绪。注意有时在复杂的Python环境如Anaconda中可能会遇到路径或权限问题。如果导入失败可以尝试以管理员身份运行命令行进行安装或者检查环境变量。2.2 建立与Zemax的连接连接Zemax OpticStudio有两种基本模式独立模式Standalone和交互模式Interactive。对于自动化构建和优化任务我们强烈推荐使用独立模式。独立模式脚本会启动一个“看不见”的Zemax OpticStudio后台进程。这个进程没有图形界面完全由你的脚本控制。它的优点是运行速度快、资源占用相对较少且不会干扰你前台可能正在进行的其他手动设计工作。脚本结束后该进程会自动关闭。交互模式脚本会连接到你已经打开的一个Zemax OpticStudio桌面程序实例。你可以实时看到脚本对镜头文件所做的修改。这常用于调试或者需要人工介入观察的复杂场景。但对于自动化流程界面刷新会带来不必要的性能开销且容易因人为操作导致状态不一致。以下是建立独立模式连接的经典代码段import win32com.client def create_standalone_connection(): 创建与ZOS-API的独立模式连接。 返回连接对象和TheApplication实例。 # 生成ZOS-API连接器 connection win32com.client.Dispatch(ZOSAPI.ZOSAPI_Connection) if connection is None: raise RuntimeError(无法创建ZOSAPI连接。请检查Zemax OpticStudio是否正确安装。) # 创建独立应用实例 app connection.CreateNewApplication() if app is None: raise RuntimeError(无法创建ZOSAPI应用实例。) # 获取顶层的API接口 zos_api app.AsIZOSAPI # 获取TheApplication对象它是大多数操作的起点 the_application zos_api.TheApplication the_application.NewApplication() # 确保是一个新的应用会话 # 获取透镜数据编辑器LDE的接口 lde the_application.PrimarySystem.LDE return app, the_application, lde # 使用示例 try: app, the_app, lde create_standalone_connection() print(成功连接到Zemax OpticStudio独立实例。) except Exception as e: print(f连接失败: {e})这段代码是几乎所有ZOS-API脚本的起点。win32com.client.Dispatch是关键它通过COM系统定位并激活ZOSAPI库。CreateNewApplication()方法触发了独立进程的启动。获取到的the_application对象是你的“总控台”通过它可以访问当前光学系统、文件、工具等一切资源。lde对象则是对透镜数据编辑器的直接引用后续添加表面、设置参数都要通过它。实操心得务必在脚本的最后加入资源清理的代码即调用app.CloseApplication()。否则独立模式下的Zemax后台进程可能会残留积累多了会占用大量内存。一个好的实践是使用Python的上下文管理器with语句或try...finally块来确保连接被正确关闭。3. 单透镜的自动化构建从零到初始结构有了连接之后我们就可以开始“无中生有”用代码构建一个单透镜。我们假设要构建一个经典的平凸单透镜材料为N-BK7用于可见光波段焦距约100mm。3.1 创建系统与设置全局参数首先我们需要一个干净的系统并设置一些全局参数如孔径、波长和视场。def setup_system(the_app, focal_length100.0): 设置光学系统的基本参数。 the_app: TheApplication 对象 focal_length: 期望的近似焦距mm # 获取当前主系统 system the_app.PrimarySystem # 1. 设置系统孔径以入瞳直径为例 sys_data system.SystemData aperture sys_data.Aperture aperture.ApertureType sys_data.ApertureType.ENtrancePupilDiameter # 孔径类型入瞳直径 # 一个粗略估计对于f/4的单透镜入瞳直径约为焦距/4 est_epd focal_length / 4.0 aperture.ApertureValue est_epd print(f系统孔径设置为入瞳直径: {est_epd:.2f} mm) # 2. 设置波长 wavelengths sys_data.Wavelengths wavelengths.SelectWavelengthPreset(sys_data.WavelengthPreset.dVisible) # 选择可见光预设 # 也可以手动添加/修改波长这里使用预设简化操作 print(波长数据已设置为可见光预设。) # 3. 设置视场 fields sys_data.Fields fields.Clear() # 清空现有视场 # 添加一个轴上视场0度 field1 fields.AddField() field1.X 0.0 field1.Y 0.0 field1.Weight 1.0 # 添加一个轴外视场例如2度 field2 fields.AddField() field2.X 0.0 field2.Y 2.0 # Y方向2度 field2.Weight 0.5 print(视场已设置轴上(0,0)和轴外(0,2 deg)。) return system这里有几个细节值得注意孔径设置我们选择了“入瞳直径”作为孔径类型。在自动化脚本中这是一个稳妥的选择因为它不依赖于镜头具体结构。我们根据期望的F数这里假设f/4反推了一个初始入瞳直径。在实际优化中这个值本身也可以作为变量。波长预设SelectWavelengthPreset方法非常方便它直接加载了Zemax预定义的“可见光”波长F, d, C线。如果你有特殊需求可以遍历wavelengths集合并手动设置每个波长的值和权重。视场设置对于单透镜的初步优化设置一个轴上视场和一个较小的轴外视场通常就够了。权重用于在优化时平衡不同视场的像差贡献。轴上权重高确保近轴性能轴外权重较低开始控制彗差、像散等。3.2 构建透镜表面结构接下来是核心步骤在透镜数据编辑器LDE中创建表面并设置属性。一个最简单的平凸单透镜需要3个面物面、透镜前表面球面、透镜后表面平面、像面。def create_singlet_lens(lde, materialN-BK7, thickness5.0, curvature-0.02): 在LDE中创建一个平凸单透镜结构。 lde: 透镜数据编辑器对象 material: 透镜材料 thickness: 透镜中心厚度 (mm) curvature: 前表面的曲率 (1/mm)。负值表示曲率中心在表面左侧对于光从左向右传播这是凸面。 # 确保从干净状态开始通常新系统已有物面、光阑面、像面 # 我们将在第1面光阑面和第2面之间插入透镜的前后表面。 # 假设初始LDE结构0-物面1-光阑面2-像面 # 我们需要在面1和面2之间插入两个面。 # 1. 插入新面作为透镜前表面将在当前面1之后插入成为新面2 lde.InsertNewSurfaceAt(2) # 在索引2处插入0-based原面2及以后的面序号会1 surf1 lde.GetSurfaceAt(2) surf1.Comment Lens Front Surface surf1.Radius 1.0 / curvature if curvature ! 0 else 0.0 # 半径 1/曲率 surf1.Thickness thickness surf1.Glass material # 设置材料Zemax会自动识别材料库中的名称 # 设置面型为标准球面通常默认就是 surf1.SurfaceType lde.SurfaceType.Standard # 2. 插入新面作为透镜后表面在新面2之后插入成为新面3 lde.InsertNewSurfaceAt(3) surf2 lde.GetSurfaceAt(3) surf2.Comment Lens Rear Surface surf2.Radius 0.0 # 平面曲率半径为无穷大用0表示 surf2.Thickness 50.0 # 后表面到像面的空气间隔给一个初始估计值 surf2.Glass # 空气空字符串表示 # 3. 更新像面位置现在像面是面4 # 通常我们会将最后一个面的厚度设为变量在优化中确定最佳像面位置。 image_surf lde.GetSurfaceAt(4) image_surf.Thickness 0.0 # 像面厚度固定为0 image_surf.Comment Image Plane print(f单透镜结构创建完成。前表面曲率半径: {surf1.Radius:.2f} mm, 材料: {material}.) return surf1, surf2这段代码揭示了ZOS-API操作LDE的核心逻辑通过索引来定位和操作每一个面。InsertNewSurfaceAt(index)是关键方法它会在指定的索引位置插入一个新面。需要注意的是插入操作会改变后面所有面的索引因此编写代码时顺序很重要。我们通过GetSurfaceAt(index)获取特定面的对象然后修改其属性如Radius半径、Thickness厚度、Glass材料、Comment注释等。踩坑提醒Glass属性赋值时必须使用Zemax材料库中确切的名称字符串例如“N-BK7”。如果拼写错误或材料不存在Zemax会将其视为“模型玻璃”并可能引发后续计算错误。建议在脚本中维护一个常用材料列表或先通过API查询材料库。4. 定义优化目标与执行自动化优化有了初始结构接下来就是定义我们想要透镜达到什么目标评价函数并设置变量让程序自动调整。4.1 配置评价函数编辑器MFE评价函数是Zemax优化的“指挥棒”它通过一系列操作数Operands及其目标值和权重来量化系统性能的好坏。def setup_merit_function(the_app, target_efl100.0): 设置评价函数以优化单透镜。 the_app: TheApplication 对象 target_efl: 目标有效焦距 (mm) mfe the_app.PrimarySystem.MFE # 获取评价函数编辑器 mfe.Clear() # 清空现有操作数 # 1. 控制有效焦距EFFL操作数 oper1 mfe.AddOperand() oper1.ChangeType(mfe.OperandType.EFFL) # 操作数类型有效焦距 oper1.Target target_efl oper1.Weight 1.0 oper1.GetCellAt(11).Integer 1 # 波长相通常为1主波长 # 2. 控制透镜中心厚度避免太薄或太厚CTGT操作数 oper2 mfe.AddOperand() oper2.ChangeType(mfe.OperandType.CTGT) # 操作数中心厚度大于 oper2.Target 3.0 # 目标大于3mm oper2.Weight 1.0 oper2.GetCellAt(9).Integer 2 # 面1前表面 # 注意由于我们插入了面需要确认面序号。这里假设透镜前表面是当前LDE的第2面。 # 更稳健的做法是根据面的Comment或类型来动态确定索引。 # 3. 控制边缘厚度避免边缘太尖ETGT操作数 oper3 mfe.AddOperand() oper3.ChangeType(mfe.OperandType.ETGT) # 操作数边缘厚度大于 oper3.Target 2.0 # 目标大于2mm oper3.Weight 1.0 oper3.GetCellAt(9).Integer 2 # 面1 oper3.GetCellAt(10).Integer 3 # 面2 # 4. 优化像质使用点列图RMS半径RSCH操作数 oper4 mfe.AddOperand() oper4.ChangeType(mfe.OperandType.RSCH) # 操作数点列图RMS半径质心参考 oper4.Target 0.0 # 理想情况下为0 oper4.Weight 2.0 # 给予较高的权重 oper4.GetCellAt(9).Integer 0 # 视场0表示所有视场需要查证。更常用的是为每个视场单独添加。 oper4.GetCellAt(11).Integer 1 # 波长相主波长 # 更精细的做法为每个视场添加独立的RSCH操作数 # for field_idx in range(num_fields): # oper mfe.AddOperand() # oper.ChangeType(mfe.OperandType.RSCH) # oper.Target 0.0 # oper.Weight 1.0 # oper.GetCellAt(9).Integer field_idx 1 # 视场编号从1开始 # oper.GetCellAt(11).Integer 1 print(f评价函数已设置目标焦距: {target_efl} mm.) return mfe配置MFE时最大的挑战是正确设置每个操作数的参数即操作数编辑器中的各个单元格。每个操作数类型都有其特定的参数含义。例如EFFL操作数的第11个参数通过GetCellAt(11)访问通常代表“波长相”。这些索引数字在Zemax手册或API文档中有详细说明但在脚本中容易出错。一个实用的调试方法是先在Zemax图形界面手动添加并配置好你需要的操作数然后通过API读取这些操作数的属性来学习正确的参数索引和值。4.2 设置变量与执行优化现在告诉优化器哪些参数是可以改变的。def set_variables_and_optimize(lde, the_app): 设置变量并执行优化。 lde: 透镜数据编辑器对象 the_app: TheApplication 对象 # 1. 设置变量 # 假设透镜前表面是面2后表面是面3 front_surf lde.GetSurfaceAt(2) rear_surf lde.GetSurfaceAt(3) # 将前表面的曲率半径设为变量 front_surf.RadiusCell.MakeSolveVariable() # 更API化的方式 # 或者使用旧式APIfront_surf.GetCellAt(lde.SurfaceColumn.Radius).MakeSolveVariable() # 将前表面的厚度透镜中心厚度设为变量 front_surf.ThicknessCell.MakeSolveVariable() # 将后表面的厚度透镜到像面的空气间隔设为变量 rear_surf.ThicknessCell.MakeSolveVariable() print(变量已设置前表面半径、透镜中心厚度、后空气间隔。) # 2. 执行优化 tool the_app.PrimarySystem.Tools.OpenLocalOptimization() if tool is None: print(无法打开局部优化工具。) return # 配置优化算法通常使用默认的阻尼最小二乘法即可 # tool.Algorithm tool.OptimizationAlgorithm.DampedLeastSquares # 运行优化 print(开始优化...) result tool.Run() print(f优化完成。状态: {result}) # 检查优化结果可以读取评价函数值 mfe the_app.PrimarySystem.MFE current_mf mfe.GetOperandAt(0).Value # 获取第一个操作数的当前值仅供参考 print(f当前评价函数值部分: {current_mf}) tool.Close()MakeSolveVariable()方法是设置变量的标准方式。优化工具LocalOptimization提供了多种算法对于大多数成像镜头优化默认的“阻尼最小二乘法”是一个很好的起点。tool.Run()是阻塞调用脚本会等待优化完成。优化结果result通常是一个状态码表示优化是否成功收敛。重要经验优化不是一次性的。上述代码只进行了一次局部优化。在实际工作中复杂的优化流程可能包括锤形优化Hammer Optimization在局部优化基础上加入全局搜索有助于跳出局部极小值。可以通过Tools.OpenGlobalOptimization()来调用。多次循环交替进行局部优化和变量微调。优化边界检查在优化循环中加入代码检查透镜厚度、曲率等是否在物理可加工范围内如果超出则自动调整边界条件或操作数权重。 脚本化的优势就在于你可以轻松地将这些步骤组合成一个复杂的、自适应的优化流程。5. 结果分析与脚本健壮性提升优化完成后我们需要评估结果并将整个流程封装得更健壮、更通用。5.1 关键性能指标的提取与分析优化后不能只看评价函数值还需要提取关键的光学性能指标来判断透镜设计是否合格。def analyze_system_performance(the_app): 分析当前系统的关键性能指标。 system the_app.PrimarySystem analysis system.Analyses # 1. 获取系统数据焦距、F数等 sys_data system.SystemData paraxial_data sys_data.ParaxialData efl paraxialData.EffectiveFocalLength # 有效焦距 fno paraxialData.ImageSpaceFNumber # 像空间F数 print(f系统参数 - 有效焦距: {efl:.3f} mm, F数: {fno:.2f}) # 2. 执行并读取点列图分析 # 首先创建或获取一个点列图分析 # 注意NewAnalysis() 可能会创建新窗口在独立模式下可能不支持图形。我们使用API计算。 # 更可靠的方式是使用“标准分析”Standard Analysis功能进行计算。 # 这里以获取RMS点列图半径为例通过快速聚焦Quick Focus和计算特性来实现。 # 这是一个简化示例实际分析可能需要更复杂的设置。 from win32com.client import constants as const # 使用ZOSAPI.Analysis.Settings.RayFan类的API进行计算较为复杂。 # 更实用的方法直接读取评价函数中我们之前设置的RSCH操作数的值。 mfe system.MFE # 假设我们之前为每个视场添加了RSCH这里读取最后一个或某个RSCH操作数的值 num_operands mfe.NumberOfOperands for i in range(num_operands): oper mfe.GetOperandAt(i) if oper.TypeName RSCH: print(f视场 {oper.GetCellAt(9).Integer} 的RMS点列图半径: {oper.Value:.4f} mm) # 3. 检查透镜形状和合理性 lde system.LDE front_surf lde.GetSurfaceAt(2) rear_surf lde.GetSurfaceAt(3) center_thickness front_surf.Thickness edge_thickness system.LDE.GetSurfaceAt(2).EdgeThickness # 可能需要计算 # 注意EdgeThickness属性可能不直接暴露需要通过Solve或序列计算得到。 # 更简单的方法使用TTHI操作数在评价函数中计算并读取。 print(f透镜中心厚度: {center_thickness:.3f} mm) # 打印材料 print(f透镜材料: {front_surf.Glass}) # 4. 可以在这里添加更多分析如MTF、畸变等。 # 创建MTF分析 # mtf_settings analysis.New_Analysis_SettingsFirst(constants.AnalysisIDM.Mtf) # mtf_results analysis.ApplyAndWaitForAnalysis(mtf_settings) # 独立模式下可能有限制在独立模式下直接进行图形化分析如弹出MTF图可能受限或效率不高。更常见的做法是通过评价函数获取像RMS半径、MTF值等都可以通过对应的操作数如RSCH,MTFT,MTFS在优化后直接读取其Value属性。这是最高效、最程序化的方式。使用ZOS-API的分析接口ZOS-API提供了诸如I_Analysis_RayFan、I_Analysis_SystemViewer等接口可以以编程方式计算并返回数据数组而不是渲染图形。这需要更深入的API知识。输出ZMX文件并调用外部工具脚本可以将优化后的系统保存为.zmx文件然后由其他支持Zemax文件格式的专业分析工具或另一个脚本进行深度分析。5.2 构建容错与可配置的完整脚本将上述所有步骤整合并加入错误处理和配置选项形成一个实用的脚本。import win32com.client import sys import traceback class SingletLensOptimizer: def __init__(self, zemax_version20.3): self.app None self.the_app None self.lde None self.connected False def connect(self): 建立与ZOS的独立连接 try: connection win32com.client.Dispatch(ZOSAPI.ZOSAPI_Connection) self.app connection.CreateNewApplication() self.the_app self.app.AsIZOSAPI.TheApplication self.the_app.NewApplication() self.lde self.the_app.PrimarySystem.LDE self.connected True print( 成功连接到Zemax OpticStudio。) return True except Exception as e: print(f!!! 连接失败: {e}) traceback.print_exc() return False def disconnect(self): 关闭连接 if self.app: self.app.CloseApplication() self.app None self.the_app None self.lde None self.connected False print( 连接已关闭。) def run_design_cycle(self, config): 执行完整的设计周期。 config: 包含设计参数的字典例如 { target_efl: 100.0, material: N-BK7, initial_curvature: -0.02, initial_thickness: 5.0, min_center_thickness: 3.0, min_edge_thickness: 1.5 } if not self.connected: print(未连接到Zemax请先调用connect()。) return False try: # 1. 系统设置 print(\n--- 步骤1: 设置系统参数 ---) system self.the_app.PrimarySystem sys_data system.SystemData # 设置孔径、波长、视场 (代码略同上文) # ... # 2. 创建透镜结构 print(\n--- 步骤2: 创建初始透镜结构 ---) # (代码略同上文) # ... # 3. 设置评价函数 print(\n--- 步骤3: 配置评价函数 ---) self._setup_merit_function(config[target_efl], config[min_center_thickness], config[min_edge_thickness]) # 4. 设置变量 print(\n--- 步骤4: 设置优化变量 ---) self._set_variables() # 5. 执行优化循环 print(\n--- 步骤5: 执行优化 ---) self._run_optimization_cycle() # 6. 分析结果 print(\n--- 步骤6: 分析最终设计 ---) performance self._analyze_results() return performance except Exception as e: print(f\n!!! 设计过程中出现错误: {e}) traceback.print_exc() return None def _setup_merit_function(self, target_efl, min_ct, min_et): 内部方法设置评价函数 mfe self.the_app.PrimarySystem.MFE mfe.Clear() # 添加EFFL, CTGT, ETGT, RSCH等操作数 (代码略) # ... def _run_optimization_cycle(self, max_cycles3): 内部方法执行优化循环可能包含局部和全局优化 tool_local self.the_app.PrimarySystem.Tools.OpenLocalOptimization() for cycle in range(max_cycles): print(f 优化循环 {cycle 1}/{max_cycles}...) result tool_local.Run() if not result: # 假设优化成功返回True print(f 警告第{cycle1}次局部优化未完全收敛。) # 可以在这里加入一些判断如果评价函数下降不明显则提前退出 tool_local.Close() # 可选加入锤形优化 # print( 执行锤形优化...) # tool_global self.the_app.PrimarySystem.Tools.OpenGlobalOptimization() # tool_global.Algorithm tool_global.GlobalAlgorithm.Hammer # tool_global.Run() # tool_global.Close() # 使用示例 if __name__ __main__: config { target_efl: 100.0, material: N-BK7, initial_curvature: -0.02, initial_thickness: 5.0, min_center_thickness: 3.0, min_edge_thickness: 1.5 } optimizer SingletLensOptimizer() if optimizer.connect(): try: result optimizer.run_design_cycle(config) if result: print(\n 设计完成) # 可以在这里保存文件 # optimizer.the_app.PrimarySystem.SaveAs(rC:\MyLens.zmx) else: print(\n!!! 设计失败。) finally: optimizer.disconnect()这个类封装了完整流程并提供了基本的错误处理。在实际项目中你可以进一步扩展它例如从配置文件如JSON/YAML读取设计规格使脚本更通用。实现多线程或并行优化同时探索多个不同的初始结构。集成更复杂的分析如公差灵敏度分析、热分析等。自动生成设计报告将关键参数、性能指标和系统图输出为PDF或HTML。通过这样的脚本你将单透镜的设计从依赖于个人经验和手动操作的艺术转变为一个可重复、可验证、可集成的自动化工程流程。这不仅极大提升了效率也为更复杂的光学系统自动化设计奠定了坚实的基础。