Merit Function and Optimization
The merit function (Zemax's Merit Function Editor) is written directly in the .lens file, versioned and diffed together with the lens data. For all operands see 操作数参考(评价函数).
Writing a merit function
var R1..R6 T6
default spot centroid rings=3 arms=6 glass_min=1 air_min=0.5 # default merit function + thickness bounds
op EFFL target=50 weight=10 # equality target
op TOTR <= 62 # inequality: counted only when violated
op MNCT 1..6 >= 0.8 # range constraint: same as Zemax, one value per line (target + sum of the excess over all surfaces)
op RSCE ring=3 Hy=1 target=5um # the target can carry a unit (operand values use the lens unit, mm)
op EFFL name=f weight=0 # weight=0: computed only, not part of the optimization
op EPDI name=epd weight=0
op EXPR expr="f/epd" target=5 # an expression refers to the name of other linesVariables can carry lower and upper bounds (Zemax variables have no bounds and rely on operands such as MNCT / MXCT for constraints; LensBench supports both):
var R1 in 10..100 R2..R4 in -80..-15 T6 in 30..50mm K1- Written after a reference (or range):
in lower..upper, applying to every variable in that item;lb vary x.lens T6 in 38..45updates the bounds of a variable that already exists. - The nd and vd of a model glass
glass=model(nd, vd)can be variables:var nd@3 vd@3(see "Glass" below). - R and T are lengths (mm, units allowed); K and asphere coefficients are raw values. The two ends of a radius bound must have the same sign — curvature is discontinuous across 0; to allow the sign to flip, give no bounds.
- With bounds, local optimization switches to a bounded trust-region method (the Levenberg-Marquardt of DLS does not support bounds), and R is bounded in terms of curvature; the perturbed points of global optimization (hammer) are also clamped inside the bounds.
- If the starting value is outside the bounds: it is pulled back inside first and then optimized, and the result is still written back (at the very least the design is pulled back inside the bounds), with a note in the result; variables that end up sitting on a bound are reported separately ("relaxing the bounds might do better").
Key points:
Operand names follow Zemax; parameters use names in place of Int1/Int2/Hx/Hy/Px/Py. For operands that need a surface number, the surface number (or range) can be written as the first parameter.
Wavelength and field indices start at 1; for image-quality operands
wave=0means polychromatic (weighted by wavelength weights).default spotexpands into per-ray transverse residuals (Gaussian quadrature of rings × arms, over all fields × wavelengths with weights), referenced to the centroid or the chief ray;default wavegives per-ray OPD residuals. With only the default merit function, the merit function value is approximately the weighted RMS spot radius (mm).For systems with a central obscuration (Cassegrain, etc.), add
obscuration=ε(obscuration radius / pupil radius, same as the Obscuration of Zemax's optimization wizard): the quadrature rings are laid out in the annulus ρ ∈ [ε, 1]. If omitted, the rings fill the entire pupil, rays landing on the obscuration are penalized as "ray failures", and the merit function is swamped by the penalty.The merit function value is normalized as in Zemax:
where
, , and are the weight, current value, and target value of the -th term (everyopline and every term expanded from the default merit function counts as one term).The total weight of the default merit function is the same as in Zemax: the sum of the expanded weights = π × mean field weight × mean wavelength weight (times
weight=ofdefault), so the weight of the D rows inlb meritis not 1. With only the default merit function this factor cancels; when mixed withoplines, the relative weight of the two matches Zemax.Boundary operands with a surface range (
MNCT…MXEA,MNIN…MXAB) follow Zemax: one line computes a single value = target + sum of the excess over all surfaces (equal to the target when nothing is violated), with one residual and the weight counted once; to see the per-surface values use the expressionMNCT(1:6)orlb calc.For the syntax of
EXPRsee the next section, "Expressions (MATLAB style)".
Expressions (MATLAB style)
op EXPR expr="…" takes a small vectorized expression language modeled on MATLAB; the result can be a single number or a vector (a vector gets a residual per element, and with <= / >= only the elements that violate are counted).
op EXPR expr="EFFL(wave=1) - EFFL(wave=3)" target=0 # call operands directly, no need for a separate weight=0 line
op EXPR expr="max(RSCE(ring=3, Hy=0:0.25:1))" <= 0.01 # RSCE over fields 0, 0.25 … 1, take the maximum
op EXPR expr="RSCE(ring=3, Hy=[0 0.7 1])" <= 0.01 # vector: one residual for each of the three fields
op EXPR expr="max(CTVA(1:6)) - min(CTVA(1:6))" <= 4 # range of the center thicknesses
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 # refer to a line with name= (the old syntax still works)| Syntax | Examples |
|---|---|
| Numbers, constants | 1, 2.5, 1e-3, pi |
| Vectors | [0 0.5 1], [0, 0.5, 1]; [1 -2] has two elements (MATLAB rule: a minus sign preceded by a space and not followed by one starts a new element), [1 - 2] is −1; elements can be vectors (concatenated) |
| Ranges | a:b (step 1), a:step:b; linspace(a, b, n) |
| Operators | + - * / ^, also ** and .* ./ .^; always element-wise, scalars broadcast, two vectors must have equal length; the exponent of a power must be a number (x^2, h.^0.5) |
| Math functions | abs sqrt exp log log10 sin cos tan asin acos atan atan2 hypot floor ceil round sign clip deg2rad rad2deg, and the degree versions sind cosd tand asind acosd atand |
| Reductions | max min (one argument takes the maximum / minimum, several are element-wise), sum mean rms std numel first last (std uses N − 1 as in MATLAB) |
| Names | the name= of an op line |
| Operands | uppercase name with parentheses: EFFL(), RSCE(ring=3, Hy=h), CTVA(3), REAY(5, Hy=1, Py=1) — parameter names follow the operand reference, and the first positional argument is the same as on an op line; a vector parameter is expanded element by element and the result is a vector; surface-range parameters (MNCT etc.) are written 1:6 and return the value for each surface (not the single combined number of an op line) |
Units: operands use their own units for values (length mm, wavefront λ, angle °, distortion %), and numbers in expressions carry no units.
Intermediate quantities with let
let name = expression defines an intermediate quantity (a number or a vector) that other let statements and op EXPR lines can refer to; a let can refer to the name= of op lines and to other let statements. It can be placed anywhere: like a spreadsheet, evaluation follows dependencies (what is used is computed first), independent of the order of writing — the two spellings below give bit-for-bit identical results, and the same evidence id for the merit function:
let h = 0:0.25:1 # fields
let r = RSCE(ring=3, Hy=h) # RMS spot size of each field (mm)
op EXPR expr="max(r)" <= 0.01
op EXPR expr="max(r) - min(r)" <= 0.004 # uniformity across fieldslet r = RSCE(ring=3, Hy=h) # writing the one that uses h first also works
let h = 0:0.25:1Circular references are not allowed: let a = b + 1 with let b = 2 * a (or let a = a + 1) raises E-LET-CYCLE, and the diagnostic lists the names and line numbers on the cycle.
lb merit and the desktop “评价函数” (Merit Function) tab (at the end of the let line) list the value of each let, or the reason it failed (in the order written). To edit with commands: lb let-set x.lens h "[0 0.7 1]", lb let-delete x.lens h (rejected while it is still used by another let or an op EXPR line, and the lines that use it are listed — those written before or after it both count).
One-off calculations with lb calc
Like typing a line into the MATLAB command window, without changing the design:
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))"It can refer to the name= of op lines and to let. The result carries an evidence id (the evidence key includes the expression and the merit function: change a let and the evidence id of the same expression changes too). The command line at the bottom of the desktop app and the “终端” (Terminal) tab on the right accept the same input.
To keep a file shared by someone else from exhausting resources with an expression: an expression is limited to 400 characters, 12 levels of parentheses, and 160 nodes; the exponent of a power must be a number within ±16, numbers may not exceed 1e12 in absolute value, vectors may not exceed 1001 elements, and one expression may not trigger more than 512 operand evaluations after expansion. Exceeding a limit raises E-EXPR-LIMIT (a line that exceeds a limit during evaluation is penalized as a failure). Expressions execute no code and read or write no files.
Glass: optimize with model glass first
The usual Zemax practice: write the surface whose glass is to be changed as a model glass, let nd and vd follow the optimization, then swap in the closest catalog glass.
surf 2 R=-25 T=2 glass=model(1.62, 45)
var nd@2 vd@2 # no bounds given: nd 1.40–2.10, vd 15–100
op AXCL target=0 weight=10 # for example: remove axial chromatic aberration- Each optimization step swaps the material according to the new (nd, vd) (the dispersion formula of a model glass is the same as
model(…)); when light is reflected off the back of a mirror inside the glass (a Mangin mirror), the mirror follows the swap. - After optimization, nd and vd are written back together as
glass=model(…)(nd to 6 decimals, vd to 4). - The default range without bounds is that of common optical glasses — Zemax's model glass itself is unrestricted and relies on the RGLA operand to pull it back into the glass region; without bounds the optimizer would push nd to values that do not exist. When a value stops at an end of the default range, the result says so.
- Companion operands (same names as in Zemax):
op RGLA weight=0.1(how far a model glass is from the nearest catalog glass, target 0 — keeps the optimization near real glasses, so that less changes in the later substitution),op MNIN 1..6 >= 1.5/MXIN/MNAB/MXAB(constrain the range of nd and vd; with a surface range they work as in Zemax, one value per line = target + sum of the excess over all surfaces), andop INDX 3 wave=2(the index of the medium after a surface at a given wavelength). For a model glass nd and vd take the written values; for a catalog glass they are computed from the d / F / C line indices.
After optimization, swap in catalog glass:
lb glass-substitute doublet.lens --dry-run # for each model-glass surface list the nearest few grades, and substitute the nearest
lb glass-substitute doublet.lens --reoptimize # after substituting, run one more local optimization over the remaining variables (focal length and chromatic aberration shift slightly after a glass change)
lb glass-substitute doublet.lens 2 --catalogs all # substitute only surface 2, searching all manufacturers- Distance: √((Δnd / 0.01)² + Δvd²) — an nd difference of 0.01 is equivalent to a vd difference of 1 (the spans of the two directions on the glass map are comparable). Only nd and vd are considered, not price, workability, or partial dispersion; to pick another one from the candidate table, use
lb setto changeglass=. - Candidates come from the manufacturers in the
catalogsstatement (--catalogs alluses all manufacturers); grades with the same nd and vd (equivalent glasses from different manufacturers) are listed in catalog order. - After substitution the
nd@/vd@variables of that surface are removed;--reoptimizeoptimizes once more with the remaining variables and the same merit function, and the result and the substitution are combined into one patch. - The candidate table shows the supply status; if the substituted grade is obsolete you get a note (the choice is still by distance; pick another with
lb set, or follow up withglass-replacebelow).
Glass: switching to another manufacturer's equivalent grades
For a design that already uses catalog glasses, swap them all for the equivalent grades of another manufacturer (CDGM by default) and see how image quality, price and weight change:
lb glass-replace cooke.lens --dry-run # every catalog glass → its CDGM equivalent: preview the patch and the before / after
lb glass-replace cooke.lens 3 --top 5 --dry-run # surface 3 only, list 5 candidates
lb glass-replace cooke.lens --vendor OHARA --refocus # switch to OHARA and refocus afterwards- How a grade is chosen: nearest by the (nd, vd) distance √((Δnd / 0.01)² + Δvd²) (same as
glass-substitute); candidates within 0.1 of the nearest count as equally close (inside the melt tolerances), and among them the order is: supply status (preferred first), difference in ΔPgF, in dn/dT, in thermal expansion, then relative price. Obsolete grades are skipped by default (the candidate table says which one was skipped and how close it was);--obsoletekeeps them. Surfaces whose nearest candidate is farther than--max-distance(default 1) are left alone and the nearest one is listed. - Which surfaces: every catalog-glass surface (
surfacefor just one); grades already current in the target catalog stay, obsolete ones are replaced by a current grade of the same manufacturer (CDGM F4 → H-F4); model glasses (useglass-substitute), built-in crystals / plastics,glass=pickup(k)(follows its source) and glasses given bymcrows are not touched. - The patch only changes the
glass=word — comments, layout and other parameters are untouched;--refocusalso changes the thickness before the image (same as the study steprefocus),--reoptimizere-optimizes with the design's variables and merit function (same asoptimize). - Before / after (standard sampling,
viewis not read): polychromatic RMS spot per field (6 rings, chief-ray reference, same aslb spotand verification), RMS wavefront (65×65, tilt removed, polychromatic), MTF (automatic sampling; without--freq, 1/8 of the original design's cut-off rounded to 5), mass of each element and in total (same aslb weight), relative price. Both versions get an evidence id, so numbers quoted in a report can be checked withlb check-report. - Price: only the OD line of a vendor AGF carries a relative price (relative to that vendor's own reference glass — SCHOTT N-BK7, CDGM H-K9L — so prices of different vendors are not comparable; across vendors treat it as a hint). The bundled material library has no prices: add an
agfline pointing at a vendor AGF or the Zemax Glasscat folder, and the target grades, prices, densities and supply status all come from that catalog. Where the supply status comes from is described in Orchestration, "Weight and material data".
Example: the Cooke triplet (examples/cooke_triplet.lens, SCHOTT N-SK16 / F2) switched to CDGM becomes N-SK16 → H-ZK9B, F2 → H-F4 (the closer F4 is obsolete and skipped); the RMS spot of every field changes by at most 0.061 µm, MTF at 40 /mm by at most 0.011, and the total glass mass goes 3.417 → 3.353 g (H-F4 is lead-free and 26 % lighter).
Inspecting the merit function
lb merit my.lensIt lists the value, weight, and percentage contribution of each row (which row dominates the merit function is obvious at a glance); the # column is the op line number.
Optimization
lb optimize my.lens --dry-run # look first: diff + merit function change + before/after value of each variable
lb optimize my.lens # write after confirming (can be undone)
lb optimize my.lens --mode hammer --seconds 60 --seed 1 # global: time-limited random perturbation + local optimizationlocalis damped least squares (Levenberg-Marquardt, i.e. Zemax's DLS). When there are fewer residuals than variables it automatically switches to a trust-region method.Radii are optimized as curvatures, so even a plane can be optimized into a curved surface; the algorithm scales asphere coefficients of very different magnitudes automatically.
The optimization result is proposed as a patch: only the numbers that were optimized change, with comments and layout untouched;
--dry-runwrites no file.A cell has only one kind of solve: a parameter carrying a solve/pickup can no longer be a variable (setting a solve with
lb setclears the variable automatically).Sampling of the MTF / Strehl operands (MTFT / MTFS / MTFA / STRH): decided automatically once at the start of the optimization from the starting design (same as the automatic sampling of
lb mtf), and unchanged throughout the optimization — if it were re-decided at every evaluation, the sampling could jump levels as the design changes and the merit function would show steps. The decision is capped: the optimization may be coarse, and the final verdict comes fromlb mtf/lb verify, which recompute with automatic sampling. The cap defaults to 256 and the result says so when it is hit; for more accuracy:lb settings optimize_mtf_sampling_cap 512(a user-level setting stored in%APPDATA%\LensBench\settings.json, shared by the CLI, MCP, and the desktop app; run it in a system terminal — the desktop app's preferences do not have this item yet), or for this one run onlylb optimize my.lens --mtf-sampling 512(in the desktop app's command line it isoptimize --mtf-sampling 512). Priority:--mtf-sampling> user setting > default 256.lb meritfollows the same rule, and the result and the evidence record the sampling actually used.Evaluation in the kernel: whatever part of a whole merit function evaluation (applying variables, pickup / solve, tracing, operands, residuals) can be done in the kernel is done there, 2–18 times faster than evaluating item by item in Python, with bit-for-bit identical results. The default spot merit function, first-order quantities (EFFL, TOTR, ISFN, PMAG, PIMH, LACL, AXCL …), geometric constraints, Seidel (SPHA, COMA, ASTI, FCUR, DIST), material constraints, edge and total thickness (ETVA, MNEG, MNEA, TTVA …, with the semi-diameter still recomputed at each evaluation), TRAX / TRAY / TRAR, RSCE / RSCH, and real rays at a surface (REAX / REAY / REAR / RAGX… / REAA… / RANG) are already in the kernel; the remaining operands (STRH, and the newly added paraxial rays PARX / PARY / PARA / PARB, centroids CENX / CENY, TRAC, OSCD, DISC, geometric MTF GMTA, and the semi-diameter constraints MNSD / MXSD; in EXPR only the four arithmetic operations on names and constants, abs, sqrt, and min / max are in the kernel; MTFT / MTFS / MTFA, ZERN, distortion DIMX / DISG, the wavefront default merit function and RWCE / RWCH / OPDC, RAID / RAED, RELI, WFNO, and EXPP / EXPD are already in the kernel) and systems with coordinate breaks or decentered / tilted elements still run entirely in Python for now (coaxial mirror systems, multi-configuration designs in which every configuration can run in the kernel, nd / vd variables of model glass (Buchdahl dispersion), and phase-term variables of diffractive surfaces are evaluated entirely in the kernel). The result at the end of the optimization states which path was taken ("merit function: evaluated entirely in the kernel" or "entirely in Python (reason)").
Multiple processes: the finite-difference Jacobian of each iteration (n variables means n evaluations) is split between this process and several worker processes, and the result is bit-for-bit identical to a single process (same points, same order, same number of evaluations). By default the total is min(8, ⌈n / 2⌉, physical cores, the number that fit in half of the available memory) processes — each process takes about 2 points, and more only adds dispatch overhead and waiting for the slowest one. With few variables, very fast evaluations, or an estimated short optimization, a single process is used; worker processes start in the background, the work is done in this process as usual until they are ready, and a share that crashed or hung is also redone in this process.
--processes 1forces a single process, and--processes 6sets 6 in total. The result at the end of the optimization states how many processes were used. The packaged build is single-process for now.Measured on the reference machine (EPYC 9554 workstation, 128 physical cores), local optimization of 60 iterations, in seconds; "once ready" timing starts after all worker processes are up, and "actual" is real use (without waiting for the workers, including starting the processes, which takes 1.5–2 s for 8 processes at once on this machine):
Design (variables) 1 process 3 5 7 9 13 Double Gauss (8), once ready 0.26 0.18 0.16 0.17 0.16 Wide angle (10), once ready 0.41 0.25 0.20 0.20 0.21 Double Gauss (13), once ready 1.89 0.94 0.70 0.58 0.60 Double Gauss (8), actual 0.27 0.28 0.29 0.30 Double Gauss (13), actual 1.93 1.30 1.38 1.39 1.55 Double Gauss (18), actual 14.85 6.55 5.25 4.76 4.39 4.78 An optimization of under half a second finishes before the worker processes are ready (costing only 0.01–0.03 s extra), and it only pays off from a few seconds up; hammer is time-based, and in the same 6 seconds the number of Double Gauss (8) evaluations goes 8948 (1 process) → 19772 (4) → 22557 (9).
Improvement exploration (lb improve)
"Can this design get any better?" — optimizing the radii alone often hits the ceiling quickly (radii and focus are already optimal; neither local nor global optimization brings the merit function down). lb improve tries several more freedoms in order without changing the system specification; each step starts from the best version of the previous one, and at the end it honestly reports the numbers of every step:
lb improve my.lens --dry-run # the default four steps, 120 s budget; look at the verdict and the diff first
lb improve my.lens --strategies radii,thickness --seconds 30
lb improve my.lens --seed 1 --glass-candidates 5 # another seed, more glass candidates per element| Step | What is freed |
|---|---|
1 radii | the radii of curved lens surfaces (planes stay planes) + the thickness in front of the image (focus); asphere / conic variables the design itself marks are freed too |
2 thickness | plus the air spaces and lens center thicknesses |
3 glass | glass trials element by element: the N catalog glasses nearest to the original in the design's catalogs (default 3; same distance as glass-substitute), at least nd 0.01 or vd 1 away from the original and from each other (anything closer is another vendor's equivalent glass and would not help), discontinued glasses skipped; every swap is re-optimized locally with the previous step's freedoms and kept only if it lowers the merit function by at least another 1 % |
4 hammer | a short global search (same as optimize --mode hammer, given as a number of rounds --hammer-rounds, default 40) |
Target and constraints
- The design's own merit function is used; without one, the default merit function
default spot centroid rings=3 arms=6(the default of Zemax's Optimization Wizard). Comparisons, the threshold, and the report all use this merit function. - The system specification is not changed: aperture, fields, and wavelengths stay (only R / T / glass change); when the merit function has no
EFFLrow, a row holding the starting focal length is added, and every version is checked for |ΔEFFL| ≤ 0.1 %. - Constraints enter only the optimization and are not written into the design: total length
TOTR ≤ start × (1 + 5 %)(--ttl-growth); the edge thickness of every lensMNEG ≥ 0.6 mmand the edge of every air spaceMNEA ≥ 0.1 mm(check's thresholds are a 0.5 mm lens edge and no air-space collision; the operands keep a 0.1 mm margin because the two measure edge thickness differently — the operands, like Zemax, at each surface's own semi-diameter, check at the larger semi-diameter of the two; values already smaller at the start do not get smaller); center thicknesses use variable bounds: glass [min(start, 1 mm), 2 × start + 2 mm], air [min(start, 0.1 mm), 2 × start + 5 mm], the focus space [0.5, 1.5] × start. When a local optimization runs into check's clearance test, the edge bound of that space is tightened (by the shortfall) and the step is rerun; the result says what was tightened. - Every version must pass the gates to count: it builds; the focal length is unchanged; the total length is not exceeded; manufacturability (clearance violations, lens edges < 0.5 mm) gains no new problems compared with the start; probe rays that reached the image at the start (each field's chief ray + the four marginal rays up / down / left / right) still do; with
specrows, no spec that passed at the start may fail.
An honest verdict: only when the best version's merit function is at least 2 % lower than the start, or a spec goes from failing to passing, does it count as "found a better one" — the patch goes through --dry-run / apply / undo as usual; otherwise the verdict is "nothing clearly better found", the design is not changed, and the table still lists what every step tried and by how much it was lower. When the merit function went down but the worst-field RMS spot got larger, or the worst MTF at some frequency got lower, an extra line says so.
Report: for every step the merit function, the change from the start, the worst-field RMS spot (µm, the defaults of lb spot), the maximum RMS wavefront over the field (λ, same as lb rms-field --data wavefront), and the worst MTF at 1/16, 1/8, 1/4 of the cutoff (rounded, the defaults of lb mtf-vs-field); every glass trial (original, candidate, distance, merit function, kept or not and why); which numbers the best version changed; the evidence ids of the start and of the best version — the same ids as running lb spot / lb rms-field --data wavefront / lb mtf directly on that version, checkable with lb evidence <id>.
Reproducibility and budget: the same design, the same parameters, and the same --seed give bit-for-bit identical results as long as the time budget is not hit. --seconds (default 120) is the total budget: when it is hit the run wraps up early and gives the best so far, the steps not done are marked "not done (time budget used up)", and the result depends on the machine's speed. As a long task: in MCP add background=true and follow with job_status / job_result / job_apply; the desktop app likewise shows progress and can cancel (a cancelled run gives the best so far and is not applied automatically). Multi-configuration designs and designs without a first-order model (decentered / tilted) are not supported yet.
Tiers: the verdict, the numbers of every step, and the evidence ids are free; opening the improved version (the patch's diff and applying it) starts at Personal Basic — with a lower tier the run completes and gives the verdict and numbers but no patch, with the notice I-TIER-PATCH (nothing is blocked while login enforcement is off).
Example (the Cooke triplet examples/cooke_triplet.lens, no merit function, so the default one; measured 10-20):
| Step | Merit function | vs start | Worst RMS µm | Worst MTF@25/50/100 |
|---|---|---|---|---|
| Start | 0.011554 | — | 17.73 | 0.229 / 0.054 / 0.005 |
| 1 radii only (+ focus) | 0.011475 | −0.7 % | 17.57 | 0.299 / 0.089 / 0.015 |
| 2 plus air spaces and center thicknesses | 0.011049 | −4.4 % | 17.18 | 0.358 / 0.102 / 0.010 |
| 3 plus glass trials | 0.010064 | −12.9 % | 16.06 | 0.350 / 0.094 / 0.007 |
| 4 global search | 0.008608 | −25.5 % | 12.29 | 0.433 / 0.119 / 0.035 |
Freeing only the radii lowers it by just 0.7 % (below the threshold: with --strategies radii alone the verdict is "nothing clearly better found"); only after freeing the thicknesses and trying glasses does it clearly improve. The focal length stays at 50.0214 mm, and the total length goes from 60.18 to 54.36 mm.
Function API (reused by the health check and the overnight designer): lensbench.explore.explore(evaluator_factory, text, strategies, budget, seed, …) returns an ExploreResult (the start, the best version after every step, the verdict, the changed numbers, spec changes, whether the budget was hit …); lb improve only adds evidence registration and the report text around it.
Editing the merit function
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 # save as .mf for reuse with other lenses
lb mf-import other.lens triplet.mf # replace the merit function of other.lens
lb mf-import other.lens zemax.MF # Zemax text merit functions are accepted too (see below)Bringing it over from Zemax. Older zmx files embed the merit function, and lb import-zmx imports it along with the lens; use lb mf-import for Zemax's text .MF. Supported operands are converted into op lines according to the meaning of Zemax's Int1 / Int2 / Data (surface numbers, wavelengths, Hx Hy Px Py, number of rings, frequencies; length targets are converted with the lens unit), and the default merit function generated by the optimization wizard (the per-ray TRAR / TRAC / OPDX rows after DMFS) is replaced as a whole by a single default line, with type, reference, rings, and arms taken from its comments. Combination and bound rows are rewritten in .lens style: CONS / SUMM / DIFF / PROD / DIVI / ABSO / SQRT / OSUM / MAXX / MINN → op EXPR (the referenced rows get name=z<row-number>); when OPLT / OPGT / OPVA point at a row that only reads a value (weight 0), that row is turned directly into an inequality (such as op TTHI 1..24 <= 285), otherwise it becomes a line of op EXPR … <= target. Multi-configuration operands and operands not yet supported (and rows whose configuration number exceeds the number of configurations) are not imported, and the messages list them counted by name; combination rows in files with inch units (CONS / SUMM …) are not imported either (the dimension of the constant is unclear). The sum of the weights of the optimization wizard block is converted to weight= of default (so that weights edited by the author also match).
Multi-configuration (zoom). When a design has config / mc (see "Multi-configuration (zoom)" in .lens Guide), the merit function and optimization cover all configurations: op lines without config= are evaluated in every configuration, and op EFFL target=70 config=2 only in configuration 2; the residuals of the configurations are combined into one merit function with the configuration weights (mc weight) (the same normalization as the multi-configuration merit function in Zemax). Variables: those not controlled by mc (such as var R1..R6) are shared across configurations; those controlled by mc and flagged var (such as mc T5 … + var T5) are one variable per configuration — this is how zoom spacings are optimized. The result is written back to the surf lines (shared variables, values of configuration 1) and the mc lines (values of each configuration). In zmx, the CONF k rows of the merit function are imported as config=k, and on export they are grouped into blocks by configuration.
Taking it back to Zemax. lb export-zmx writes the merit function into the zmx too: op lines are written back with the same parameter meanings; when the comparison type differs from the operand's own (such as op TTHI 1..4 <= 25), it is written as a weight-0 row plus an OPLT / OPGT row; default is written as DMFS plus a comment line (type, reference, rings, arms) and thickness-bound rows — Zemax will not generate the per-ray rows from this automatically, so after opening the file, rebuild them with the optimization wizard according to the comment. op EXPR has no counterpart in Zemax (it would need a ZPL macro); the Zemax parameter layouts of STRH, DISG, and RGLA have not been verified yet; boundary operands with a surface range (MNCT 1..6 etc.) are written out only when the comparison type matches Zemax's. None of these are exported, and the messages list them. Exported rows re-import identically row by row. The Zemax parameter layouts of the newly added CENX / CENY, OSCD, DISC, and GMTA were inferred from the manual (PARX…PARB being the same as REAX and TRAC the same as TRAY is certain) and have not yet been checked against files written by Zemax; rows of GMTA with Data3 / Data4 other than 0 are not imported, and rows of CENX / CENY written with Hx / Hy (rather than a field index) are not exported.
Optimizing with AI
In an AI client connected through MCP (Claude Code, Claude Desktop, Cursor …, see Native AI), the AI can: use merit to see which row contributes most → adjust weights or add constraints (op_add) → show you the result with optimize(dry_run=true) (add background=true when it is expected to take over 20 seconds, and follow up with job_status / job_apply) → run it for real once you confirm → when acceptance specs exist, run verify for the verdict, then compare before and after with spot / mtf, quoting numbers with evidence ids. The prompt template “诊断并优化” (Diagnose and optimize) (diagnose_optimize) in the client is exactly this workflow.