lens_system — OPTICS design op

• Data kinds: none → table (an op determined by its arguments alone — it takes no image or data input)

• Call: import fullseye as fs; fs.ledger.lens_system(surfaces=None, stop=None, object_mm=inf, wavelength_um=0.58756, index_object=1.0, image_mm=None, field=None, aim='paraxial', vignetting=None, image_R=inf, image_k=0.0, afocal=False) (to call the implementation directly, import raytrace; raytrace.lens_system(surfaces=None, stop=None, object_mm=inf, wavelength_um=0.58756, index_object=1.0, image_mm=None, field=None, aim='paraxial', vignetting=None, image_R=inf, image_k=0.0, afocal=False); from the registry, opsoptics.get("lens_system"))

• GPU: none (CPU reference implementation only)

Usage

Build a validated sequential prescription (the table every other op consumes).

*surfaces* is a list; each entry is a dict {"R", "t", "n", "k", "ap", "mirror", "decenter", "tilt", "asph"} or a tuple (R, t, n[, k[, ap]]):

• R radius (mm, inf for flat), t thickness to the next surface (mm, the last one is the distance to the image plane when *image_mm* is not given — use None to place the image at the paraxial focus), n medium after the surface (index, (nd, vd) or glass), k conic (default 0), ap semi-aperture in mm (default None = unlimited), mirror bool, decenter (dx, dy) mm, tilt (ax, ay) degrees about x and y, asph even aspheric coefficients (A4, A6, A8, …) in mm⁻³, mm⁻⁵, … (default none = pure conic), f the focal length (mm, negative = diverging) of an ideal paraxial lens.

• An ideal lens surface ({"f": 50.0, "t": …}) has zero thickness, no shape and no glass (R is flat and n is the medium it sits in; both may be left out). It turns every ray by the thin-lens law exactly, tan θ' = tan θ − h/f per transverse axis (h = the ray height), so a collimated beam crosses the axis at f and a point object images at the Gaussian conjugate with m = −s'/s. Its optical path is the stigmatic one (f − √(h² + f²) for a collimated input, which is the quadratic phase −h²/(2f) at small h; a first surface with a finite object uses that object and its Gaussian image instead), so spot, OPD and PSF analyses of an aberration-free lens read zero aberration. It adds no Seidel terms and no dispersion, takes no mirror / conic / asph, and the non-coaxial raytrace3d does not read it.

• *stop*: index of the aperture-stop surface (default: the first surface). The stop's ap is the stop radius. If omitted, the smallest non-null aperture on any surface is used; an error is raised only if none exists.

• *object_mm*: distance from the first vertex to the object (inf for a collimated object); *index_object*: index of object space.

• *field*: default field for the analysis ops — degrees (infinite object) or object height in mm (finite object). Default 0 (on axis).

• *aim*: how the pupil rays are launched. "paraxial" (the default, and what every earlier version did) aims them at the paraxial entrance pupil, with only the chief ray moved onto the stop centre; in a system with pupil aberration the marginal rays then land outside the stop and are clipped (the stock Cooke triplet loses 48 of 217 rays on axis). "stop" is real-ray aiming: every pupil ray is iterated (Newton, tolerance 1e-11 of the stop radius, ValueError if it does not converge) so that it crosses the stop surface at the requested point, which is what lens-design software reports. See with_aiming.

• *vignetting*: per-field pupil vignetting factors as design software stores them, a list of {"field", "vdx", "vdy", "vcx", "vcy", "van"} (field in the same unit as *field*; missing keys are 0). Definition: the format's user guide (topic "Vignetting Factors") and knowledge-base article KA-01393. The normalised pupil p is first scaled and shifted, p' = (VDX, VDY) + diag(1 - VCX, 1 - VCY) p, and the result is then rotated by van degrees (counter-clockwise, x' = x cos - y sin): q = R(van) p' — the decentre rotates too. It is applied before the rays are aimed (at the stop with aim="stop", at the paraxial entrance pupil otherwise). A field between two records is interpolated linearly, one outside the listed range takes the nearest record. set_vignetting computes them from the apertures.

• a2 the r^2 term of the sag (mm^-1; the even asphere's PARM 1) and odd the general power series (B1, B2, ..., B8) added to the sag, z += B1 r + B2 r^2 + ... (an *odd asphere*; the coefficient of r^i is in mm^(1-i)). Both are left out of the surface dict when zero, so a prescription without them is bit-identical to before.

• solve a declarative rule that sets this surface's curvature or the thickness after it from the rest of the prescription (marginal / chief ray height or angle, paraxial image, F/#, concentric, aplanatic, pickup); lenssolve.lens_solve_set builds one, lenssolve.lens_apply_solves resolves them (lens_system resolves them once at construction, so the R / t it returns already hold the solved values). A surface with a thickness solve may leave t out.

• *image_R*, *image_k*: radius (mm, inf flat) and conic constant of the image surface; rays are intersected with it and spot / OPD are measured on it (the paraxial image is unchanged). Positive R: centre to the right.

• *afocal*: the image space is collimated (an afocal system: telescope, beam expander). It needs an object at infinity; paraxial_trace then reports angular_magnification and the exit pupil instead of a focal length (efl = bfl = inf), ray_bundle / spot_stats / ray_fan report the exit angle in arcminutes (relative to the chief ray) in place of the image-plane position, and opd_samples references a plane wave. A system flagged afocal whose collimated beam does not leave collimated is refused (ValueError).

Default (no *surfaces*): a plano-convex BK7 singlet, f ≈ 100 mm, f/4, stop at the first surface — a sensible starting point for the examples and for the no-argument registry call.

Returns a plain dict (JSON-friendly) — pass it to the other functions.

Family-wide input contract (fail-closed)

Every optics op validates its input before computing (nothing slips through silently):

• Units are baked into the argument name — _mm / _um / _deg / _mrad. Confusing mm with µm does not crash; it yields a plausible-looking wrong answer, so the name prevents it. Nothing here guesses the unit from the magnitude.

• Strings raise ValueError — float('50') succeeds, so an unparsed configuration value would slip through as a length (measured: thin_lens('50', '200') returned a plausible 66.667 mm). bool is refused too, as the implicit promotion True == 1.

• complex / masked arrays raise ValueError (real-valued slots only; silently dropping the imaginary part or peeling off the mask is refused). NaN/Inf raises ValueError on every input. Exception, declared: the u_map of bottle_fit_from_pattern takes pixels that could not be measured as NaN (infinity is still refused).

• Division by zero and its relatives are refused by name: focal length 0, radius of curvature 0, refractive index <= 0, a fully opaque aperture (all zeros, so the normalisation is 0/0), a PSF whose sum is <= 0, a Stokes vector with S0 = 0, and an object sitting at the front focal point (the image is at infinity).

• Only two ops return a non-finite value, and both state it as a contract: depth_of_field returns far_mm = inf beyond the hyperfocal distance (that is what the hyperfocal distance means), and gaussian_beam returns wavefront_radius_mm = inf at the waist (the radius of curvature of a plane wavefront). Both also return a finite companion (far_is_infinite / curvature_per_mm). Any other silent NaN/Inf is detected internally and raises ValueError — "float64 overflowed" and "the answer is infinite" are different claims, so the first is never returned wearing the face of the second. Exception, declared: bottle_background_map returns NaN in u_mm for pixels whose ray is unusable (total internal reflection and the like), together with a valid mask and the counts.

• Size caps: generated grids are capped by optics.MAX_GRID (4096); supplied fields/PSFs/apertures by optics.MAX_FIELD_ELEMENTS (2^24); ABCD element chains by optics.MAX_SYSTEM_ELEMENTS (1024); Zernike by MAX_ZERNIKE_TERMS (512) / MAX_ZERNIKE_ORDER (40) / MAX_ZERNIKE_BASIS (2^25). This closes, fail-closed, the paths where a small argument triggers a huge internal allocation (measured: n_max=40 × 4096² needs 108 GB).

• Physically impossible states are refused too: a Stokes vector with degree of polarisation > 1, negative transmittance, negative intensity, and invalid Zernike indices such as n-|m| odd.

Detailed usage guide

• optics_imaging family guide

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• anim3d_lens_aberrations — py -3.11 examples/anim3d_lens_aberrations.py

• lens_calibration_loop_demo — py -3.11 examples/lens_calibration_loop_demo.py

• lens_defect_dataset_demo — py -3.11 examples/lens_defect_dataset_demo.py

• lens_design_demo — py -3.11 examples/lens_design_demo.py

• lens_optimize_demo — py -3.11 examples/lens_optimize_demo.py

• optical_design_metrics — py -3.11 examples/optical_design_metrics.py

• plenoptic_capture_pipeline — py -3.11 examples/plenoptic_capture_pipeline.py

• poc_chromatic_confocal — py -3.11 examples/poc_chromatic_confocal.py

• poc_lens_file_roundtrip — py -3.11 examples/poc_lens_file_roundtrip.py

• poc_lens_global_search — py -3.11 examples/poc_lens_global_search.py

• poc_lens_merit_operands — py -3.11 examples/poc_lens_merit_operands.py

• poc_lens_solves_afocal — py -3.11 examples/poc_lens_solves_afocal.py

• poc_lens_tolerancing — py -3.11 examples/poc_lens_tolerancing.py

• poc_material_optical_library — py -3.11 examples/poc_material_optical_library.py

• poc_noncoaxial_optics — py -3.11 examples/poc_noncoaxial_optics.py

• poc_optics_drawings — py -3.11 examples/poc_optics_drawings.py

• poc_photograph_through_prescription — py -3.11 examples/poc_photograph_through_prescription.py

• poc_plenoptic_camera — py -3.11 examples/poc_plenoptic_camera.py

• poc_tdi_linescan — py -3.11 examples/poc_tdi_linescan.py

• poc_telecentric_metrology — py -3.11 examples/poc_telecentric_metrology.py

• poc_zoom_configurations — py -3.11 examples/poc_zoom_configurations.py

Ops the type connects to (they accept table as input)

abcd_matrix · wavefront_stats · sfr_from_edge · paraxial_trace · seidel_coefficients · spot_stats · tolerance_analysis · wavefront_from_opd

Same category (design)

thick_lens · glass · example_system · glass_catalog · sellmeier · paraxial_trace · seidel_coefficients · spot_stats


*Provenance: raytrace.py — OPTICS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.