API Cheat Sheet#
Copy-paste snippets for the 20 most common Optiland tasks. New to these concepts? See the Glossary first.
1. Install and import#
pip install optiland
from optiland import optic
import optiland.backend as be
2. Load a sample lens#
from optiland.samples.objectives import CookeTriplet, ReverseTelephoto
lens = CookeTriplet()
lens.info() # print surface table
3. Build a simple singlet from scratch#
from optiland import optic
lens = optic.Optic()
lens.surfaces.add(index=0, radius=float("inf"), thickness=float("inf")) # object at infinity
lens.surfaces.add(index=1, radius=50.0, thickness=5.0, material="N-BK7", is_stop=True)
lens.surfaces.add(index=2, radius=-50.0, thickness=45.0)
lens.surfaces.add(index=3) # image plane
lens.set_aperture(aperture_type="EPD", value=10.0)
lens.fields.set_type("angle")
lens.fields.add(y=0.0)
lens.wavelengths.add(value=0.5876, is_primary=True)
lens.updater.image_solve()
4. Add a surface#
# Insert a surface at index 2 with radius, thickness, and material
lens.surfaces.add(index=2, radius=-435.76, thickness=6.0, material=("F2", "schott"))
5. Set aperture, field, and wavelength#
lens.set_aperture(aperture_type="EPD", value=10.0)
# alternatives: "imageFNO", "objectNA", "float_by_stop_size"
lens.fields.set_type("angle") # or "object_height"
lens.fields.add(y=0.0)
lens.fields.add(y=14.0)
lens.fields.add(y=20.0)
lens.wavelengths.add(value=0.4861) # F-line
lens.wavelengths.add(value=0.5876, is_primary=True) # d-line
lens.wavelengths.add(value=0.6563) # C-line
6. Switch backend (NumPy ↔ PyTorch)#
import optiland.backend as be
be.set_backend("torch") # enable PyTorch (autograd, GPU)
be.set_backend("numpy") # revert to NumPy (default)
# GPU and precision (PyTorch only)
be.set_device("cuda")
be.set_precision("float64")
7. Draw the lens (2D)#
lens.draw(num_rays=5, distribution="line_y")
8. Draw the lens (3D)#
lens.draw3D(num_rays=24, distribution="ring")
9. Trace rays manually#
# Trace a distribution of rays for a given field and wavelength
rays = lens.trace(Hx=0, Hy=0, wavelength=0.5876, num_rays=64, distribution="hexapolar")
print(rays.x, rays.y) # image-plane x, y coordinates
# Trace a single ray defined by normalized field + pupil coordinates
ray = lens.trace_generic(Hx=0, Hy=1, Px=0, Py=0, wavelength=0.5876)
10. Spot diagram#
from optiland.analysis import SpotDiagram
spot = SpotDiagram(lens)
spot.view()
11. Ray fan plot#
from optiland.analysis import RayFan
fan = RayFan(lens)
fan.view()
12. Wavefront / Zernike decomposition#
from optiland.wavefront import Wavefront, ZernikeOPD
wf = Wavefront(lens, field=(0, 0), wavelength="primary")
wf.view()
zfit = ZernikeOPD(lens, field=(0, 0), wavelength="primary", num_terms=37)
zfit.view()
13. PSF and MTF#
from optiland.psf import FFTPSF
from optiland.mtf import FFTMTF
psf = FFTPSF(lens, field=(0, 0), wavelength="primary")
psf.view()
mtf = FFTMTF(lens)
mtf.view()
14. Paraxial properties (EFL, f/#, pupil positions)#
print("EFL:", lens.paraxial.f2())
print("f/#:", lens.paraxial.FNO())
print("EPD:", lens.paraxial.EPD())
print("EPL:", lens.paraxial.EPL()) # entrance pupil location, relative to surface 1
print("XPL:", lens.paraxial.XPL()) # exit pupil location, relative to the image surface
print("Magnification:", lens.paraxial.magnification())
# EFL of a lens group, i.e. surfaces 1 through 2 only
print("Group EFL:", lens.paraxial.f2_range(1, 2))
Note
f2_range(start, end) treats the surface range as a lens group in
isolation, with its own conjugates. It is not a decomposition of the
system’s power, so the group focal lengths of a design will not add back up
to f2(). The underlying 2x2 ray-transfer matrix of a surface range is
available as lens.paraxial.ray_transfer_matrix(start, end).
Note
EPL() is measured relative to the first physical surface (surface 1),
matching the convention of XPL() (relative to the image surface) and the
other first-order quantities. If you need the entrance pupil on the same
axial coordinate as surface positions — e.g. to compare against object or
surface positions — use lens.paraxial.entrance_pupil_axial_position()
(entrance_pupil_z() is a deprecated alias). For the pupil’s real-space
location in a folded system, use
lens.paraxial.entrance_pupil_point_gcs().
15. Define an optimization variable#
from optiland.optimization import OptimizationProblem
problem = OptimizationProblem()
problem.add_variable(lens, "radius", surface_number=1)
problem.add_variable(lens, "thickness", surface_number=1)
16. Define an operand#
input_data = {"optic": lens}
problem.add_operand(operand_type="f2", target=50.0, weight=1, input_data=input_data)
problem.add_operand(operand_type="rms_spot_size", target=0.0, weight=1,
input_data={"optic": lens, "field_index": 1, "wavelength_index": 0,
"distribution": "hexapolar", "num_rays": 100})
17. Run local optimization#
from optiland.optimization import LeastSquares
optimizer = LeastSquares(problem)
result = optimizer.optimize()
print(result)
18. Run global optimization#
from optiland.optimization import DualAnnealing
optimizer = DualAnnealing(problem)
result = optimizer.optimize()
print(result)
19. Save / load system (JSON)#
from optiland.fileio import save_optiland_file, load_optiland_file
save_optiland_file(lens, "my_lens.json")
lens2 = load_optiland_file("my_lens.json")
20. Generate a prescription report#
from optiland.prescription import Prescription
p = Prescription(lens)
p.view() # Rich console output
p.save("prescription.txt") # plain text
p.save("prescription.pdf") # PDF (requires reportlab)
21. Non-sequential (illumination / stray-light)#
from optiland.coordinate_system import CoordinateSystem
from optiland.nonsequential import (
NSQScene, Spectrum,
CollimatedSourceConfig, LensConfig, IrradianceDetectorConfig,
)
scene = NSQScene()
spec = Spectrum.monochromatic(0.55) # wavelengths in µm
scene.add_source("S", CoordinateSystem(z=0),
CollimatedSourceConfig(spectrum=spec, total_flux=1.0,
aperture_radius=10))
scene.add_lens("L", CoordinateSystem(z=50),
LensConfig(r1=100, r2=-100, thickness=5, material="N-BK7",
front_aperture_radius=12.5))
scene.add_detector("D", CoordinateSystem(z=150),
IrradianceDetectorConfig(width=20, height=20))
result = scene.trace(num_rays=1_000_000, max_depth=16, seed=42)
result.detectors["D"].plot() # irradiance map
22. Convert a sequential lens to non-sequential#
from optiland.nonsequential import sequential_to_nonsequential
scene = sequential_to_nonsequential(lens) # for stray-light / ghost analysis
23. Differentiable non-sequential (gradients)#
import optiland.backend as be
be.set_backend("torch") # build scene AFTER this
# ... build scene with torch.Tensor params, then:
result = scene.trace(num_rays=2_000, max_depth=16, seed=42)
loss = ((result.detectors["D"].data - target) ** 2).mean()
loss.backward() # gradients flow to scene params