评价函数与优化
评价函数(Zemax 的 Merit Function Editor)直接写在 .lens 里,和镜头数据一起版本管理、一起 diff。 全部操作数见 操作数参考(评价函数)。
写评价函数
var R1..R6 T6
default spot centroid rings=3 arms=6 glass_min=1 air_min=0.5 # 默认评价函数 + 厚度边界
op EFFL target=50 weight=10 # 等式目标
op TOTR <= 62 # 不等式:只在越界时计入
op MNCT 1..6 >= 0.8 # 范围约束:同 Zemax,一行一个值(目标 + 各面超限量之和)
op RSCE ring=3 Hy=1 target=5um # 目标可以带单位(操作数的值用镜头单位 mm)
op EFFL name=f weight=0 # weight=0:只计算、不参与优化
op EPDI name=epd weight=0
op EXPR expr="f/epd" target=5 # 表达式引用其他行的 name变量可以带上下界(Zemax 的变量没有界,靠 MNCT / MXCT 这类操作数约束;LensBench 两种都支持):
var R1 in 10..100 R2..R4 in -80..-15 T6 in 30..50mm K1- 写在引用(或范围)后面:
in 下界..上界,作用于这一项里的全部变量;lb vary x.lens T6 in 38..45对已经是变量的会更新它的界。 - 模型玻璃
glass=model(nd, vd)的 nd、vd 可以当变量:var nd@3 vd@3(见下「玻璃」)。 - R、T 是长度(mm,可写单位),K、非球面系数是原值。半径的界两端要同号——跨过 0 时曲率不连续;要允许正负翻转就不给界。
- 有界时局部优化改用带界的信赖域法(DLS 的 Levenberg-Marquardt 不支持界),R 按曲率守界;全局(hammer)的扰动点也夹在界内。
- 初值在界外:先拉回界内再优化,结果照样写回(至少把设计拉回界内),并在结果里说明;结束时停在边界上的变量单独报出 (「放宽上下界可能还能更好」)。
要点:
操作数名照 Zemax;参数用名字代替 Int1/Int2/Hx/Hy/Px/Py。需要面号的操作数可以把面号(或范围)写成第一个参数。
波长、视场序号从 1 起;像质类操作数
wave=0表示复色(按波长权重)。default spot展开成逐光线横向残差(高斯求积 rings × arms,全部视场 × 波长按权重),参考质心或主光线;default wave是逐光线 OPD 残差。只有默认评价函数时,评价函数值约等于加权 RMS 弥散斑半径(mm)。中心遮拦系统(卡塞格林等)加
obscuration=ε(遮拦半径 / 光瞳半径,与 Zemax 优化向导的 Obscuration 相同):求积环布在 ρ ∈ [ε, 1] 的环带里。不填时环布满整个光瞳,落在遮拦上的光线按「光线失败」计罚,评价函数会被罚值淹没。评价函数值的归一化与 Zemax 相同:
其中
、 、 是第 项的权重、当前值和目标值(每条op行和默认评价函数展开出的每一项各算一项)。默认评价函数的总权重与 Zemax 相同:展开出的各项权重之和 = π × 视场权重均值 × 波长权重均值(再乘
default的weight=), 所以lb merit里 D 行的权重不是 1。只有默认评价函数时这个系数约掉;与op行混用时,两者的相对比重与 Zemax 一致。带面号范围的边界操作数(
MNCT…MXEA、MNIN…MXAB)同 Zemax:一行只算一个值 = 目标 + 各面超限量之和 (都没越界时等于目标),残差一个、权重只计一次;想看各面的值用表达式MNCT(1:6)或lb calc。EXPR的写法见下一节「表达式」。
表达式(MATLAB 风格)
op EXPR expr="…" 里写的是一个小的向量化表达式语言,写法照 MATLAB;结果可以是一个数,也可以是一个向量 (向量逐元素计残差,<= / >= 时只有越界的元素计入)。
op EXPR expr="EFFL(wave=1) - EFFL(wave=3)" target=0 # 直接调用操作数,不用另开 weight=0 的行
op EXPR expr="max(RSCE(ring=3, Hy=0:0.25:1))" <= 0.01 # 视场 0、0.25 … 1 上的 RSCE,取最大
op EXPR expr="RSCE(ring=3, Hy=[0 0.7 1])" <= 0.01 # 向量:三个视场各一个残差
op EXPR expr="max(CTVA(1:6)) - min(CTVA(1:6))" <= 4 # 中心厚度的极差
op EXPR expr="rms(TRAY(Hy=1, Py=-1:0.5:1))" target=0
op EFFL name=f weight=0
op EPDI name=epd weight=0
op EXPR expr="f/epd" target=5 # 引用 name= 的行(旧写法照旧)| 写法 | 例子 |
|---|---|
| 数、常数 | 1、2.5、1e-3、pi |
| 向量 | [0 0.5 1]、[0, 0.5, 1]、[1 -2] 是两个元素(MATLAB 规则:前有空格、后无空格的负号开始新元素),[1 - 2] 是 −1;元素可以是向量(拼接) |
| 区间 | a:b(步长 1)、a:步长:b;linspace(a, b, n) |
| 运算 | + - * / ^,也认 ** 与 .* ./ .^;一律逐元素,标量自动展开,两个向量要等长;乘方的指数必须是数字(x^2、h.^0.5) |
| 数学函数 | abs sqrt exp log log10 sin cos tan asin acos atan atan2 hypot floor ceil round sign clip deg2rad rad2deg,角度版 sind cosd tand asind acosd atand |
| 归约 | max min(一个参数取最大 / 最小,多个参数逐元素)、sum mean rms std numel first last(std 同 MATLAB 按 N − 1) |
| 名字 | op 行的 name= |
| 操作数 | 大写名字加括号:EFFL()、RSCE(ring=3, Hy=h)、CTVA(3)、REAY(5, Hy=1, Py=1)——参数名照操作数参考,第一个位置参数同 op 行;参数是向量时逐个展开、结果是向量;面号范围参数(MNCT 等)写成 1:6,返回各面的值(不是 op 行里合成的那一个数) |
单位:操作数的值用它自己的单位(长度 mm、波前 λ、角度 °、畸变 %),表达式里的数不带单位。
中间量 let
let 名 = 表达式 定义一个中间量(一个数或一个向量),别的 let 与 op EXPR 都能引用;let 能引用 op 行的 name= 与别的 let。写在哪里都行:像电子表格那样按依赖关系求值(被用到的先算),与书写顺序无关——下面两种写法算出来逐位相同, 评价函数的证据 id 也相同:
let h = 0:0.25:1 # 视场
let r = RSCE(ring=3, Hy=h) # 各视场的 RMS 弥散斑(mm)
op EXPR expr="max(r)" <= 0.01
op EXPR expr="max(r) - min(r)" <= 0.004 # 视场间的均匀性let r = RSCE(ring=3, Hy=h) # 先写用到 h 的也行
let h = 0:0.25:1不能循环引用:let a = b + 1 与 let b = 2 * a(或 let a = a + 1)报 E-LET-CYCLE,诊断里列出环上的名字与行号。
lb merit 与桌面端「评价函数」页签(let 行的行尾)列出每个 let 的值或失败原因(按书写顺序)。命令里改: lb let-set x.lens h "[0 0.7 1]"、lb let-delete x.lens h(还被别的 let 或 op EXPR 行用到时拒绝,并列出用到它的行——写在它 前面后面的都算)。
一次性计算 lb calc
像在 MATLAB 的命令窗口里算一句,不改设计:
lb calc cooke.lens "EFFL(wave=1) - EFFL(wave=3)"
lb calc cooke.lens "RSCE(ring=3, Hy=0:0.25:1) * 1000" # µm
lb calc cooke.lens "max(CTVA(1:6)) - min(CTVA(1:6))"可以引用 op 行的 name= 与 let。结果带证据 id(证据键含表达式与评价函数:改了 let,同一个表达式的证据 id 也会变)。 桌面端底部的命令行、右侧「终端」页里同样能敲。
为防止别人分享的文件用表达式耗光资源:表达式不超过 400 字符、括号不超过 12 层、不超过 160 个节点,乘方的指数必须是 ±16 以内的 数字,数字绝对值不超过 1e12,向量不超过 1001 个元素,一个表达式展开后的操作数求值不超过 512 次。超限报 E-EXPR-LIMIT (求值时超限的那一行按失败计罚)。表达式不执行任何代码、不读写文件。
玻璃:先用模型玻璃优化
Zemax 的常见做法:要换玻璃的面先写成模型玻璃,让 nd、vd 跟着优化,再换成最接近的目录玻璃。
surf 2 R=-25 T=2 glass=model(1.62, 45)
var nd@2 vd@2 # 没写上下界:nd 1.40–2.10、vd 15–100
op AXCL target=0 weight=10 # 例如:消轴向色差- 每一步优化按新的 (nd, vd) 换材料(模型玻璃的色散公式同
model(…));光在玻璃里经反射镜背面反射(曼金镜)时镜面跟着换。 - 优化完 nd、vd 一起写回成
glass=model(…)(nd 6 位小数、vd 4 位)。 - 没写上下界时的默认范围是常见光学玻璃的范围——Zemax 的模型玻璃本身不受限,靠 RGLA 操作数把它拉回玻璃区; 不给界时优化器会把 nd 推到不存在的值。停在默认范围端点时结果里会说明。
- 配套操作数(与 Zemax 同名):
op RGLA weight=0.1(模型玻璃离最近的目录玻璃多远,目标 0——让优化停在真实玻璃附近, 之后替换时变化小)、op MNIN 1..6 >= 1.5/MXIN/MNAB/MXAB(约束 nd、vd 的范围;带面号范围时同 Zemax,一行一个值 = 目标 + 各面超限量之和)、op INDX 3 wave=2(某面后介质在某波长的折射率)。模型玻璃的 nd、vd 取写的值,目录玻璃从 d / F / C 线折射率算。
优化完换成目录玻璃:
lb glass-substitute doublet.lens --dry-run # 每个模型玻璃面列出最近的几个牌号,最近的那个换上
lb glass-substitute doublet.lens --reoptimize # 换完再对剩下的变量做一次局部优化(换玻璃后焦距、色差会偏一点)
lb glass-substitute doublet.lens 2 --catalogs all # 只换第 2 面,从全部厂商里找- 距离:√((Δnd / 0.01)² + Δvd²)——nd 差 0.01 与 vd 差 1 等价(玻璃图上两个方向的跨度相当)。只看 nd、vd,不看价格、 可加工性、部分色散;候选表里挑别的就用
lb set改glass=。 - 候选来自
catalogs语句里的厂商(--catalogs all用全部厂商);同 nd、vd 的牌号(不同厂商的等价玻璃)按目录顺序列出。 - 换上后去掉该面的
nd@/vd@变量;--reoptimize用剩下的变量和同一个评价函数再优化一次,结果和替换合成一个补丁。
看评价函数
lb merit my.lens列出每行的值、权重和贡献百分比(哪一行在主导评价函数一目了然),# 列是 op 行序号。
优化
lb optimize my.lens --dry-run # 先看:diff + 评价函数变化 + 每个变量前后值
lb optimize my.lens # 确认后写入(可撤销)
lb optimize my.lens --mode hammer --seconds 60 --seed 1 # 全局:限时随机扰动 + 局部优化local是阻尼最小二乘(Levenberg-Marquardt,即 Zemax 的 DLS)。残差数少于变量数时自动换成信赖域法。半径按曲率优化,所以平面也能被优化成曲面;非球面系数的量级差异由算法自动缩放。
优化结果以补丁形式提出:只改被优化的那几个数,注释和排版不动;
--dry-run不写文件。一个格子只有一种求解类型:挂了 solve/pickup 的参数不能再当变量(用
lb set设 solve 时会自动取消变量)。MTF / Strehl 操作数(MTFT / MTFS / MTFA / STRH)的取样:优化开始时按起始设计自动判定一次(同
lb mtf的自动取样), 整个优化不变——每次求值都重判的话,设计一变取样就可能跳档,评价函数会出现台阶。判定封顶:优化可以粗略,最终结论看lb mtf/lb verify,它们按自动取样复算。上限默认 256,被封顶时结果里会提示;要更准:lb settings optimize_mtf_sampling_cap 512(用户级设置,存在%APPDATA%\LensBench\settings.json,CLI、MCP、桌面端共用; 在系统终端里运行——桌面端的偏好设置里暂时还没有这一项),或只这一次lb optimize my.lens --mtf-sampling 512(桌面端的命令行里是optimize --mtf-sampling 512)。优先级:--mtf-sampling> 用户设置 > 默认 256。lb merit用同一规则, 结果与证据记的是实际用的取样。在内核里求值:评价函数的整次求值(施加变量、pickup / solve、追迹、各操作数、残差)能在内核里做的就在内核里做,比逐项 在 Python 里算快 2–18 倍,结果逐位相同;默认点列评价函数、一阶量(EFFL、TOTR、ISFN、PMAG、PIMH、LACL、AXCL……)、几何约束、 Seidel(SPHA、COMA、ASTI、FCUR、DIST)、材料约束、边缘厚度与总厚度(ETVA、MNEG、MNEA、TTVA……,每次求值照样重算半口径)、 TRAX / TRAY / TRAR、RSCE / RSCH、某面上的实际光线(REAX / REAY / REAR / RAGX… / REAA… / RANG)已经进了内核,其余操作数 (STRH、ZERN……;EXPR 只有名字与常数的加减乘除、abs、sqrt、min / max 进了内核;MTFT / MTFS / MTFA、畸变 DIMX / DISG、 波前的默认评价函数与 RWCE / RWCH / OPDC、RAID / RAED、RELI、WFNO、EXPP / EXPD 已进内核) 与坐标断点、偏心 / 倾斜的系统暂时整次走 Python(同轴的反射镜系统、各组态都能走内核的多重结构、模型玻璃的 nd / vd 变量(Buchdahl 色散)、衍射面的相位项变量整次在内核里算)。优化结束时的结果里写明走了哪条路 (「评价函数:整次在内核里算」或「整次走 Python(原因)」)。
多进程:每一轮的有限差分雅可比(n 个变量就是 n 次求值)分给本进程和几个工作进程同时算,结果与单进程逐位相同 (同样的点、同样的次序,求值次数也一样)。默认一共 min(8, ⌈n / 2⌉, 物理核数, 可用内存一半能放下的) 个进程——每个进程 约分 2 个点,再多就是派发和等最慢那个的开销。变量少、一次求值很快、或估计整个优化很短时单进程;工作进程在后台起, 就绪之前照常在本进程算,崩了、卡住的那份也在本进程补算。
--processes 1强制单进程,--processes 6指定一共 6 个。 优化结束时的结果里写明用了几个进程。打包版暂时单进程。参照机(EPYC 9554 工作站,128 物理核)实测,局部优化 60 轮,单位秒;「就绪后」是工作进程都起来以后才开始计时, 「实际」是真实用法(不等工作进程,含起进程,这台机器上同时起 8 个进程要 1.5–2 s):
设计(变量数) 1 个进程 3 个 5 个 7 个 9 个 13 个 双高斯(8),就绪后 0.26 0.18 0.16 0.17 0.16 广角(10),就绪后 0.41 0.25 0.20 0.20 0.21 双高斯(13),就绪后 1.89 0.94 0.70 0.58 0.60 双高斯(8),实际 0.27 0.28 0.29 0.30 双高斯(13),实际 1.93 1.30 1.38 1.39 1.55 双高斯(18),实际 14.85 6.55 5.25 4.76 4.39 4.78 不到半秒的优化在工作进程就绪前就结束了(只多 0.01–0.03 s),几秒以上的才见效;hammer 按时间算,同样 6 秒里双高斯(8) 的求值次数 8948(1 个进程)→ 19772(4 个)→ 22557(9 个)。
编辑评价函数
lb op-add my.lens "EFFL target=50 weight=10"
lb op-set my.lens 2 "TOTR <= 60"
lb op-delete my.lens 3
lb default-merit my.lens "spot centroid rings=4 glass_min=1"
lb default-merit my.lens off
lb mf-export my.lens triplet.mf # 存成 .mf 供别的镜头复用
lb mf-import other.lens triplet.mf # 替换 other.lens 的评价函数
lb mf-import other.lens zemax.MF # 也认 Zemax 的文本评价函数(见下)从 Zemax 带过来。 老版本 zmx 里内嵌着评价函数,lb import-zmx 会一起导入;Zemax 的文本 .MF 用 lb mf-import。 支持的操作数按 Zemax 的 Int1 / Int2 / Data 含义换成 op 行(面号、波长、Hx Hy Px Py、环数、频率;长度目标随镜头单位换算), 优化向导生成的默认评价函数(DMFS 之后的 TRAR / TRAC / OPDX 逐光线行)整块换成一行 default,类型、参考、环数、臂数取自它的注释。 组合与界限行改写成 .lens 的写法:CONS / SUMM / DIFF / PROD / DIVI / ABSO / SQRT / OSUM / MAXX / MINN → op EXPR(被引用的行加 name=z<行号>);OPLT / OPGT / OPVA 指向只看值(权重 0)的行时,那一行直接改成不等式(如 op TTHI 1..24 <= 285),否则成一行 op EXPR … <= 目标。多重结构操作数、还不支持的操作数(以及组态号超出组态数的行)不导入,提示里按名字计数列出; 英寸单位文件里的组合行(CONS / SUMM …)也不导入(常数的量纲不明)。优化向导那一块的权重和换算成 default 的 weight= (作者改过权重的也能对上)。
多重结构(变焦)。 设计写了 config / mc(见 .lens 指南「多重结构」)时,评价函数与优化覆盖所有组态: 不写 config= 的 op 行每个组态都算,op EFFL target=70 config=2 只算组态 2;各组态的残差按组态权重(mc weight)合成一个 评价函数(与 Zemax 多组态的评价函数相同的归一化)。变量:没由 mc 控制的(如 var R1..R6)各组态共用;由 mc 控制又标了 var 的(如 mc T5 … + var T5)每个组态各是一个变量——变焦间隔就这样优化。结果写回 surf 行(共用变量、组态 1 的值)与 mc 行(各组态的值)。zmx 里评价函数的 CONF k 行导入成 config=k,导出时按组态分块。
带回 Zemax。 lb export-zmx 把评价函数一起写进 zmx:op 行按同样的参数含义写回;比较方式与操作数自己的不同时(如 op TTHI 1..4 <= 25) 写成权重 0 的那一行加一行 OPLT / OPGT;default 写成 DMFS 加一行注释(类型、参考、环数、臂数)和厚度边界行——Zemax 不会据此自动生成 逐光线行,打开后用优化向导按注释重建。op EXPR 在 Zemax 里没有对应(要 ZPL 宏);STRH、DISG、RGLA 的 Zemax 参数写法 还没核实;带面号范围的边界操作数(MNCT 1..6 等)只有比较方式与 Zemax 相同时才写出。这些都不导出,提示里列出。导出了的行再导入逐行相同。
和 AI 一起优化
在接了 MCP 的 AI 客户端里(Claude Code、Claude Desktop、Cursor……,见 原生 AI),AI 可以:merit 看哪一行贡献最大 → 调整权重或加约束(op_add)→ optimize(dry_run=true) 给你看结果(预计超过 20 秒时加 background=true,用 job_status / job_apply 跟进) → 你确认后正式执行 → 有验收规格时跑 verify 作结论,再用 spot / mtf 前后对比,报数附证据 id。 客户端里的提示模板「诊断并优化」(diagnose_optimize)就是这套流程。