Using LensBench from Python
Call the same computation engine from a Python script: open a lens, call operands, evaluate expressions, pull analysis data, make plots, and edit the design. The style is numpy-like. Usually you let the AI write the script: scripts live in scripts/ next to the design file, results (figures, tables) go to reports/, and you browse them in "脚本和产物" (Scripts and Outputs) at the bottom of the browser on the left of the desktop app (see "Scripts and Outputs" in the Desktop App guide).
import numpy as np
from lensbench.workspace import Lens
L = Lens("cooke.lens") # or Lens(text="…")
L.effl() # 50.0214
h = np.linspace(0, 1, 21)
r = L.rsce(ring=3, hy=h) # RMS spot size (mm) at 21 fields, as a numpy array
L.calc("max(RSCE(ring=3, Hy=0:0.25:1))") # same as lb calcRunning a script requires a Python with LensBench installed (a development environment installed from source). The program in the installer is compiled, so for now you cannot import lensbench from your own Python or Jupyter, and the desktop app cannot run scripts yet. That is coming: on first use it will download a runtime (with numpy and the LensBench library) over the network. Until then, have your AI client call the commands directly through MCP (no script needed), or use the command-line lb calc.
Operands are lowercase methods
Every operand (see the operand reference) is a lowercase method of Lens. Parameter names are the same as in the reference and are case-insensitive:
| Form | Description |
|---|---|
L.effl(), L.effl(wave=1) | A single number (float) |
L.ctva(3), L.reay(L.image, hy=1, py=1) | The first positional argument is the same as in an op line (surface number, etc.); L.image is the image surface number |
L.rsce(ring=3, hy=np.linspace(0, 1, 11)) | Array arguments are expanded element by element and an array is returned (multiple array arguments must have equal length) |
L.mnct((1, 6)) | A surface-range argument is written as (lo, hi); returns the value for each surface |
Values use the operand's own units (length in mm, wavefront in λ, angle in °, distortion in %). This is the same implementation as the op lines in a merit function and the calls inside expressions, and the numbers are bit-for-bit identical. The one exception is the boundary operands that take a surface range (mnct and the like): here they return the value for each surface, while in an op line they are combined into a single number following Zemax (target plus the sum of each surface's excess over the limit).
Expressions, analysis data, plots
L.calc("EFFL(wave=1) - EFFL(wave=3)") # expression (syntax: "Expressions" in optimization.md), returns a number or an array
d = L.analysis("mtf") # raw plot data (the same data as the desktop app and lb plot): a dict
d = L.analysis("rms_field", num=11) # options are written as in the view statement
L.plot("spot", out="spot.png") # LensBench plot style and figure frame; returns a result (.evidence is the evidence id)
L.plot("mtf", out="mtf.pdf", max_freq="100/mm") # options with dimensions carry unitsAll design edits go through commands
L.set(3, R=-21.5, T=4) # edit surface 3: touches only these two numbers; comments and layout stay as they are
L.set("IMA", R=-60)
L.run("vary", refs=["R1", "R2"]) # any command: same name as in lb (hyphens become underscores), arguments as in the command reference
L.run("optimize", cycles=20, dry_run=True).text
L.undo()
L.save("cooke_v2.lens")
print(L.text)These are the same commands used by the command line, the desktop app, and the AI: an edit that introduces an error is rejected (a LensError is raised, and its message is the reason and hint given by the command), and edits can be undone.
Comparison with MATLAB
| MATLAB | LensBench (Python) |
|---|---|
h = linspace(0, 1, 21); | h = np.linspace(0, 1, 21) |
r = rsce(lens, h); | r = L.rsce(hy=h) |
max(r), mean(r) | r.max(), r.mean() (numpy) |
plot(h, r*1000) | matplotlib.pyplot.plot(h, r * 1000), or use L.plot(...) for a LensBench-style figure |
Where scripts go and where to see the results
Everything sits next to the design file: scripts/ holds scripts and reports/ holds outputs (files produced by lb plot, the report pack and similar commands are written here by default anyway). Run the script from the directory containing the design file (python scripts/field_rms.py); relative paths in the script are resolved against that directory, and results land in reports/:
# scripts/field_rms.py: RMS spot size per field, written as a table and a plot
import numpy as np
from lensbench.workspace import Lens
L = Lens("cooke_triplet.lens")
h = np.linspace(0, 1, 21)
r = L.rsce(ring=3, hy=h) * 1000 # µm
np.savetxt("reports/field_rms.csv", np.column_stack([h, r]), delimiter=",",
header="hy,rms_um", comments="")
L.plot("spot", out="reports/spot.png")"Scripts and Outputs" at the very bottom of the browser on the left of the desktop app lists the files in these two directories (refreshed every 5 seconds while expanded, so newly written files appear on their own). A single click opens images, tables and text in the "产物" (Outputs) window, and scripts are shown with Python syntax coloring; svg and pdf files open in the default program.
Custom operands written in Python (usable directly in .lens files) will be provided in a later version.