Coherent optical beam propagation, geometric ray tracing, and manipulation using the Angular Spectrum Method.
Project description
lumenairy
A comprehensive Python library for coherent optical beam propagation and manipulation using the Angular Spectrum Method (ASM) and related techniques.
Author: Andrew Traverso
Release notes
See CHANGELOG.md for the full per-release
notes (v3.0 through current). Recent release highlights are
also published on the
wiki Release-Notes page
and the
GitHub Releases
feed. The PyPI project page (this README) historically embedded
per-version release notes inline, but those entries went stale
after v5.1.0; v5.3.1 retires the embedded form in favour of the
single pointer above.
Cookbook
Short, self-contained examples for the six public functions
added in v4.14.0. Each block is a runnable snippet (assumes a
prior import lumenairy as la and a populated complex field
E on an N x N grid with pixel pitch dx).
Encircled energy curve
encircled_energy_curve(E, dx, *, dy=None, radii=None, centroid=None, n_radii=64) returns the fraction of total power
within radius r of the centroid as a 1-D curve.
import numpy as np
import lumenairy as la
N, dx = 256, 2e-6
x = (np.arange(N) - N / 2) * dx
X, Y = np.meshgrid(x, x)
E = np.exp(-(X * X + Y * Y) / (2 * (10e-6) ** 2)).astype(np.complex128)
radii, ee = la.encircled_energy_curve(E, dx, n_radii=64)
print(f"50% encircled radius ~= {radii[np.searchsorted(ee, 0.5)] * 1e6:.2f} um")
Encircled energy radius
encircled_energy_radius(E, dx, *, dy=None, threshold=0.84, centroid=None) returns the radius enclosing the given
threshold fraction (default 0.84 -- the standard "84% encircled
radius" lens-spec convention).
import numpy as np
import lumenairy as la
N, dx = 256, 2e-6
x = (np.arange(N) - N / 2) * dx
X, Y = np.meshgrid(x, x)
E = np.exp(-(X * X + Y * Y) / (2 * (10e-6) ** 2)).astype(np.complex128)
r84 = la.encircled_energy_radius(E, dx, threshold=0.84)
print(f"r84 = {r84 * 1e6:.2f} um")
MTF cutoff
mtf_cutoff(mtf_profile, freq, *, threshold=0.5) returns the
spatial frequency at which a 1-D MTF profile drops below
threshold. Returns np.inf if the MTF stays above the
threshold for all frequencies.
import numpy as np
import lumenairy as la
# Synthetic Gaussian MTF profile.
freq = np.linspace(0, 1000, 200) # cycles/mm
mtf = np.exp(-(freq / 300.0) ** 2)
f50 = la.mtf_cutoff(mtf, freq, threshold=0.5)
print(f"50% MTF cutoff = {f50:.1f} cycles/mm")
Beam diameter
beam_diameter(E, dx, *, dy=None, threshold='1/e^2', centroid=None) returns the diameter at which the intensity
drops below the named threshold. Accepts '1/e^2', '1/e',
'FWHM', or 'D4sigma'.
import numpy as np
import lumenairy as la
N, dx = 256, 2e-6
x = (np.arange(N) - N / 2) * dx
X, Y = np.meshgrid(x, x)
E = np.exp(-(X * X + Y * Y) / (2 * (10e-6) ** 2)).astype(np.complex128)
d_1e2 = la.beam_diameter(E, dx, threshold='1/e^2')
d_4s = la.beam_diameter(E, dx, threshold='D4sigma')
print(f"1/e^2 diameter = {d_1e2 * 1e6:.2f} um; D4sigma = {d_4s * 1e6:.2f} um")
Depth of focus
depth_of_focus(wavelength, f_number, *, formula='rayleigh')
returns the one-sided depth of focus. 'rayleigh' evaluates
+/-4 f# ** 2 * lambda; 'marechal' evaluates +/-lambda / NA ** 2.
import lumenairy as la
# F/2.8 lens at 633 nm.
dof = la.depth_of_focus(wavelength=633e-9, f_number=2.8,
formula='rayleigh')
print(f"Rayleigh depth of focus = +/-{dof * 1e6:.2f} um")
Wavefront map plot
plot_wavefront(opd, dx, *, dy=None, aperture=None, units='waves', wavelength=None, cmap='RdBu_r', show_stats=True, ax=None, fig=None, title=None) produces a
Zemax-style wavefront map: NaN outside the aperture, divergent
colormap centred on zero, PV/RMS overlay.
import numpy as np
import lumenairy as la
N, dx = 128, 4e-6
x = (np.arange(N) - N / 2) * dx
X, Y = np.meshgrid(x, x)
# Synthetic OPD: 1/2 wave of defocus across a 200 um aperture.
opd = 0.5 * 633e-9 * (X * X + Y * Y) / (100e-6) ** 2
aperture = (X * X + Y * Y) <= (100e-6) ** 2
# Headless backend so the example runs in CI without a display.
import matplotlib
matplotlib.use('Agg')
fig, ax = la.plot_wavefront(opd, dx, aperture=aperture,
units='waves', wavelength=633e-9,
title='Defocus')
Anamorphic-grid (dy) migration example for makedammann2d
A short worked example for users migrating off the legacy
makedammann2d µm-units form to the v4.14.3+ explicit
_legacy_units='SI' opt-in. Required reading for THz / MMW
DOE designers with mm-scale periods.
import lumenairy as la
# Pre-v4.14.3 (legacy) -- silent µm-units rescale at value > 1e-3.
# T = la.makedammann2d(periodx=100, periody=100, waveln=1.31)
# v4.14.3+ canonical SI form -- explicit opt-in, no rescale.
T = la.makedammann2d(periodx=100e-6, periody=100e-6,
waveln=1.31e-6, _legacy_units='SI')
What's new in 4.14.2
Closes the v4.14.1 audit (docs/audits/AUDIT_V4_14_1_2026_05_17.md).
The audit found 1 NEW P0 (a 3-release carryover: glass.py
S-LAH64/S-LAH79 dispatch broken since v4.11.2) + 10 new P1s,
including 4 "fix N, miss N+1" recurrences on v4.14.1 itself
(aperture=0 sentinel missed 2 sites; 7 older caches still unlocked;
4 residual 0+0j literal sites; clear_asm_caches chain narrower
than docstring) and 6 P1s in under-examined modules (freeform
domain guard, makedammann unit drift, polarization API gaps,
source factory validation). **v4.14.2 closes the P0 + all 10 P1s
- a follow-up cache-lock gap discovered by Agent C's new meta-pin.** 1190 unit tests pass; 34/34 validation files pass.
Documentation reorganisation
docs/audits/— all audit reports andCORRECTION_PLAN.mdmoved from repo root.docs/release_notes/— all.release_notes_v*.mdper-release drafts moved from repo root.
Repo root now contains only the primary project docs:
README.md, CHANGELOG.md, ROADMAP.md, CONTRIBUTING.md,
GUI_README.md, GUI_CHANGELOG.md, REAL_LENS_CHANGES.md.
P0 closure — glass.py S-LAH64 / S-LAH79 dispatch
3-release carryover regression. v4.11.2 removed both glasses from
SELLMEIER_COEFFICIENTS after discovering the in-code coefficients
were off by ±5.8% vs the Ohara catalog — but never updated
GLASS_REGISTRY, leaving both flagged '__sellmeier__'. The
dispatcher raised ValueError on every call. UI's "known-good
preset" path broken for 3 releases.
v4.14.2 routes both to ('specs', 'OHARA-optical', '<name>') tuple
form (catalogue book name verified by introspection). Numerical
agreement vs Ohara catalog: 5e-5. Module-load consistency check
added that fails fast at import time on any future
GLASS_REGISTRY ↔ SELLMEIER_COEFFICIENTS drift.
P1 closures — sibling-gap recurrences on v4.14.1
- Aperture=0 sentinel finish — v4.14.1 missed
ToleranceAware Merit._evaluate_perturbedandMatchIdealSystem._make_source. Both now use the canonicalis _ZERO_APERTURE_MASKbranch. Investigation finding:apply_perturbationsdoesn't mutate prescription-levelaperture_diameter(only per-surfacedecenter/tilt/form_error), so the audit's worry about perturbed-to-zero apertures is not triggered — pinned the invariant as a regression guard. - 7 older caches now locked (
_ZERNIKE_BASIS_CACHE,_THROUGH_FOCUS_SCAN_JAX_CACHE,_PROPAGATE_SYSTEM_JAX_CACHE,_GS_KERNEL_CACHE,_ER_KERNEL_CACHE,_HIO_KERNEL_CACHE,_TRACE_JAX_CACHE). Lock-scope discipline matches v4.14.1 (hold forOrderedDictops only, release before expensive kernel build). Concurrent 4-thread tests confirm no exceptions. clear_asm_caches()chain expanded to 5 sibling caches via lazy-import + call. Docstring rewritten to honestly enumerate all caches it clears.- 2 P1-severity residual
0+0jliteral sites swept (optimize/core.py:966,phase_retrieval.py:402).
P1 closures — under-examined modules
surface_sag_xy_polynomialdomain guard added (matches the Chebyshev-branch precedent).makedammann2dSI metres with per-parameter deprecation heuristic — handles pure-legacy and hybrid µm/SI calls without breaking existing tests.apply_rotatoracceptsangle_deg=(matches v4.7 polarization-family convention).JonesField.__init__input validation (2-D shape + positive-finitedx, dy).create_led_sourcesignature normalised to v4.7-canonical(N, dx, wavelength, *, diameter, divergence_angle, dy=None, ...)with legacy-positional deprecation shim._validate_grid_paramshelper + applied to all 10create_*factories insources/core.py.
Follow-up cache-lock gap closed
The new cache↔lock meta-pin discovered _JAX_IFT_SOLVER_CACHE in
propagators/asymptotic.py — a single-cell lazy-init cache
without a lock. Race window on first concurrent call would
double-decorate the JAX custom_vjp solver. v4.14.2 closes the
gap in the same release via double-check locking pattern:
fast-path no-lock for the common populated case; slow-path
acquires lock, re-checks, delegates the actual build to
_build_jax_ift_solver_impl (worker function). Meta-pin's
known-gap set is now empty.
Two new structural meta-pins
Extending v4.14.1's cache-clear dispatcher-pin pattern to two more sibling-gap classes:
test_v4_14_2_dispatcher_pin_cache_locks.py— 39 collected (38 pass + 1 documented_ZARR_MKDIR_PATCH_LOCKskip): every module-level_<NAME>_CACHEmust have a corresponding lock. Accepts both_FOO_LOCKand_FOO_CACHE_LOCKnaming conventions. Reverse check on_LOCKnames + documented_PATCH_LOCKexemption.test_v4_14_2_dispatcher_pin_zero_plus_zeroj.py(123 tests) — walks all 117lumenairy/*.pyfiles fornp.where(.*, 0+0j)literals. Three exemption layers (comment lines, trailing.astype()recovery, explicit P3 allowlist).
What's new in 4.14.1
Closes the v4.14.0 audit (docs/audits/AUDIT_V4_14_0_2026_05_17.md).
v4.14.0 shipped 7 perf wins + 6 new public functions + 80
parametrized dispatcher pins, but the audit found 1 P0 silent-
wrong-physics bug in the 77× LG/HG mode-stack cache key plus 6
P1s. v4.14.1 closes the P0 + all 6 P1s + the top-priority P2s.
911 unit tests pass (up from 858); 34/34 validation files pass.
P0 closure — LG/HG mode-stack cache key
The 77× perf win in v4.14.0 had a silent-wrong-physics bug. The
cache key omitted dx, dy — two calls with the same shape but
different physical pitch (e.g. dx=1e-6 then dx=2e-6, both at
N=256) collided on this key, returning the FIRST grid's modes
evaluated against the second call's field. Silently wrong on
multi-resolution analysis, wavelength-adaptive grid sweeps, and
optimisation loops where dx is a free variable. v4.14.1 adds
dx, dy to both keys with a regression pin.
P1 closures
-
Aperture=0 semantics regression fixed. v4.14.0's wrapper- merit cache mapped
aperture <= 0tomask=None, which downstream interpreted as "no clipping, full grid plane wave." v4.14.1 adds a_ZeroApertureMaskSentinelso callers can distinguish "no aperture specified" from "aperture explicitly zero (block all light)." Pre-v4.14 semantics restored. -
Brewster-angle phase aggregation bug in
coatings.py(the v4.13.1 audit flagged it; v4.14.0's wavelength-batch rewrite inherited the bug).0.5 * (angle(r_s) + angle(r_p))is wrong by π/2 or π at Brewster. v4.14.1 changes tonp.angle(0.5 * (r_s + r_p))(complex sum then angle — robust to π discontinuities). -
_solve_envelope_stationary_batchcontract violation — function promisedconverged=Falsefor failed pixels but setTrueto drop them from the active set. Separatefinishedmask added;convergednow matches the docstring. -
clear_lg_mode_stack_cachenow in top-level__all__— v4.14.0's CHANGELOG claimed this was "Public" but the import wasn't inlumenairy/__init__.py. The audit-meta-finding recurring on the very release that shipped 80 dispatcher pins. v4.14.1 closes the gap AND adds a structural counter-measure: a cache-clear dispatcher pin that walks every submodule's__all__forclear_*names and asserts each is re-exported at top level. Future cache-clear additions can't regress this. -
encircled_energy_radiusdocstring corrected — claimee[0] = 0 alwaysis false;ee[0]equals the centre-pixel intensity contribution. -
row_resetresets the Newton warm-start (P1-NEW-5) — therow_resetbranch now resetslast_v_star = (v_cx, v_cy)at each raster row wrap, eliminating the cross-row Newton chain that plausibly entered wrong-saddle basins near grid edges. Coordinated with the fix, the bit-equal pin and the older 1e-10 rel pins both had their inline scalar references updated to resetlast_v_starat row wrap too.
Tier-2 follow-ups
- Thread-safety locks on the three new v4.14.0 caches.
- Monkey-patch removed in
optimize/core.py—clear_asm_caches()now lazy-imports and calls_clear_wrapper_merit_cache+clear_lg_mode_stack_cachedirectly, eliminating re-import recursion risk. - LG/HG mode-stack cache now wired into
clear_asm_caches()(v4.14.0 only wired it intolumenairy_context()). - Final
0+0j/1+0jliteral sweep — 2 missed sites (lenses_maslov.py:448,_lens_thin.py:173). fiber_modeadded toTestP1CSourceFactoryDispatcherPinparametrize list (stale exclusion from v4.13.2).
Doc-trust hygiene
4 confirmed doc-drift items from the audit retroactively
corrected in the v4.14.0 CHANGELOG entry below: "16 tests at 1e-10
rel" (→ 3 in cited class), "6 batched helpers consumed by 77×
win" (→ helpers have zero production consumers, reserved for
v4.15+), HG cache key w[s] shorthand (→ both wx, wy),
lenses_maslov.py line drift.
What's new in 4.14.0
Phase B of the v4.13.1 audit (docs/audits/AUDIT_V4_13_1_2026_05_17.md).
v4.13.2 closed Tier-0; v4.14.0 closes Tier-1: 7 perf wins, 6 new
user-facing API functions, and 80 dispatcher pins that close out
the recurring sibling-gap audit-meta-finding. All 858 unit tests
pass; 34/34 validation files pass.
Performance wins
| Hot path | Speedup |
|---|---|
coating_reflectance wavelength batch (50 layers × 200 wv) |
24.6× |
decompose_lg / decompose_hg cache (warm, 256², 28 modes) |
77× |
MultiWavelengthMerit meshgrid cache (N=512, 5 wl, 20 FD) |
6.17× |
_evaluate_polynomial_4d_and_grad34 (typical configs) |
4.6× |
MultiWavelengthMerit meshgrid cache (N=128, 3 wl, 5 FD) |
4.16× |
ToleranceAwareMerit meshgrid cache |
3.17× |
| Shack-Hartmann scatter-gather loop | 2.27× |
Phase-retrieval angle/exp round-trip (GS) |
2.26× |
| Phase-retrieval (ER / HIO) | 1.85× / 1.59× |
Multi*Merit meshgrid build count drops from O(n_wl × n_field × FD) to 1 per (N, dx, aperture) signature for an entire
optimisation run. LG/HG mode-stack cache wired into
clear_asm_caches() and lumenairy_context().
Coating Snell-chain hoist is what unlocks the 24× batched
speedup: the documented n.imag-dropped-at-Snell-step approximation
makes the per-layer chain wavelength-independent, walked ONCE
outside the polarisation loop.
Modal asymptotic vectorisation (audit target 20-100×) was
investigated but NOT shipped publicly — the cold-start batched
Newton finds the physical saddle uniformly across pixels, while the
pre-v4.14 warm-started Newton lands in wrong-saddle basins at grid
edges that the overflow guard zeros. The existing test pin bit-
equals the (subtly wrong-at-edges) warm-start behaviour. This is a
real physics finding worth a coordinated v4.15+ release that
updates the test pin alongside the algorithm change. The
vectorised helpers ship privately and are consumed by the
decompose_lg/decompose_hg cache (the 77× warm win).
New user-facing API
Six new functions exposed at lumenairy.*:
encircled_energy_curve(E, dx, *, dy=None, radii=None, ...)returns(radii, ee)whereee[i]is the fraction of total power withinradii[i]of the centroid.encircled_energy_radius(E, dx, *, threshold=0.84, ...)— standard "84% encircled radius" lens spec metric.mtf_cutoff(mtf_profile, freq, *, threshold=0.5)— spatial frequency at which a 1D MTF drops below threshold; returnsnp.infif the MTF stays above threshold everywhere.beam_diameter(E, dx, *, threshold='1/e^2', ...)— diameter at intensity threshold. String thresholds:'1/e^2','1/e','FWHM','D4sigma'.depth_of_focus(wavelength, f_number, *, formula='rayleigh')— Rayleigh / Marechal depth of focus.plot_wavefront(opd, dx, *, dy=None, aperture=None, units='waves', wavelength=None, ...)— Zemax-style wavefront map: NaN outside aperture, divergent colormap, PV/RMS overlay.
All six honour dy from the start; dy=None defaults to dx.
Sibling-gap parametrized dispatcher pins (audit meta-finding closure)
80 new pins across 3 test files, closing out the recurring "fix swept N sites, missed N+1" pattern for three sibling families:
(scalar, vectorial) HFPI— 29 pins covering_spawn_rngindependence, grazing-ray inf/NaN guards, alive-mask correctness.(NumPy, JAX) apply_real_lensfamily — 35 pins covering case- insensitiveglass_after='MIRROR',dyhandling, dtype preservation across all 5 variants.- Welford-mirror convention — 16 pins across
seidel_coefficients,petzval_radius,aberration_summary,chromatic_focal_shift,distortion_grid,field_aberration_sweep,eval_image_plane_wfe.
Sibling-gaps DISCOVERED and fixed in this release by the new
pins (the meta-finding self-closing): complex64 dtype preservation
was broken on apply_real_lens_maslov (3 sites in
lenses_maslov.py) and both JAX twins (_lens_jax.py:573, 819).
Fixed by threading out_dtype=E_in.dtype through the maslov
helpers (_integrate_quadrature, _integrate_local_quadrature,
_integrate_stationary_phase) and routing _resolve_jax_complex _dtype(E_in.dtype) instead of the library-default. A final
post-normalisation .astype(E_in.dtype) catches the python-float
multiply that promotes complex64 → complex128.
What's new in 4.13.2
Closes the v4.13.1 audit (docs/audits/AUDIT_V4_13_1_2026_05_17.md) plus its
Part 10 consolidation with a parallel cross-library survey. v4.13.1
audit identified 12 new P1s (5 sibling-gap recurrences + 5 fresh-eyes
bugs + 2 partial-closure follow-ups); the cross-library survey turned
up 5 additional P0s in optimize/ + io/ and 7 P1s mostly around
dy convention drift. v4.13.2 closes the full consolidated Tier-0
set. All 710 unit tests pass; 34/34 validation files pass.
P0 closures (cross-library survey)
make_lg_aberration_merit_jaxno longer silently ignores itstargetsdict (loop body was literallypass; weighted sum was never computed). Public API. v4.13.2 weights the(0,0)target correctly; non-(0,0)raisesNotImplementedError.MultiFieldMerit.field_anglesaccepts(theta_x, theta_y)tuples (was Y-axis tilt only despite docstring). Scalar form preserved with a one-shotDeprecationWarning.load_plane_slicedocumented return type now matches actual ((arr, attrs)tuple).- CODE V
.seq+ Quadoa.qosround-trip preserves BFL via a new'back_focal_length'prescription key populated by readers and exporters.
Sibling-gap P1 closures
Five new "fix-swept-N-sites-missed-N+1" instances closed, each with parametrized dispatcher pins:
vectorial_hfpiRNG-correlation — v4.11.2 fixed scalar HFPI; the vectorial twin was missed. Ports_spawn_rng.subaperture.propagate_subaperture_asymptotic— v4.11.2 added per-patchdecompose_lginhf.py; the subaperture sibling still passed unit-amplitude(0,0). Functionally wrong for any non- trivial input. v4.13.2 ports the per-patch decomposition with 3 new tunable kwargs.petzval_radiusWelford-mirror convention —seidel_coefficientswas fixed in v4.11.2;petzval_radiuswas still skipping every mirror. Wrong by 100% for catadioptric / Cassegrain designs._build_jax_prescriptionglass_after='MIRROR'check added (v4.13.1 P1-A's two-guard fix only partly applied to the JAX prescription builder).- JAX lens twins thread
dy=(apply_real_lens_traced_jax,apply_real_lens_maslov_jax). RaiseValueErrorondy != dxmatching NumPy precedent.
Fresh-eyes P1 closures
apply_mirrorNaN guard matchesapply_real_lens(hyperbolic mirror at conic-domain boundary no longer poisons every pixel)._zero_C_air_gapraisesRuntimeErroron degenerate ABCD (was silently returning placeholder thickness).propagate_to_planezeroes the step on dead/grazing rays in BOTH scalar (hfpi) and vector (vectorial_hfpi) paths.RandomState.choiceint dtype aligned across NumPy and JAX backends (both return int64 now).trace_prescriptionuses_surface_copy_withinstead of mutating sharedSurface.thicknessin place.
Partial-closure follow-ups
dual_annealingcallback wired intoCancellableProgress(v4.13.1's inline lambda didn't pollis_cancelled).RandomState.choiceold-JAX safety net —try/except TypeError→RuntimeErrorwith a clear "JAX >= 0.4.0 required" message.
Cross-library survey P1 closures
strehl_ratio+polychromatic_psfacceptdy=(v4.13.0 L3 sweep missed these). Back-compat preserved bit-for-bit whendy is None.- Wrapper-merit context threads
x=ctx.xthrough the threeMulti*Merit/ToleranceAwareMeritsub-contexts (the analytic- gradient path no longer silently degrades to FD inside these). propagate_through_systemelement handlers threaddythrough all 13 element types (was silently squaring anamorphicdytodx).Source.fiber_modeacceptsdy=end-to-end (create_fiber _modewidened). Dispatcher pin now covers all 6 factories.
Thin-lens family sibling-gap sweep
Cross-library survey found additional sibling-gaps in the thin-lens family:
- 9 sites of
0.0+0.0jcomplex128 literal replaced withxp.zeros((), dtype=E.dtype). - 6 thin-lens functions now match
phase_exp.dtypeagainstE.dtype(were silently upcasting complex64 → complex128 via thexp.exp(1j*phase)mask). - 3 thin-lens functions gained the documented
use_gpuparameter and canonical CuPy dispatch. - Latent CuPy dispatch bug fixed in
apply_thin_lens,apply_spherical_lens,apply_aspheric_lens— barecpreferences (Python LEGB rules skip module-level PEP 562__getattr__for function-local lookups) routed through_lenses_module.cpinstead.
Quick wins
- 5 mis-tiered names in
__init__.py.__all__moved to correct tiers. - Duplicate
reset_fft_backendimport removed. - Broken wiki TOC anchor fixed (
#whats-new-in-4-14-0→#whats-new-in-4-13-1).
What's new in 4.13.1
Closes the v4.13.0 audit (docs/audits/AUDIT_V4_13_0_2026_05_17.md) plus an
additional perf-survey pass. v4.13.0 was tagged in git but never
published to PyPI; that audit identified 7 P1 (latent bug), 9 P2
(code smell), and 6 P3 (cleanup) findings. v4.13.1 closes every
Tier-0 / Tier-1 / Tier-2 / Tier-3 item plus 3 new perf wins
discovered in the parallel survey. All 654 unit tests pass;
34/34 validation files pass.
Sibling-gap P1 closures
The audit's headline finding: 3 "sibling-gap" recurrences in v4.13.0's own audit closures (a fix sweeps N call sites but misses N+1). All three closed in v4.13.1:
- P1-A:
apply_real_lensmirror guard -- the v4.13.0 L4a sweep hardened 4 siblingapply_real_lens_*variants but missed the parent function itself. A hand-built prescription withsurfaces[i]['is_mirror']=Truewould silently miscompute through the parent. Guard ported from the_lens_traced.pytemplate; parametrized dispatcher pin added over all 5 variants to prevent future sibling-gaps in this family. - P1-B: ER/HIO complex-dtype routing --
gerchberg_saxton_jaxcorrectly routeddtypethrough_resolve_jax_complex_dtypein v4.13.0, buterror_reduction_jaxandhybrid_input_output _jaxskipped the resolver -- the EXACT silent float64 -> complex64 demotion bug L2 was meant to close, still live. Both now route through the resolver; cache keys pinned on resolved complex dtype. Parametrized pin across all 3 phase- retrieval kernels. - P1-C:
Source.propagate()+ 5 factories threaddy--Source.propagate(...)and the 5 classmethod factories (gaussian,plane_wave,point_source,top_hat,fiber_mode) were droppingdyfrom the returned Source even though the underlying E-field was built on the anamorphic grid.Source.propagatealso gained an optionaloutput_dykwarg for symmetry withoutput_dx. Parametrized dispatcher pin over the 4 dy-aware factories.
UI + infrastructure P1 closures
- P1-D:
ThinGratingDocknon-functional -- the dock's_runwas callinggrating_efficiency_vs_wavelengthwith wrong kwargs (groove_index,substrate_index,profile,angle) while the function expected(period, depth, *, n_ridge, n_groove, n_substrate, n_superstrate, order, ...). Every click raisedTypeErrorsilently swallowed into the summary text box. Rewrote with correct signature, added missing UI inputs. Math path extracted into_compute_efficiency_data (inputs: dict) -> dictso it's unit-testable without Qt. - P1-E:
_context.pycache-clear import moved inside try --from .propagators.propagation import clear_asm_cacheswas OUTSIDE the try-block, so a rename / circular import there would bypass ALL 6 subsequent guarded cache-clears. Fixed. - P1-F:
RandomState.choicehonoursreplace=Falseon JAX -- previously silently ignored the flag on thep=Nonepath. Now dispatches tojax.random.choice(..., replace=False). - L7: benchmark cache clear --
test_bench_through_focus _scan_jax_first_vs_warmnow clears_THROUGH_FOCUS_SCAN_JAX _CACHEbefore timing, matching the 4 sibling benchmarks.
Tier-2 follow-ups (P2 code smells)
_RestoreDtypetry/finally -- explicitrestore()is idempotent; main call sites wrapped intry/finally. More robust underKeyboardInterruptand exception unwinding._merit_jac_autousesscheme='forward'+ cachedf0-- scipy already evaluatesmerit_fn(x)before calling jac on the samex; that value is now passed asf0. FD eval count drops from2NtoN + 2per gradient (~30% saving for N=10).- Cancellation protocol -- new
CancellableProgressclass (exposed atlumenairy.CancellableProgress) wired into all 4 scipy callbacks indesign_optimize.cancel()causes scipy to stop gracefully; the post-loop final eval +DesignResultreturn still executes so the caller gets best-so-far state instead of a partial-dataKeyboardInterrupt. - Merit propagator inconsistency warning -- when
wave_propagator != 'real_lens'AND any ofMultiWavelengthMerit/MultiFieldMerit/ToleranceAware Meritis in use,design_optimizeemits aUserWarningat entry pointing out that off-nominal Merit legs always useapply_real_lens, regardless of the selected propagator. - Shared
_build_asm_H_squarehelper -- the v4.13.0 Shack- Hartmann FFT batching introduced a local_build_asm_H_for _lensletinanalysis/detector.pythat duplicated propagator H-build logic. Now consolidated intopropagators/propagation .py:_build_asm_H_square(N, dx, z, wavelength, dtype=None, bandlimit=True), imported bydetector.py. Pinned at 1e-14 against a hand-built reference. _fd_grad_purevalidate_f0parameter -- opt-in validation gate for stale-f0detection on the forward path.- BSDF TIS shape assertion --
np.broadcast_tomask replaced with aValueErrorlisting expected vs actual shape.
Tier-3 cleanups
memory.set_max_ramvalidates non-negative input (was silently accepting-5as -5 GB).get_max_ramadded to__all__.MultiPrescriptionParameterizationraisesValueErroron duplicate(prescription_index, *path)entries infree_vars._is_jax_prng_keyrecognises JAX 0.4.20+ opaque PRNG keys via the canonicaljax.dtypes.issubdtypecheck.- Dtype-aware zero in
apply_mirrorandapply_aperturereplaces0.0+0.0jliteral that could upcast on JAX x32. apply_mirroraperture docstring corrected (said "ellipse"; code computes a circle).- Stale pyc cleanup from the v4.13 γ.1 revert.
New performance wins (beyond audit scope)
| Hot path | Speedup |
|---|---|
vectorial_hfpi.accumulate_vector_to_grid (1M paths, 256²) |
1.65 - 1.75x (bit-exact) |
analysis/detector.py:shack_hartmann scatter-back (K=4096) |
9.5 - 25x (bit-exact) |
gbd.reconstruct_field_from_beamlets (1024 beamlets, 96²) |
1.2 - 1.5x typical, 2.3x cache-warm (rel_err ~4e-16) |
Plus a side-effect win from the ThinGratingDock P1-D fix: the
dock now does one thin_grating_efficiency_1d call per
wavelength (full per-order matrix) instead of N sweeps through
the single-order helper.
Deferred perf candidates catalogued for v4.14+:
HF callable per-pixel loop (5-20x potential, needs API contract
change); fused (sag, dz_dh) helper for Newton iters (1.5-2x on
aspherics, needs lenses.py:surface_sag_general reorganisation);
analytic pure-conic intersection extending the v4.12.1 Newton-skip
(5-10x on pure-conic surfaces, needs broader root-selection
validation).
v4.13.0 CHANGELOG hygiene
11 doc-vs-code mismatches in the v4.13.0 CHANGELOG entry have been
retroactively corrected. The "Breaking changes" subsection now
explicitly enumerates the two breaking changes from v4.13.0
(rcwa.py rename without shim; wavelength sentinel).
What's new in 4.13.0
**Three-phase bundle since v4.12.2: audit known-limitations closure
except Exception:sweep + Tier-2 perf wins.** All 573 unit tests pass; 34/34 validation files pass.
Phase 1 — audit known-limitations closure
Closes S1, S2, S3 + L2, L3, L4, L6, L8 from the v4.12.1 pre-PyPI audit:
- S1:
io.storagecomplex-dtype preservation --save_jones _field_h5,append_plane_h5,_zarr_append_plane, and the unifiedappend_planedispatcher gained apreserve_dtypeparameter that threads through the write path. Default behaviour preserved (v4.12.x promotion). - S2:
io.codegenaperture-stop emission + wavelength sentinel --_decompose_prescriptionnow emits anaperturestep wheneveris_stop=True; missingwavelength_nmraisesValueErrorinstead of silently defaulting to 1.31 µm. - S3:
analysis.ghostR/r convention -- uppercase Fresnel reflectance disambiguated from lowercase curvature radius; locals renamed, public dict keys preserved. - L2: JAX complex-dtype routing -- auto-enables
jax_enable _x64with a one-shot RuntimeWarning when complex128 is required;_PROPAGATE_SYSTEM_JAX_CACHEkey now includes dtype to prevent float32 → float64 cache aliasing. - L3:
PropagationResult.dy+Source.dy-- anamorphic-grid metadata round-trips end-to-end through the dispatch path. - L4: sibling mirror-guards --
apply_real_lens_traced_jax,apply_real_lens_maslov_jax, andlenses_maslov.apply_real_lens _maslovall rejectis_mirror=Truesurfaces consistently. - L6:
apply_mirrorxp + dy -- backend dispatch via_xp_of(E_in)and adyparameter for anamorphic grids; NumPy numba fast-path preserved.dy=Nonereproduces v4.12.x behaviour exactly. - L8: zarr thread-safety -- module-level
threading.Lockguards thePath.mkdirmonkey-patch in_open_zarr_group_safe.
Phase 2 — except Exception: sweep
Non-UI library files: 99 → 3 justified KEEP-AS-IS sites.
- ~85 narrowed to typed exception tuples covering the
documented failure modes (pyFFTW failures, cache-clear guards,
system_abcdfallbacks, glass-loader failures, etc.). - 3 WARN-BEFORE-PASS upgrades surfacing previously-silent
degradations:
analysis/field.py petzval_radiusmissing-glass,propagators/hf.pyLG-decomposition fallback, anddesign_optimize plane_loggercallback failures. - Regression budget pin: non-UI
except Exception:count must stay ≤ 15 (tests/unit/test_audit_fixes_v4_13_0_except _sweep.py).
Phase 3 — Tier-2 performance wins
Representative speedups (perf_counter on Win11 / Python 3.14):
bsdf.total_integrated_scatter: 188x -- per-(θ_i, θ_s) loop replaced with vectorised meshgrid +np.trapz.- Shack-Hartmann WFS (
analysis/detector.py): 10.8x -- per-lenslet FFT loops batched as a singlenp.fft.fft2on a stacked(n_lenslets², sa_pixels, sa_pixels)array. thin_grating_efficiency_1d: 10.5x -- per-diffraction-order Python loop vectorised.seidel_field_sweep: 4 - 72x -- analytical chief-ray linearity (S1, S4 ∝ 1; S2, y_chief ∝ σ; S3 ∝ σ²; S5 ∝ σ³) replaces the per-field re-trace. All field-independent work (glass indices, ABCD, marginal-ray trace) hoisted out of the loop. Machine-precision agreement with the pre-hoist reference.- Freeform Chebyshev: 4.43x --
arccos(rho)hoisted out of the per-order loop;T_i(rho)factors cached per polynomial order. - Coating stack (50 layers): 3.26x -- tournament matmul
reduction on a
(N, 2, 2)per-layer tensor; scalar Snell chain usesmath.sin/math.asin(bit-identical via libm). _fd_grad_pureforward-FD opt-in: 2.23x -- now acceptsscheme='central'|'forward', default'central'. Default preserves bit-identical pre-v4.13 gradient values; forward path trades O(h²) for O(h) for an N+1 eval count (or N whenf0=<value>is supplied).wave_opd_2d: 1.89x -- two-passnp.unwrap(axis=...)replaces the per-pixel Python unwrap loop.
rcwa.py → thin_grating.py rename (no back-compat shim)
The file name "rcwa" was misleading: the function inside is the
analytical scalar thin-phase grating formula, not rigorous
coupled-wave analysis. v4.4.0 renamed the public symbols
(thin_grating_efficiency_1d, grating_efficiency_vs_wavelength);
v4.13.0 finishes the rename at the file level:
lumenairy/elements/rcwa.py→lumenairy/elements/thin_grating.pylumenairy/ui/rcwa_dock.py→lumenairy/ui/thin_grating_dock.py(classRCWADock→ThinGratingDock)
import lumenairy.elements.rcwa raises ModuleNotFoundError.
Stage gates worth knowing
- γ.1 (HFPI bincount swap) reverted before ship. NumPy ≥ 1.25
vectorised
np.add.atfor complex via_PyArray_UFuncBufferedAtVectorized, making the bincount path a perf wash (0.4 - 1.0x) on the shipping toolchain. No behavioural change vs v4.12.2 inaccumulate_to_grid. - β.3 (
_fd_grad_for) parameterised, not switched. Default staysscheme='central';design_optimize._merit_jac_autokeeps the default explicitly so existing optimisation runs see no behavioural change. Forward-FD is opt-in per call.
What's new in 4.12.2
Closes the round-5 / v4.12.1 pre-PyPI audit blockers. Three
documentation-drift items become true (NumPy through_focus_scan
H-hoist actually implemented, through_focus_scan_jax JIT cache
actually implemented, 878× benchmark headline reconciled to
~300×). Cache hygiene infrastructure landed: clear_asm_caches()
extended, unbounded jit caches converted to LRU(32), 4 new public
clear_*_cache() helpers, all wired into lumenairy_context (clear_caches_on_exit=True). All 482 unit tests pass.
Pre-PyPI release blockers fixed
pytest-benchmarkadded todevextra -- the release-notes claim since v4.12.0 is now true;pip install lumenairy[dev]installs it.benchpytest marker registered sopytest benchmarks/ --benchmark-onlycollects under--strict-markers.- Cache-clear / FFT-toggle helpers exposed at the top level --
la.set_fft_auto_promote,la.clear_zernike_basis_cache,la.clear_lg_polynomial_cache, and four newla.clear_*_cache()helpers (trace_jax, through_focus_scan_jax, propagate_through_system_jax, phase-retrieval).
Documentation drift made true
- NumPy
through_focus_scanH-hoist actually implemented. Input FFT, kx/ky, propagating mask, and target dtype now hoisted outside the z-loop. v4.12.0's 4.7× speedup came from underlying pyFFTW MEASURE; the H-hoist now stacks on top. through_focus_scan_jaxJIT cache actually implemented. Module-scope OrderedDict + LRU(32) caches the compiled vmapped kernel per signature. First call ~150 ms; warm ~2 ms. ~77× at N=64.
Cache hygiene infrastructure
clear_asm_caches()extended to also drop_PYFFTW_PLAN_CACHE_PYFFTW_BAD_SHAPES.
_PROPAGATE_SYSTEM_JAX_CACHE,_TRACE_JAX_CACHE, and the three phase-retrieval kernel caches converted from unboundedDict[Any, Any]toOrderedDict+ LRU(32). No more compiled- XLA-binary leaks under iterative optimisation.lumenairy_context(clear_caches_on_exit=True)now calls all sixclear_*helpers, each guarded.
878× → ~300× benchmark reconciliation
v4.12.1 release notes claimed trace_jax warm-call 878× at
127 ms → 0.40 ms. Those don't reconcile (127/0.40 = 317×).
Fresh measurement: first 140 ms, warm 0.47 ms, speedup
~300×. Corrected everywhere.
Coverage test strengthened
Zemax coord-break STOP-marker test now actually places the coord-break BEFORE the stop (exercises the pre-v4.11.2 off-by-one bug); the v4.12.1 version placed it after, which didn't.
Known limitations deferred to v4.13/v4.14
CHANGELOG documents the silent-data-loss class (S1: io/storage
append-side complex128 hardcode; S2: codegen aperture-stop drop +
1.31 µm wavelength default; S3: ghost.py R convention conflict)
and structural/latent items (L2: JAX complex64 hard-casts; L3:
PropagationResult missing dy; L4: sibling-gap remnants; L5: 346
except Exception: clauses; L6: apply_mirror array_namespace;
L8: zarr thread-safety).
What's new in 4.12.1
Closes the three perf items deferred from v4.12.0 plus the 14 missing regression tests from the round-4 audit. All 453 unit tests pass; full validation suite (34 files / 314 tests) passes.
Performance recovered
trace_jaxwarm-call: ~300x (140 ms cold -> 0.47 ms warm, median of 20 warm calls on a 1001-ray AC254-100-C doublet) via a pytree-registeredJaxPrescriptionwrapper + tracer-detection bypass that preservesjax.gradsemantics. v4.12.0 reverted this because the flat-tuple cache producedjax.grad = NaN; root cause turned out to be a JAX lstsq backward bug, not the cache key. Fix: bypass the jit-cache layer when any leaf is a tracer. (The pre-release notes quoted 878x with inconsistent absolute timings; v4.12.2 reconciled this after re-running on a stable system state.)- Raytrace Newton spherical fast-path: 1.50x on 1k-ray doublet traces. v4.12.0 attempted 1.64x by skipping Newton AND switching to the analytic spherical normal; the normal change compounded a 1.17e-3 NumPy<->JAX drift through Maslov asymptotic. v4.12.1 ships only the Newton skip; the normal computation stays bit-identical to v4.11.2.
- B1-10 half-pixel grid drift unified -- five propagator- family files (gbd, mhs x2, subaperture, optimize/core) switched from cell-centred to pixel-centred convention, matching the library-wide ASM / Fresnel / RS / sources standard.
14 round-4 coverage-gap tests added
Each v4.11.2 fix that landed in code without a pin now has one:
compute_psf non-square pupil, apply_detector non-integer
ratio, find_best_focus NaN guard, MC tolerancing a_k>=0
clamp, load_material dispersion-drop warning, Source.*
**factory_kwargs, apply_real_lens_traced M_x/M_y, NaN
sentinel mask, stop_index warns, freeform RuntimeWarning,
Zemax coord-break STOP, JAX<->NumPy phase-retrieval parity,
Cassegrain S1/S2/S3/S5 hand-derived, Richards-Wolf vs paraxial
Airy at low NA.
Other
- 2 weak v4.11.2 tests strengthened (behavioural pins replace source-string scans).
- Residual
try/except: skippedremoved fromvalidation/io/test_io.py:196. - Both cross-backend critical tests that caught v4.12.0's reverts
now pass:
aberration_tensor_lg00_jax matches NumPyrel_err = 4.53e-04;jax.grad through fit_canonical_polynomials _jax is finitegrad = 1.0274e+04.
What's new in 4.12.0
Combined performance pass + round-4 pre-PyPI audit.
Performance — Tier-1 speedups (vs v4.11.2)
- ASM propagate 1024^2: 4.3x -- pyFFTW double-buffer cache + auto-promote ESTIMATE -> MEASURE.
through_focus_scan7-pt N=256: 4.7x -- input FFT and kx/ky hoisted outside the z-loop.propagate_through_system_jaxwarm: 163x -- module-scope jit caches at the JAX system entry.gerchberg_saxton_jax/error_reduction_jax: 36-46x steady-state -- jit-cached iteration kernel.propagate_hf_chebyshev_quadrature: up to 22x -- replaced scalar pixel loop with vectorised chunk broadcast +np.einsum.zernike_basis_matrixwarm cache: ~12000x (22 ms -> 1.8 us);zernike_decompose10-call loop: 3.7x.
The 4.7x through_focus_scan speedup propagates through
MultiWavelengthMerit / MultiFieldMerit / Monte-Carlo
tolerancing: 100-trial MC at 31-pt N=256 drops from 71 s to
15 s.
Round-4 audit: ~20 PyPI release blockers fixed
- README cookbook examples now runnable -- 11 broken positional / renamed / missing-kwarg calls fixed.
- Back-compat deprecation shims for
load_zmx_prescriptionandload_zemax_prescription_txt. - JAX
propagate_through_system_jaxunified aperture schema with NumPy; raisesNotImplementedErrorup-front for non-traceable element types. - Rayleigh-Sommerfeld
z<=0guard matches Fresnel / Fraunhofer / SAS. - Dispatcher negative-z routing and
output_gridfor ASM family correctly auto-promote / raise. _apply_doe_kick_jaxgradient flow preserved.makedammann2dglobal RNG no longer mutated.image_plane_wfereference-sphere1/N_chieffor off- axis fields.distortion_gridL^2+M^2 < 1guard.apply_real_lens_tracedmirror guard with a properly named error.gerchberg_saxton(backend='jax')forwardsseed/dtype/initial_phaseend-to-end.ghost.pydocstring clarifies the'intensity'upper-bound caveat.
Deferred to v4.12.1
- Raytrace Newton spherical fast-path (caused a 1.17e-3 NumPy<->JAX drift).
trace_jaxjit cache (brokejax.grad(fit_canonical _polynomials_jax)).- B1-10 half-pixel grid convention drift between propagator families.
Tooling
- New
benchmarks/directory withpytest-benchmarkper-area perf tests. - All 390 unit tests pass; full validation suite (34 files / 314 tests) passes.
See CHANGELOG.md for the full per-finding breakdown.
What's new in 4.11.2
Round-3 fresh-eyes audit response. An 11-agent fresh-eyes audit of v4.11.1 surfaced ~120 new substantive findings. v4.11.2 closes ~70 of the highest-impact findings across seven parallel work tracks plus reconciliation, and ships ~55 new pinning regression tests.
Headline meta-finding: the v4.10 "C-LR-1 fix" was itself wrong.
The pre-v4.10 sign on the Seidel-correction OPL inside
apply_real_lens was correct; the round-1 audit's physics-reasoning
step that justified flipping it was reversed. v4.11.2 restores the
original sign and pins it with a ground-truth regression test
against apply_real_lens_traced.
Other highest-impact fixes:
- GBD axial-OPL fix activated (v4.11.1 version was calling
.geton a dataclass — silently swallowed AttributeError, axial_opl always None). - S-LAH64 / S-LAH79 Sellmeier coefficients removed (in-code values were off by 5-6% in n_d; now route through refractiveindex.info).
- Chained-mirror Seidel parity tracked across
system_abcdandseidel_coefficients; Cassegrain / Schwarzschild correct now. - EVENASPH PARM off-by-one in Zemax loader fixed (every Zemax- authored EVENASPH file previously lost its α₄ on load).
- Quadoa aspheric serializer wrote powers instead of values.
normalize_prescriptionmirror filter was a no-op (wrong key).- HFPI finite-conjugate path was killing all rays; stratified sampler was sampling only 2 of 16 strata.
- Richards-Wolf prefactor
1/f²+ sign restored. compute_psfParseval test was passing for the wrong reason.
~55 new regression tests; full validation suite (34 files, 314 tests) passes.
See CHANGELOG.md and AUDIT_ROUND3_2026_05_16.md for the full
per-finding breakdown.
What's new in 4.11.1
Round-2 verification follow-up. An independent verification of the v4.10 / v4.11.0 fix wave identified five fixes that had landed dead-on-arrival (call-signature mistakes / wrong-API lookups), several new bugs the fix wave introduced, three unfixed JAX silent- failure paths, and one over-coarse threshold. 4.11.1 closes all of these and ships the first round of pinning regression tests.
- MultiWavelengthMerit chromatic optimisation actually runs now
(was a no-op for the entire v4.10 series -- positional call to a
keyword-only
apply_real_lensraised TypeError on every iteration, swallowed by a bare except). - Decentered aperture stop is honoured (the 4.10.2 fix called
getattron a dict with the wrong key names and was inoperative). - Circular-polarisation handedness consistent across
create_circular_polarized,apply_waveplate,stokes_parameters, andvector_diffraction.py-- all three sites now agree onS3 > 0 = "right". - Richards-Wolf rim mask built before clipping (was identically True over the whole grid -- geometric pupil unenforced).
- JAX intersect alive-masking propagates disc<0, non-finite t,
and Newton-stuck rays into
state.alive. _refract/_reflectclearrays.alive(not onlyerror_code) on direction-vector collapse.create_point_sourcecentral pixel is now bounded byamplitude / dx(was 1e30 from a1e-30floor on r).- GBD-through-prescription populates
axial_opl; reconstruction carries the system's absolute axial phase reference. - Test coverage: 9 new pinning regression tests
(
tests/unit/test_audit_fixes_v4_11_1.py). All 179 unit tests pass; the v4.10 series shipped with zero new test files.
See CHANGELOG.md for the full per-finding breakdown.
What's new in 4.11.0
Multi-agent physics audit response — full series. 4.11.0 closes the converged findings from three independent audit runs (one external 8-agent audit, two internal multi-agent audits) of the v4.9.0 codebase. Across five patch releases (4.10.0 → 4.10.1 → 4.10.2 → 4.10.3 → 4.11.0) ~100 audit findings were addressed. All 34 validation files (314 tests) pass.
Highest-impact fixes
- Mirror Seidel coefficients no longer zero. Every reflective /
catadioptric system silently reported "diffraction-limited"
Seidel sums pre-4.11 because the mirror branch in
seidel_coefficientsupdated ray heights but never wrote S1..S5. Fixed via Welford form with n2 = -n1. - Exit-pupil radius uses transverse magnification 1/D (was D, the angular magnification). Off by 1/D² for non-trivial post- stop systems.
- Tilted-ASM band-limit now centred on the original-frame
spectrum FX + fx0. Pre-4.11 the default
bandlimit=Truezeroed the propagated field for any non-trivial tilt. - Rayleigh-Sommerfeld kernel sign flipped to the Goodman 3-43 form (1/r − ik). Coherent superposition of RS with ASM / Fresnel was 180° out of phase pre-4.11.
- Richards-Wolf adds the missing 1/√(cos θ) Jacobian and the -ikf/(2π)·exp(-ikf) prefactor. High-NA focal-plane amplitudes are now physical.
- Coord-break order matches Zemax PARM 6 default (decenter- then-tilt). Imported folded designs now get the correct frame transform.
MultiWavelengthMeritactually re-propagates the wave leg per wavelength (was a no-op chromatic constraint pre-4.11).create_circular_polarized('right')returns the RHC Jones vector (1, -i)/sqrt(2) under the library's exp(-i omega t) convention.- JAX trace correctness floor: sign(R) on sag derivatives, TIR
double-where for finite
jax.grad,trace_jaxraises on unsupported surface types instead of treating them as flat refractive. - HFPI Kirchhoff weighting now includes the 1/(i*lambda) prefactor and solid-angle Monte-Carlo weight. Absolute amplitudes were unphysical by ~10^6 pre-4.11.
- Shack-Hartmann drops the bogus wavelength/(2π) factor on wavefronts; reference-centroid calibration pass added.
through_focus_scan_jaxnow usesjax.vmap(was a Python for-loop) -- ~5-15x speedup, more on GPU.
New / improved API
ChromaticFocalShiftMerit(wavelengths=[...])is self-contained.check_sampling_conditions(NA=...)for relaxed Nyquist.register_fixed_glassworks withoutrefractiveindexinstalled.- Phase-retrieval functions accept
seed=anddtype=. - Source factories (
create_top_hat_beam,create_annular_beam,create_bessel_beam) acceptdyfor anamorphic grids. apply_real_lensrespects decentered stop apertures.
Documented limitations (carried forward)
_transfer_jaxuses the paraxial formx += L·thickness. The math-correctt = (thickness − z)/Nintroduces a gradient instability throughfit_canonical_polynomials_jaxwhose root cause needs deeper investigation. Paraxial form is accurate to ~1 % for NA ≤ 0.1.
See CHANGELOG.md for the full per-release fix list and the explicit "not addressed" items with rationale.
What's new in 4.9.0
External-audit response + scoped runtime-environment manager.
4.9 bundles the 4.8.1 lumenairy_context work with correctness
fixes from the v4.8.0 external audit
(LumenAiry_Audit_Report.md). 170 unit tests + 34 validation
files pass.
Audit fixes (physics)
- #2.1 Seidel formula --
seidel_coefficientsnow usesΔ(u/n) = u_after/n2 − u_before/n1(Welford 8.46), not the pre-4.9Δ(1/n)shorthand. Buggy magnitudes were off by 1.5×-5× per surface depending on incidence angle / index; the flat- refracting-surface branch (which zeroed S1/S2/S3) is also fixed. - #2.5
aberration_tensorℓ ≠ 0 outputs -- pre-4.9 the chief-ray projection collapsed to the constant term of the LG output polynomial, silently returning zero for any ℓ ≠ 0 mode (coma, astigmatism, tilt). 4.9 does the full output-plane σ-integration viapropagate_modal_asymptotic+decompose_lg; ℓ ≠ 0 modes now carry real physical meaning. - #4.6 / #4.7
seidel_wfePetzval H² -- uses the Lagrange invariant|H|²instead of baresigma²(off by (D/2)² ~ 100× for a typical singlet); S5 Schwarzschild relation also picks up the missing H² factor. - #2.2 GBD axial phase --
exp(1j·k·t)instead ofexp(1j·k·abs(t)); back-propagation now round-trips. - #2.3 Coronagraph λ/D scaling --
coronagraph_contrast_curvenow usespupil_diameter_mto compute the correctpix_per_lam_over_D = λ·f/(D·dx_focal); pre-4.9 hard-codedN(correct only for FFT-natural pitch). - #3.3 Fresnel/Fraunhofer/SAS z<=0 guards -- these forward-only
propagators now raise
ValueErroron negative or zero z with a pointer to ASM / RS for back-propagation. - #3.5 TIR mask placement -- runs for
slant_correction=Trueeven whenfresnel=False. - #4.5 Cosmic-ray scaling -- new
cosmic_ray_rate_per_m2_per_skwarg scales by detector area · exposure time; legacycosmic_ray_ratedeprecation-warns.
Audit fixes (small / documentation)
- #4.3
dx > 1 mmvalidator loosened todx > 100 mm(was rejecting large-telescope pupils). - #4.4 Zemax INCH / INCHES aliases added.
- #2.4 / #3.1 / #3.2 / #3.4 / #4.2 docstring warnings on coating Snell simplification, HFPI absolute-amplitude non-normalisation, GBD position-only limitation, real-lens scalar Fresnel applicability, and LG polynomial normalisation.
New (4.8.1 work bundled in)
lumenairy_context(**kwargs)-- scope a block of code with isolated library runtime settings (complex_dtype,pyfftw_planner,fft_threads,max_ram, ASM cache caps). Nests cleanly, restores on exception, optionalclear_caches_on_exit=True.dtype=kwarg on the 11 source factories --create_gaussian_beam(..., dtype=np.complex64)allocates explicitly; defaultdtype=Noneinherits from the global default.atexitauto-restore -- import-time defaults snapshot on firstimport lumenairy; restored on process shutdown to catch the long-Jupyter-session foot-gun whereset_default_complex_dtypewould otherwise leak.- New getters:
get_pyfftw_planner,get_fft_threads,get_asm_cache_size,get_max_ram.
What's new in 4.8.0
Library-correctness pass triggered by the external wiki audit.
4.8 fixes three verified bugs surfaced by the audit and closes the
two long-deferred items (DeformableMirror._IF_basis memory
foot-gun from the 4.0.1 deferred list; folded-design wave-optics
silent-drop from the 3.7.8 archive page). All fixes ship with
regression tests; 119 unit tests + 34 validation files pass.
Bug fixes
create_point_sourcesign convention. The formulaE = A * exp(+i*k*r) / rwas sign-independent inz0and always produced a diverging wave even when the caller requested converging. Fixed: switch the exponent sign based onz0—z0 <= 0→exp(+i*k*r)/r(diverging, source before grid),z0 > 0→exp(-i*k*r)/r(converging, focus after grid). Behaviour change: code that usedz0 > 0to model an outgoing wave needs to flip toz0 < 0.FocalLengthMerit/BackFocalLengthMeritafocal target. Docstring promised "pushing EFL toward infinity drives merit to zero" but the code returnedweight * efl**2, growing without bound. Switched thetarget == 0branch to penalise optical power(1/efl)^2, so merit → 0 asefl → ∞. Optimisers targeting a collimator now have the correct gradient.
Long-deferred items now fixed
DeformableMirror._IF_basismemory foot-gun. Eager pre-allocation of the(n_act, n_act, N, N)influence-function stack was 8 GB float64 for a 32×32 actuators × 1024×1024 pupil config (deferred since the 4.0.1 audit). Newcache_basis = {'auto', True, False}flag (default'auto') caches eagerly below a 512 MB ceiling and switches to on-demand evaluation past that — same FLOP count, no large allocation. NewDeformableMirror.fit_phase(target_phase)public modal-to- zonal projection helper, replacing the worked-example pattern of reaching into the private_IF_basisattribute.- Wave optics through folded designs silent-drop.
apply_real_lens,apply_real_lens_traced, andapply_real_lens_maslovwalkedprescription['surfaces'](refracting-only) and silently ignored mirrors inprescription['elements']; for a folded.zmxthis propagated the unfolded-equivalent path with the mirror's focusing phase and world-frame axis change dropped. All three entry points now raise aValueErrorwith a clear message unless the caller acknowledges the unfolded-equivalent treatment viaprescription['allow_unfolded_equivalent'] = True. New public helperssplit_prescription_at_mirrors(rx)andhas_mirrors(rx)support the explicit segment-by-segment workflow.
Wiki audit closeout (cross-cutting)
The companion wiki underwent a comprehensive review pass: all 20
critical, all 58 major, and ~290 of 294 medium findings addressed
across 18 commits. Highlights: ASM aliasing thresholds documented
in Tutorial / Quickstart; test-count contradictions reconciled
(34 files / ~700 tests / 119 unit); Physics XII §1 / §5 photon-noise
formulas corrected; Marechal-Strehl exponent fixed; Fresnel-number
aliasing criterion corrected N²/4 → N/4; new FR-Memory page
documenting RAM + FFT-planner helpers. The full audit response is
documented in the wiki Release Notes.
What's new in 4.7.0
Polish-pass release + API-consistency pass. 4.7 implements the verified items from the polish-pass audit, including the J.2 API-consistency work scheduled for this milestone. Breaking changes (no deprecation cycle, since the library has a single user at present): see below.
Breaking changes
- Lens-function args are keyword-only past
E_in-- all 8apply_*_lensentry points (apply_thin_lens,apply_spherical_lens,apply_aspheric_lens,apply_cylindrical_lens,apply_grin_lens,apply_real_lens,apply_real_lens_traced,apply_real_lens_maslov) now enforce keyword-only arguments after the input field. This removes the positional inconsistency wherewavelengthlived at pos 3 in some, 5 in others, and 6 inapply_spherical_lens. apply_real_lenstrio:lens_prescription=→prescription=(matching the rest of the library, 54 prior uses).- Diffractive-lens factories drop the
_msuffix --create_diffractive_lens,create_kinoform,create_fresnel_zone_platenow takedx,focal_length,wavelength(no suffix). Library uses SI metres throughout. - Source-factory ordering standardised on
(N, dx, wavelength, *, source_specific).sigma/diameter/outer_diameter/inner_diameter/mode_field_diameterare now keyword-only. wavelengthis now required (no default) onkeplerian_telescope,beam_expander_prescription, every Zemax / CODE V / Quadoa exporter, andmake_ray. Removes the disagreeing defaults (550 nm vs 1310 nm) that surfaced in the audit.- Zemax loaders renamed:
load_zmx_prescription→load_zemax_zmx;load_zemax_prescription_txt→load_zemax_prescription_data_txt.
Added
- Input validation on every public propagator and on
surfaces_from_prescription-- catcheswavelength = 0,wavelength = 1.31(forgot units),dx <= 0, malformed prescriptions, NaN inputs, with messages that quote the parameter, value, and calling function. - Glass-registry rebuild -- callable entries via
GLASS_REGISTRY[name] = lambda wl: n; 30 new bundled Sellmeier glasses (N-FK51A, N-SF57, N-LASF44, S-LAH64, ...);list_glasses()/search_glasses()helpers; typo suggestions on unknown names. dy=Nonekwarg on theapply_real_lenstrio for API symmetry.- Dataclass returns for
distortion_grid,footprint_per_surface,spot_diagram_vs_field(with dict-style subscript preserved). - Canonical-order propagator aliases:
propagate_gbd,propagate_hfpi,propagate_huygens_fresnelall share(E_in, z, wavelength, dx, ...)matching ASM. asm_propagate+which_propagator-- auto-selector and advisor for the ASM family (plain / tilted / MFT / SAS / Fraunhofer) based on geometry.plot_lens_layout(prescription, ...)-- script/notebook cross-section drawing extracted from the GUI'sLayout2DView.plot_glass_map+abbe_diagram-- glass-catalogue scatter / dispersion plots extracted from the GUI'sglass_map_dock.py.typedmarker so type checkers honour the library's hints. 4.7's annotation pass took coverage from 28.5% to 90.2% (>=partial) and 10.5% to 70.1% (fully annotated).pytest validation/-- the 670-test validation suite is now pytest-discoverable via avalidation/conftest.pyadapter. The legacypython validation/run_all.pydriver keeps working; the new path adds IDE test discovery,-kfiltering, JUnit XML, and parallel execution viapytest-xdist.backend='jax'kwarg unifies the_jax-suffixed analysis siblings (Gerchberg-Saxton, error reduction, HIO, through-focus scan, Monte-Carlo tolerancing).- Array-namespace dispatch documented: passing a CuPy or
JAX array as
E_inautomatically routes the entire pipeline through that backend.use_gpu=Trueremains as a back-compat way to force NumPy→GPU promotion.
Packaging hygiene
-
Module-level
__all__in every analysis submodule. -
lumenairy.analysis.analysis→lumenairy.analysis.core(with back-compat shim). -
PyPI
Changelog/ReleasesURLs inpyproject.toml. -
Propagator input validation. Every public propagator (ASM, Fresnel, Fraunhofer, RS, scalable ASM, the MFT variants, batch and tilted) now calls a shared
_validate_propagator_inputshelper at the entry point. It catches the silent-failure regimes the old code shipped with --wavelength = 0(wasZeroDivisionError),wavelength = 1.31(forgot the e-6, was silent garbage),dx = 0(wasZeroDivisionError),dx = 2.0(forgot units, was silent garbage), 3-D / 1-D / empty inputs, non-finitez. The error message quotes the parameter, the value, and the calling function. -
Prescription validation. New public
lumenairy.validate_prescription(prescription, *, strict=True), also called internally bysurfaces_from_prescription. Catches empty dict, missing keys, surface/thickness length mismatch, NaN radius, bad aperture, etc., with a precise message. -
Glass-registry rebuild.
GLASS_REGISTRYentries can now be a custom callablef(wavelength_m) -> n_real_or_complexso you register custom dispersion (Cauchy, temperature-dependent, etc.) with a one-line lambda. A bundledSELLMEIER_COEFFICIENTStable adds ~30 Schott / Ohara entries (N-FK51A, N-SF57, N-LASF44, S-LAH64, etc.) usable without therefractiveindexpackage.list_glasses()/search_glasses(pattern)helpers + typo suggestions on unknown glass names. -
dykwarg on the lens trio.apply_real_lens,apply_real_lens_traced, andapply_real_lens_maslovnow takedy=None.apply_real_lenshonours non-square pixels through the per-surface phase screens and in-glass ASM; the traced / Maslov variants accept the kwarg and raise ondy != dx. -
Field-analysis dataclass returns.
distortion_grid,footprint_per_surface, andspot_diagram_vs_fieldnow return named dataclasses (DistortionGrid,SurfaceFootprint+FieldFootprint,SpotDiagramField) -- closing the inconsistency with the rest of the 4.4 field-analysis suite. Dict-style indexing keeps working for back-compat. -
Module rename.
lumenairy.analysis.analysis→lumenairy.analysis.core. Back-compat shim preserves the old dotted import. -
Packaging hygiene. Module-level
__all__in every analysis submodule. PyPIChangelog+ReleasesURLs.
What's new in 4.6.0
Documentation overhaul -- decision-tree front door + lens-family cross-refs. No code changes; the public API is identical to 4.5.0.
- README rewritten around "which function should I use?" -- the
former Quick Start was replaced with a decision-tree-style set of
"if you need X, use Y" tables covering free-space propagation,
lens application (the three
apply_real_lens*variants), folded designs, field analysis, and design optimisation. Three minimal end-to-end examples follow; longer recipes moved into a "Cookbook" section near the end. apply_real_lens/_traced/_maslovdocstrings cross-link to each other. Each now opens with aSee Alsoblock + a one-line selection guide so the choice between the three fidelity points is visible right at the top ofhelp(la.apply_real_lens).- Dense physics citations moved out of the README main flow. Matsushima-Shimobaba, Heintzmann-Loetgering-Wechsler kernel details, and the per-variant real-lens accuracy strategy now live in an "Appendix: Physics references" at the bottom -- still here for anyone who wants them, just out of the way for first-read.
What's new in 4.5.0
World-frame ray tracing for folded prescriptions, end-to-end.
Closes the deferred gap from 4.4: folded .zmx designs can now be
loaded, world-frame-traced, and analyzed paraxially from any script,
without instantiating the GUI's SystemModel. Pure-additive; no
breaking changes.
-
Mirror inference (
surfaces_from_prescription). A prescription surface withglass_after='MIRROR'(the Zemax convention fromload_zmx_prescription) is now auto-detected and emitted withis_mirror=True, andglass_afteris normalised back toglass_before(since reflection does not change the surrounding medium). Previously this flag was silently dropped, and a folded prescription needed manual fix-up. No effect on un-folded prescriptions. -
paraxial_focus_world(world_surfaces, wavelength)-- returns the world-coordinate(focus_origin, focus_normal)of the paraxial image plane. Traces a chief + paraxial-marginal ray through the world surfaces and finds their closest-approach point, so it works correctly on folded prescriptions where the unfolded BFL would carry the wrong sign for a direct world-frame walk. -
Folded-design validation suite. New
validation/raytrace/ test_folded_designs.pybuilds a periscope (plano-convex singlet + 45-deg flat fold mirror) and a 45-deg-tilted concave spherical mirror from prescription dicts, then verifies:- The fold mirror lands at the correct world coordinate
(0, 0, 53mm)/(0, 0, 100mm)and the detector lands 50mm(100mm)post-fold in+y. - An on-axis chief ray reflects off the mirror and lands at the detector vertex.
paraxial_focus_worldreturns the correct image-plane world position for both designs. For the curved fold the result matches the analytical tangential focal lengthf_t = R/2 cos(45 deg)~ 70.7mm, the obliquity-induced astigmatism that a paraxial trace through a tilted spherical mirror produces.- Straight-axis singlets give a world focus identical to
last_surface_z + BFL.
- The fold mirror lands at the correct world coordinate
-
All 34 validation files pass (33 from 4.4 + 1 new), 52 unit tests still green.
What's new in 4.4.0
Field-resolved analyses lifted from GUI + world-frame surface builder for folded designs + Strehl alternatives + grating rename + glass-catalog warnings. Mostly additive; one breaking name change.
-
World-frame surface builder (
la.world_surfaces_from_prescription). Previously, building world-frame surfaces from a folded prescription (e.g. a.zmxwith COORDBRK surfaces) required going through the GUI'sSystemModel. 4.4 lifts that translation into the library:presc = la.load_zemax_zmx('folded_design.zmx') surfaces = la.world_surfaces_from_prescription(presc) result = la.trace_world(rays, surfaces, 1.31e-6)
The builder walks the combined coord-break + optical-surface sequence in
surf_numorder, applies tilts (about local x/y/z) and decenters with Zemax PARM ordering, and emitsSurfaceobjects withworld_origin(m) andworld_R(3×3) populated. For prescriptions without coord-breaks the result is bit-identical to the local-frame trace; for folded designs it gives correct geometry without touching the GUI.la.trace_worldis now also exported at top level. -
Eight new field-resolved analyses (
lumenairy.analysis.field). Functions that previously lived only insideui/*_dock.pyare now first-class public API. GUI docks still work -- they were refactored to delegate to these public functions.distortion_vs_field,distortion_grid-- chief-ray f-tan(theta) distortion (sweep + 2-D grid).footprint_per_surface-- per-surface ray footprints grouped by field angle.spot_diagram_vs_field-- spot diagrams at multiple field angles on a common image plane.sensitivity_ranking-- central-difference d(merit)/d(var) for an arbitrary parameter vector.relative_illumination-- geometric vignetting vs field (new).field_aberration_sweep-- real-ray sag/tan focus shifts and astigmatism vs field; companion to the paraxialseidel_field_sweepfrom 4.3.petzval_radius-- paraxial Petzval surface radius helper.- Internal
_tracedispatcher routes folded-design surface lists totrace_worldand prescription-based surface lists totrace, so the same public functions handle both.
-
Alternative Strehl definitions.
strehl_ratio(peak-ratio) is biased high on asymmetric PSFs where the peak shifts. Two new textbook definitions:strehl_marechal(rms_waves)-- closed-formexp(-(2 pi sigma)^2)approximation; ~0.82 at 1/14 wave.strehl_phase_integral(pupil)-- exact small-aberration|<A e^(i phi)>|^2 / <A>^2(Born & Wolf 9.1.10), avoids peak-finding bias entirely.
-
aberration_summarywarns loudly on unknown glass. Previously a missing glass would return zero-filled Seidel coefficients with the error silently appended to anoteslist -- making an unanalysable system look diffraction-limited. 4.4 issues aUserWarningwhile preserving the zero-fill behaviour for back-compat. -
Breaking name change:
rcwa_1dremoved. The function name advertised full Rigorous Coupled-Wave Analysis but the implementation has always been an analytical thin-grating scalar approximation. Callthin_grating_efficiency_1dinstead -- same signature, same output. The 4.0.1 alias is now the canonical name. -
Packaging fix:
validation/andtests/ship in the sdist. AddedMANIFEST.inso a downloaded source distribution actually contains the validation suite. -
GUI internals: four docks refactored.
distortion_dock,footprint_dock,spot_field_dock, andsensitivity_docknow delegate to the new public functions instead of recomputing inline. Rendering is unchanged. -
Validation: 1 new validation file (22 tests, all pass); 30 pre-existing files still pass after the rename and dock refactors.
What's new in 4.3.0
Diffractive optics, off-axis Seidel, module organization, unit-test layer. A four-stream release that closes audit gaps without breaking any existing API. All 31 validation files and 52 new unit tests pass.
-
Diffractive lens trio. Three new factory functions in
lumenairy.elements.doe, all exposed at top-level:create_diffractive_lens(N, dx_m, focal_length_m, wavelength_m)-- continuous-phase thin-lens equivalentexp(-i k r^2 / (2 f)).create_kinoform(N, dx_m, focal_length_m, wavelength_m, n_levels=8)-- quantized-phase lens. Efficiencyeta ~ sinc^2(1/n_levels): ~81% at 4 levels, ~95% at 8, ~99% at 16.create_fresnel_zone_plate(..., binary=True, n_zones=None)-- classical amplitude FZP (binary=True, default; ~10% efficiency) or Rayleigh-Wood phase FZP (binary=False; ~40% efficiency).n_zonescrops to a finite aperture.
-
Off-axis Seidel analysis (
lumenairy.raytrace.seidel_analysis). The existingseidel_coefficientsis on-axis only by design. Two new helpers extend it without breaking it:seidel_field_sweep(surfaces, wavelength, field_heights)-- evaluates the five Hopkins sums at a grid of field heights in one call. Returns per-surface arrays of shape(N_surfaces, N_fields)and total sums of shape(N_fields,), suitable for plotting S1-S5 vs field angle.seidel_wfe(seidel_result, rho, theta, ...)-- reconstructsW(rho, theta)from a Seidel total dict using the standard Welford expansion (S1 rho^4 / 8 + S2 rho^3 cos / 2 + S3 rho^2 cos^2 / 2 + S4 sigma^2 rho^2 / 4 + S5 rho cos / 2).seidel_coefficientsnow returns thefield_angleused in the result dict soseidel_wfecan apply the Petzvalsigma^2scaling automatically.
-
Module organization polish. Two functions that lived in the "wrong" subpackage are now in their conceptual home; back-compat shims keep every existing import path working:
lumenairy.ao->lumenairy.analysis.ao. AO primitives (DeformableMirror,apply_dm,LeakyIntegrator,zernike_modal_basis,slope_to_modal) are an analysis / control layer, not a top-level element family. Old import pathfrom lumenairy.ao import ...still works.coronagraph_contrast_curvemoved fromlumenairy.elements.elementsto a newlumenairy.analysis.coronagraph(it's a post-processing analysis function, not an element factory). Old import paths still work via a deferred-import shim.- New
lumenairy.elements.coronagraphnamespace module re-exports the four coronagraph builders (apply_lyot_focal_plane_mask,apply_vortex_phase_mask,apply_lyot_stop,apply_apodized_pupil) for discoverability.
-
Unit-test layer (
tests/unit/). Five new modules (test_elements_lens.py,test_propagation.py,test_sources.py,test_analysis.py,test_raytrace.py) plus a sharedtests/conftest.pygive 52 fast API-contract tests that finish in <1 second total -- complementing (not replacing) the ~30-minute integration validation suite. The existing pytest-wrapper aroundvalidation/moved totests/integration/test_validation_files.py. Run the unit layer alone withpytest tests/unit; run everything including the validation subprocess wrapper withpytest tests/ -m "unit or integration". -
Pure-additive overall. Every existing import path (
lumenairy.ao,lumenairy.elements.coronagraph_contrast_curve, the unchangedseidel_coefficientsshape) still works. No breaking changes.
What's new in 4.0.1
Bug-fix patch from the post-4.0 deep audit. A multi-agent correctness / performance / API / scope review of 4.0.0 surfaced five real or latent bugs. This release ships fixes + regression tests for each. Pure-additive; no breaking changes.
eval_image_plane_wfechief-ray N-fallback now usesabs(N) < eps -> naninstead of an exact-zero check with a non-physical unit-vector default (4.0's off-axis path had this).- Ray-trace
_refract/_reflectrenormalisation no longer silently promotes a zero-magnitude direction-cosine to a bogus unit vector -- it flags the ray dead withRAY_NAN. BSDFModelbase class is now an explicitabc.ABC; direct instantiation raisesTypeErrorat construction instead of deferring to aNotImplementedErrorat first method call.thin_grating_efficiency_1dis a new honest-name alias forrcwa_1d, which is an analytical thin-grating scalar approximation (not full RCWA) -- the namercwa_1dwas misadvertising.create_hermite_gauss/create_laguerre_gaussdocstrings now call out the normalisation inconsistency with the asymptotic- modal-propagator's analytical normalisation (a documentation fix, not a code change).
29/29 validation files still pass; 4 new regression tests guard each bug.
What's new in 4.0.0
Polish + Tier-1-gap-closing release. A four-tier audit of the library (API consistency, sign / unit conventions, cross-module pipelines, peer-library feature parity) surfaced a set of verified bugs and genuine functional gaps; 4.0 ships fixes for all of them plus a batch of helpers that compose with the existing infrastructure. Pure-additive with no breaking changes.
Adaptive optics primitives (new module lumenairy.ao). Closed-
loop AO building blocks: DeformableMirror (Gaussian-IF actuator
grid), zernike_modal_basis + slope_to_modal (SH-WFS modal
reconstructor), LeakyIntegrator (control law). Compose with the
existing shack_hartmann and generate_turbulence_screen into a
full single-conjugate AO loop. See Function Reference - Adaptive
Optics.
Off-axis image-plane WFE. eval_image_plane_wfe now accepts
field=(Hx, Hy) + field_max_m for any field point (previously
raised NotImplementedError for non-zero field). New
field_grid_wfe(prescription, wavelength, field_max_m, n_field)
runs the WFE across an n_field x n_field grid and returns the
PV/RMS/Strehl maps -- the standard "image quality across the field"
plot in one call.
More polish from the audit. coronagraph_contrast_curve for
radial contrast in lambda/D units; normalize_prescription to
unify schemas from the various loaders; paraxial helpers
(field_of_view, f_number, optical_invariant,
defocus_waves_to_zernike); JonesField.stokes_parameters() +
.degree_of_polarization() + .polarization_ellipse() bound
methods; apply_zernike_aberration and the HG/LG mode generators
gain dy kwargs for rectangular grids; storage now supports
preserve_dtype=True; detector model adds hot pixels, cosmic rays,
and Bayer-pattern colour filters.
Backend preservation through analysis. beam_centroid,
beam_d4sigma, beam_power, strehl_ratio, compute_psf,
compute_otf, compute_mtf, apply_aperture, and
apply_gaussian_aperture now dispatch through
array_namespace(...). CuPy / JAX field inputs no longer get
silently coerced to NumPy.
Source-normalisation control. create_gaussian_beam,
create_hermite_gauss, and create_laguerre_gauss all accept a
normalize kwarg with 'peak' / 'power' / 'none' options.
Each function preserves its historical default for backward
compatibility; pass normalize='power' to homogenise across the
source family.
Validation. 29/29 validation files pass (up from 27/27 in
3.9.0). Two new files (test_ao.py, test_coherence.py) plus
~30 new tests across the existing files.
New wiki pages. Function Reference - Adaptive Optics, Migration Guide (POPPy / HCIPy / prysm / Optiland / rayoptics -> LumenAiry mapping), and Glossary (sign conventions + when-to-use-which propagator cheat-sheet).
What's new in 3.9.0
Coronagraph & broadband-imaging building blocks. Four
coronagraph templates land as first-class helpers
(apply_lyot_focal_plane_mask, apply_vortex_phase_mask,
apply_lyot_stop, apply_apodized_pupil) -- transmission filters
that compose with the existing MFT propagator family
(fresnel_propagate_mft, fraunhofer_propagate_mft,
angular_spectrum_propagate_mft) into a textbook focal-plane-zoom
coronagraph pipeline. The end-to-end validation
(validation/elements/test_elements.py) confirms >100x on-axis
starlight suppression for a charge-2 vortex with an 0.85*D Lyot
stop.
polychromatic_psf complements the existing polychromatic_strehl
by returning the full integrated PSF intensity map on a common
image plane across wavelengths -- the realistic "what does a
broadband detector record" picture, including chromatic-defocus
broadening. Returns power-normalised, peak-normalised, or raw
weighted-sum scaling, plus per-wavelength diagnostics
(centroid wavelength, per-lambda peaks, D4-sigma).
The existing generate_turbulence_screen (Kolmogorov / von Kármán
PSD with outer- and inner-scale support) is now better surfaced in
the documentation; the 5/3-power structure-function and outer-scale
variance behaviours are exercised by new validation tests. The
three MFT propagators get full Function-Reference wiki coverage.
No breaking changes; all additions are pure-additive.
What's new in 3.8.2
Adds two optional convention controls to
lumenairy.eval_image_plane_wfe:
image_plane='paraxial' | 'best_rms' | 'best_pv'— select the longitudinal image-plane focus. Default'paraxial'matches Zemax / rayoptics / Optiland defaults;'best_rms'does a closed-form shift to minimise WFE RMS (what a lab tech finds by maximising spot intensity);'best_pv'numerical-minimises peak-to-valley.sphere_tangent='vertex' | 'exit_pupil'— select where on the chief ray the reference sphere is tangent. Default'vertex'is the 3.8.0 / 3.8.1 convention;'exit_pupil'matches the convention rayoptics / Optiland / Zemax use internally.
Plus four new diagnostic fields on ImagePlaneWFE
(image_plane, sphere_tangent, r_sphere_m,
img_d_m_paraxial). Validation suite extended from 11 to 18
checks; all pass. No changes to apply_real_lens_traced,
trace(), or any other pre-3.8.2 API; defaults reproduce 3.8.0
/ 3.8.1 output bit-for-bit.
See Release Notes / 3.8.2 for full examples and the per-library cheat sheet.
3.8.1 was a one-line metadata fix (__version__ was stale at
"3.7.10" in the 3.8.0 source even though pyproject.toml
bumped to 3.8.0). No API change.
What's new in 3.8.0
Image-plane wavefront-analysis release. Three peer-library features that were missing from the public API up to now:
lumenairy.eval_image_plane_wfe(prescription, wavelength, field=(0,0), n_pupil=31)— direct image-plane reference-sphere wavefront-error evaluation (the textbook Zemax / Code V WFE convention). Returns anImagePlaneWFEresult with per-ray pupil coordinates, OPD in waves, alive mask, andpv_waves/rms_waves/strehlproperties. Complementsapply_real_lens_traced(which returns the lens-exit chief-relative OPL appropriate for downstream ASM / Fresnel propagation); the two are connected by an exact ray-sphere intersection internally. Validated against rayoptics + Optiland + OPDPy across 13 lens forms; seeOPDPy_Lumenairy_Crosscheck/CROSS_CHECK_METHODOLOGY.mdin the companion repo.lumenairy.remove_low_order_aberrations(opd_w, px, py, include_r4=True)— best-fit subtraction of piston + tilt + defocus + 4th-order spherical from a scattered OPD field. Residual is the genuinely-higher-order aberration content (6th-order SA, coma, astigmatism) where ray-trace implementations actually diverge; this is the realistic apples-to-apples cross-library agreement metric.lumenairy.raytrace.first_order_data(prescription, wavelength)— single-call paraxial summary. ReturnsFirstOrderDatawith EFL, BFL, FFL, EP/XP positions and radii, principal-plane offsets, working f-number, the full ABCD matrix, and stop index.
GUI: the Analysis tab's "System Summary" dock now appends an Image-plane WFE (on-axis) block reporting PV / RMS / Marechal Strehl and the after-best-fit residual, live from the active prescription.
See CHANGELOG.md and GUI_CHANGELOG.md for full release
notes, and the 3.8.0 wiki "What's New" page for usage
examples.
What's new in 3.7.10
Workspace reorganisation pass. Each tab's default dock set is restructured so the tab's headline tool gets the dominant space. Driven by a parallel-agent survey of how Zemax OpticStudio, Code V, OSLO, and Optiland organise their equivalent workspaces.
GUI
- Wave Optics tab: 2D layout dropped;
waveopticsdock is now the dominant left ~60%;zernikeright; field-consumer tools (phase_retrieval,interferometry,jones_pupil,coherence) tabbed below. Addresses the explicit user complaint that wave-optics wasn't front-and-center. - Analysis tab: 2×2 plot grid (
spot,rayfan,psfmtf,distortion) with field-aware and aperture-coverage siblings tabbed in. - Optimize tab: optimizer + sliders left spine; live spot / rayfan / PSF-MTF top-right; summary bottom-right for live before/after metrics.
- Tolerancing tab: MC histograms dominant; sensitivity worst-offenders right; spot as worst-trial inspector.
- Materials tab: catalog list + Abbe diagram as headline pair.
- Design tab: prescription editor (central) + summary (System Explorer analogue) + layout / layout3d tabbed thumbnail + library / snapshots.
The 2D / 3D layout pair is only on the Design tab by default (layout3d tabbed with layout, so both share one dock slot). Every specialty dock remains one click away via the View menu.
Existing users get a one-time "Workspaces reorganised (3.7.10)" prompt offering to migrate to the new defaults; custom workspaces are preserved either way.
See GUI_CHANGELOG.md for the full per-tab table.
What's new in 3.7.9
Quality-of-life pass: debounced auto-retrace, wave-optics fold filters + saved-file loader, multi-row prescription editing, template-driven inserts, recent-files timestamps, F1 cheatsheet.
Library
SystemModel.trace_startedsignal — fires at the top of everyrun_trace. Lets GUIs raise a busy cursor / status label immediately rather than waiting fortrace_readyat the end.- Saved wave-optics HDF5 / Zarr outputs now embed the full prescription JSON + propagator settings + lumenairy version, so a saved run is self-describing.
GUI
- Wave Optics dock: explicit
Unfold steering mirrorsandIgnore lateral CBscheckboxes; embedded-prescription saved- file loader (with one-click "Load prescription into model"); duration forecast updated for the filtered surface count. - Debounced auto-retrace: edits coalesce into one retrace
200 ms after the last change. Honours the existing
auto_retrace_modepref. Status indicator + busy cursor. - Multi-row select in the prescription editor: Shift+click / Ctrl+click rows, right-click → "Delete N Elements" / "Duplicate N Elements".
- Insert → From Template: cemented doublet, Plossl, Petzval, Kepler telescope (afocal, user-chosen mag), 4-f relay. Same builders exposed as example-design buttons in the Welcome dock.
- F1 / Ctrl+?: keyboard shortcut cheatsheet from anywhere.
- Recent files carry timestamps now ("2h ago", "3d ago").
- What's New modal refreshed for 3.7.x and auto-titled from
__version__.
See CHANGELOG.md and GUI_CHANGELOG.md for the full list.
What's new in 3.7.8
Closes the remaining 3.7.6 / 3.7.7 fold-correctness gaps and ships three discoverability / workflow polishes. All four ray-trace analysis docks (Footprint, Distortion, Spot vs Field, Ray Fan) and the Tolerance Monte Carlo now route through the world-frame trace.
Library
- New
ray_fan_data_worldandopd_fan_data_world-- drop-in world-frame analogues ofray_fan_data/opd_fan_data.
GUI
- Ray Fan dock's tangential / sagittal / OPD plots now route
through
*_worldhelpers (3.7.7 only migrated the field- curvature plot). - Tolerance dock Monte Carlo properly perturbs world-frame
geometry: each trial shifts downstream
world_originvalues bydelta_t * surface_i.world_R[:,2]for every positive- thickness gap. Previously, 3.7.7 documented this as "intentionally not migrated" becauseSurface.thicknessperturbations are ignored by the world trace. - 2D layout: source-preview toggle surfaced as a toolbar checkbox (was a buried pref since 3.6.1).
- 2D layout: new
Export…button for SVG (vector) / PNG (2× raster).
Known limitations queued for future releases
- Wave-optics propagation through folded systems still routes
the unfolded equivalent. Infrastructure inventoried; ~500-
1000 LOC core work to wire fold-aware ASM into
apply_real_lens_traced. - Non-sequential trace (beam splitters, ghosts, double-pass) needs an architectural overhaul; the world-frame surface representation gives the right foundation but is not enough on its own.
See CHANGELOG.md and GUI_CHANGELOG.md for the full list.
What's new in 3.7.7
World-frame correctness rollout to the remaining analysis docks
(footprint, distortion, spot-vs-field, rayfan field-curvature),
plus a 3D undocked-shrink rescale fix and a GUI visual-regression
test that would have caught the prior shrink regressions in 30
seconds. Builds on the 3.7.6 trace_world infrastructure.
Library — new public API
SystemModel.build_trace_surfaces_world()— public cached accessor for the world-frame surface list (one Surface per optical surface, each carryingworld_origin+world_R).SystemModel.build_run_trace_world_surfaces(image_distance= None)— same list with an image-plane Surface appended at the Detector's world frame (or paraxial focus along the last surface's local +z). Drop-in replacement for the manualsurfaces[-1].thickness = bfl+Surface(radius=inf, ...)boilerplate every analysis dock used to write.
GUI — analysis docks migrated
- Footprint, Distortion, Spot vs Field, and the Ray-Fan dock's
field-curvature plot now use
build_run_trace_world_surfaces()trace_world(). Per-surface scatter plots and chief-ray distortion traces land at the correct world positions in folded designs.
- Tolerance dock and Wave Optics dock intentionally stay on the
legacy path — see
GUI_CHANGELOG.mdfor the rationale.
GUI — 3D undocked-shrink rescale fix
Layout3DView.eventFilteradjusts the camera'sparallel_scale(orview_anglefor perspective) proportionally to the viewport-height ratio on every resize. Shrinking the floating 3D window now CLIPS the optics at its new edges instead of rescaling — the on-screen pixel size of every world feature stays fixed.
Validation — new GUI smoke suite
validation/gui/test_layout_shrink.pyruns offscreen and catches: layout-dock pinned-at-minimum regressions, splitter- won't-shrink regressions, layout-construction crashes, and tx71 chief-ray world-position regressions. 14 assertions; picked up byrun_all.py. Suite total now 26 files.
See CHANGELOG.md and GUI_CHANGELOG.md for the full list.
What's new in 3.7.6
Sequential world-coordinate ray trace (core) plus a folded-
design correctness pass and a long-running dock-shrink fix in the
GUI. The core trace engine now has a second sequential trace
path (trace_world) that propagates rays in world coordinates
between surfaces; each Surface carries its absolute
world_origin and world_R (local-to-world rotation), and only
drops into local coords for the per-surface intersect / refract /
reflect step. This eliminates the ~1/cos(θ) world-position
error that the legacy local-frame trace's coord-break stack
introduced for any element following a tilted mirror.
Library changes
- New
lumenairy.raytrace.trace_world(rays, surfaces, wavelength, ...). Same signature astrace(), but expects each surface to haveworld_originandworld_Rpopulated. Surfacegains two optional fields (world_origin,world_R, both defaultNone). Backward compatible: existing surfaces / pickles / constructor calls are unaffected, and the legacytrace()path still works.- All 25 validation files pass unchanged.
GUI changes — folded designs
SystemModel.run_tracenow uses the world-coord trace path. Verified ontx4designstudy71.zmx(a 1× telecentric design with a 45° fold mirror): the chief ray's world positions after the fold now land at each subsequent lens centre to floating-point precision (SpatialFilter,Lens 10,Lens 11,Lens 12,Detectorall hit dead-centre).- Layout views (
surface_frames_2d_mm/surface_frames_3d_mm) emit one frame per actual optical surface — no separate cb_pre frame. Matches the world-trace ray history one-to-one. - Direction-coloured ray segments (forward / return / post-fold) in both 2D and 3D layouts (3.7.5 rolled in).
GUI changes — dock-shrink finally fixed
The 2D / 3D layout dock would refuse to shrink when docked, no
matter what minimum-size constraints the contained widget or
QDockWidget reported. The real culprit was
element_editor.setMaximumHeight(350) on the central widget:
when the user dragged the splitter UP to shrink the layout
dock, the freed vertical space wanted to flow into the central
widget — but it was capped at 350 px, so Qt refused to move
the splitter at all. Cap removed; the element table grows
into the freed space naturally.
- 3D layout's button row is now a
QToolBarwith built-in overflow popup — when the dock is narrow, hidden buttons go into a">>"menu instead of forcing the dock open. - Shrinking now CLIPS the 2D scene (and shows scrollbars if
needed) instead of rescaling —
Layout2DView.resizeEventno longer callsfitInView. The toolbar's "Reset View" button still re-fits on demand.
GUI changes — Reset to Default Layout
- New View ▸ Reset to Default Layout menu action. Captures the freshly-built default dock geometry on startup and lets the user un-float / re-tab / re-size every dock back to first-launch state without restarting the application.
See CHANGELOG.md and GUI_CHANGELOG.md for the full list.
What's new in 3.6.1
GUI update only — no core-library API changes. Audit-driven overhaul of the LumenAiry Designer GUI plus three hotfixes against bugs caught after the first 3.6.1 push.
Layout fixes
- Sources are now drawn in the 2D and 3D layouts as per-source- type glyphs (bar / ellipse / disc / annulus / dot / array).
- Selecting a row in the prescription editor now highlights the corresponding element in both layouts.
- Source-type changes refresh the layouts immediately (no more 200 ms debounce wait on a discrete combo change).
- Detector is now clickable from the 2D layout's image-plane line.
- Rays render in correct world-frame z (the previous version squished the entire fan into ~10 mm because it summed only in-glass thicknesses; the source-to-first-surface air gap was missed and the parallel pre-lens beam was invisible).
- 3D layout preserves orbit / zoom / pan across parameter edits. Only the very first rebuild snaps to iso; the toolbar's "Reset View" button still works.
Workspace UX cleanup
DEFAULT_WORKSPACEStrimmed (Analysis 13→5, Wave Optics 9→4) with a one-time "reset to new defaults?" migration prompt for upgraders.- New top-level View > Configure Workspace Docks… action.
- All 3.6.0 specialty docks now exposed in the View menu.
Industry-pattern adoptions (Optiland / Zemax / OSLO)
- Source-preview rays drawn from source plane to first surface (toggleable, off by default).
- OSLO-style "Attach slider to this parameter…" — right- click any numeric cell in the surface sub-table to promote it to an optimization variable and reveal the Sliders dock with the new slider pulsing amber for 3 seconds.
Cleanup
- Removed the legacy
optical-designerconsole script andrun_optical_designer.pylauncher. Uselumenairy-designer(orpython run_lumenairy_designer.py) instead. The QSettings storage key is unchanged so existing user customisations survive.
See GUI_CHANGELOG.md for the full list.
What's new in 3.6.0
GUI update only — no core-library API changes. Closes ~30 audit findings raised after 3.5.9: 5 new specialty docks (Richards-Wolf high-NA focus, Köhler partial coherence, Shack-Hartmann sensing, LG aberration tensor, RCWA grating), Wave Optics dispatch picks up GBD / HFPI / Huygens-Fresnel / Subaperture, Quick-run preset bar, detector-model toggle, Insert > Source presets, F6-F10 shortcuts, sortable shortcuts dialog, in-app What's-New modal, expanded Help and Tools menus, optimizer redesign, run-button standardisation, Welcome-dock + REPL improvements.
See GUI_CHANGELOG.md for the full list.
What's new in 3.5.9
GUI update only — no core-library API changes. Application renamed from "Optical Designer" to LumenAiry Designer with backward-compat aliases for the launcher and console-script entry point. GUI catch-up to 3.3-3.5 core-library work: MFT propagators exposed in the Wave Optics dock, Bandlimit + chief-relative-OPD checkboxes, Custom MHS chain tab, Caustic Diagnostic dock, JAX optimizer toggle, Tools > Scale system menu, structured Tolerance report.
See GUI_CHANGELOG.md for the full list.
What's new in 3.5.8
Propagator-family H-cache standardisation. The same
_h_cache_lookup pattern that already speeds up
angular_spectrum_propagate is now applied to the rest of the
"fast" propagator family (Rayleigh-Sommerfeld, tilted ASM, scalable
ASM, ASM-MFT), plus a Matsushima-style bandlimit kwarg on RS and
an 'rs' short alias in the apply_real_lens dispatcher.
rayleigh_sommerfeld_propagate-- adds an H-cache (FFT'd Green's function keyed on geometry signature; ~2-4x warm-call speedup at N=1024-2048), a newbandlimit=Falsekwarg (Matsushima cutoff on the padded-grid kernel; defaults to off to preserve the "exact Green's function" semantic), and standardisedtarget_cdtypeinference matching the rest of the library.angular_spectrum_propagate_tilted-- adds an H-cache keyed on(Ny, Nx, dy, dx, lambda, z, fx0, fy0, bandlimit, dtype); ~1.5-1.7x warm-call speedup at N >= 1024.scalable_angular_spectrum_propagate-- bundles the three per-call padded kernels (delta_H,H1,H2) under one cache entry; ~2x warm-call speedup across N=512-2048.angular_spectrum_propagate_mft-- caches the input-grid H separately from the per-call Bluestein step. Calls onto multiple output grids from one input plane (the natural focal-plane-probing pattern) get ~2.7x amortised speedup at N=1024 after the first build.apply_real_lens(wave_propagator='rs')-- short alias for'rayleigh_sommerfeld'; the dispatcher also forwards the function'sbandlimitkwarg into the RS path.
The internal _h_cache_store byte budget grows a small
_entry_bytes helper so it correctly accounts for both single-array
entries and tuple bundles (the SAS case). No existing API behaviour
changes; __all__ unchanged. 46/46 propagator tests pass; all 25
validation files green.
What's new in 3.5.7
Inter-library compatibility improvements. Driven by an empirical multi-library cross-validation (LightPipes, prysm, POPPy, diffractio, OPDPy) that surfaced two gaps -- a phase-convention mismatch against ray-trace-rooted aberration tools and a missing arbitrary-output-grid focal-zoom propagator. Both addressed; nothing existing changes.
-
apply_fresnel_curvature(E, dx, wavelength, R, sign=±1)-- new utility for round-tripping between phase conventions. Lumenairy and the Fresnel/ASM-propagator family (LightPipes, prysm, POPPy, diffractio, Zemax POP) keep the absolute physical phase at the output plane. OPDPy and Zemax wavefront operands store the chief-relative OPD -- a different reference, purpose-built for aberration analysis -- which differs by exactlyexp(i*k*r²/(2R))withR = v - ffor a thin-lens imager (Gaussian-beam ABCD prediction). The new utility converts cleanly in either direction. Empirical agreement post-correction: Lumenairy ↔ OPDPy at 0.99996 complex correlation. -
fresnel_propagate_mft-- single-FFT paraxial Fresnel onto a user-specified output grid via Bluestein chirp-Z transform. Pick anydx_out,N_out,centre_out=(x, y)independently ofdx_inandz. Standard tool for focal-plane zoom (sample a tightly-focused region at sub-pixel resolution without padding the input grid by the corresponding factor). Same physics asfresnel_propagate. -
fraunhofer_propagate_mft-- far-field counterpart tofresnel_propagate_mft. Excellent for coronagraph and high-contrast imaging workflows. POPPy'sapply_image_plane_fftmftand prysm'sfocus_fixed_samplingare well-established equivalents in their respective ecosystems and the inspiration for this addition. -
angular_spectrum_propagate_mft-- exact ASM (exp(i*kz*z)withkz = sqrt(k² - kx² - ky²)) followed by a Bluestein chirp-Z inverse FT onto the user-specified output grid. POPPy, prysm, and diffractio offer arbitrary-output-grid focal zoom via their paraxial Fresnel propagators (the natural choice for the imaging applications they're built for); this addition extends that capability to high-NA / strongly-diverging beams where the exact ASM kernel is preferred.
All three propagators share an internal _bluestein_2d helper module
and follow the same backend dispatch pattern as
angular_spectrum_propagate (NumPy / pyFFTW / CuPy / JAX, with
jax.grad validated to ~1e-5 of finite differences). At the natural
FFT-output grid, MFT versions agree with their non-MFT siblings to
~2e-14 relative. Bluestein cost scales as O((N + M) log (N + M))
per axis vs O(N²M²) for direct matrix DFT.
__all__ grows from 391 to 395. No existing API changes. All 40
propagator validation tests pass.
What's new in 3.5.6
CI hotfix + 14 audit-driven performance / accuracy improvements.
trace_jax_with_params-- JAX-array-aware ray tracer that accepts radii, conics, aspheric coefficients, and thicknesses as differentiable JAX arrays. Unblocks design-parameter adjoint optimization that 3.5.5'smake_lg_aberration_merit_jaxonly partially supported.- Newton iter cap reverted 8 → 12 in
apply_real_lens_traced(3.5.5 drop showed 0% speedup). - Adaptive Newton convergence warning when >1% of pixels fail.
- Maslov upsample phase fix (cubic zoom of cos/sin instead of
np.unwrap). - JAX x64 auto-enable in
fit_canonical_polynomials_jax. set_pyfftw_planner('FFTW_MEASURE')API for ~20% faster FFTs after ~1 s planning cost.- Vectorised Vandermonde + 4-D polynomial evaluator (3-5× faster Maslov hot path).
design_optimize(precision='single')for ~2× FFT throughput.non_sequential_stray_lightandmonte_carlo_tolerancing_linearizedhelpers.
Plus a CI-only fix: missing from typing import Tuple in
elements/lenses.py after the 3.5.5 split.
What's new in 3.5.1
Patch release: additive JAX-companion functions across analysis,
system, and propagators. Every change is opt-in (suffix _jax);
no existing function behaviour changes.
solve_envelope_stationary_jax_ift— JAX-grad-friendly Newton solver for the envelope-stationary equation, with backward via the implicit function theorem (jax.custom_vjp). Forward matches the NumPy solver to ~1e-12; backward IFT matches FD to single-precision JAX float32 floor.through_focus_scan_jax— JAX-batched z-scan viaangular_spectrum_propagate(already JAX-traceable).gerchberg_saxton_jax,error_reduction_jax,hybrid_input_output_jax— JAX-jit'd phase-retrieval iterations usingjax.lax.fori_loop.propagate_through_system_jax— element-by-element walk with per-element JAX dispatch forpropagate/lens/aperture/mask; falls back to NumPy at element boundaries for unsupported types.
Reserved stubs documenting the planned interface (raise
NotImplementedError):
fit_canonical_polynomials_jax, apply_real_lens_traced_jax,
apply_real_lens_maslov_jax, monte_carlo_tolerancing_jax. Each
points users at the NumPy version that's already complete.
__all__ is now 373 entries. All 25 validation files green.
What's new in 3.5.0
Major release covering three sessions of feature work and API harmonization.
-
JAX-traceable ray trace (
trace_jax) gains aspheric Newton intersect (jax.lax.fori_loop), aperture clipping, and DOE order kicks. End-to-end differentiable viajax.grad. -
JAX paths for the LG aberration tensor:
aberration_tensor_lg00_jaxandpropagate_modal_asymptotic_lg00_jaxgive differentiable Strehl-amplitude evaluation. -
Multiple Huygens Surface (MHS) framework -- new
lumenairy.propagators.mhsmodule withHuygensSurface,MhsSubdomain,MhsPipeline, four convenience builders, andMhsPipeline.from_prescription(...)one-call ASM -> lens -> ASM chain builder. Storage hooks viapipeline.run(checkpoint=..., store=path). -
Sourceclass bundles(E, dx, wavelength, source_point, name)with chainable.propagate(...)returning another Source; class-method factoriesSource.gaussian,Source.plane_wave,Source.point_source,Source.top_hat,Source.fiber_mode. -
Smarter
propagate(method='auto')-- the dispatcher now inspects surfaces for aspheric coefficients, DOE events, and hard apertures. Newaccuracy='fast' | 'balanced' | 'accurate'hint.'mhs'joins'asm'/'fresnel'/'gbd'/'hf'/'hfpi'/'maslov'/'asymptotic'. -
Unified
PropagationResult-- opt-in viareturn_result=Trueonpropagate(),MhsPipeline.run(...), andpropagate_through_system(...). Carries.field,.dx,.wavelength,.method,.historylist of(label, field, dx)planes, and.metadata. Tuple- unpacks for backward compat;np.asarray()falls through to the field. -
replay_run(filepath, ...)reads back every plane stored by an MHS / system run, returning aPropagationResultwith.historypopulated. -
Optimize layer extensions:
wave_propagator='real_lens' | 'gbd' | 'hf' | 'hfpi' | 'asymptotic'plumbs any modern propagator into the wave leg.WAVE_PROPAGATOR_REGISTRY+register_wave_propagator(name, fn)for user-registered custom propagators.JaxMeritTerm(fn, build_args=lambda x: ...)enables analytic JAX gradients flowing into SciPy viajac='auto'.
-
Unified
aberration_summary(prescription, wavelength, ...)returns Seidel + EFL/BFL + LG aberration tensor in one call;differentiable=Trueroutes through the JAX path. -
__all__reorganized into 10 user-journey tiers -- 358 entries grouped by build/propagate/trace/analyze/optimize/... -- no entries removed. -
examples/directory with five end-to-end runnable scripts demonstrating the core workflows.
import lumenairy as la
# One-liner system propagation:
src = la.Source.gaussian(w0=200e-6, N=256, dx=4e-6, wavelength=1.31e-6)
out = src.propagate(method='asm', z=10e-3)
# Aberration analysis:
presc = la.make_singlet(R1=51.5e-3, R2=float('inf'), d=4e-3,
glass='N-BK7', aperture=6e-3)
print(la.format_aberration_summary(la.aberration_summary(presc, 1.31e-6)))
# JAX-differentiable design optimization:
import jax.numpy as jnp
term = la.JaxMeritTerm(
fn=lambda R: jnp.abs((R - 30e-3) * 1e3),
build_args=lambda x: (x[0],),
real_part=True,
)
res = la.design_optimize(parameterization=..., merit_terms=[term],
jac='auto', ...)
See examples/ for full runnable scripts.
What's new in 3.4.0
Major release.
-
Multi-backend (NumPy / CuPy / JAX). New
lumenairy.backendsubpackage provides namespace dispatch (array_namespace), array predicates, multi-backend FFT (preserves the long-standing pyFFTW > scipy.fft > numpy.fft hierarchy for NumPy + adds cuFFT for CuPy + JAX-XLA for JAX arrays),RandomStatewith per-backend RNG idioms, and scipy-special / linalg dispatch. JAX arrays are differentiable viajax.gradand JIT-compilable viajax.jit. -
New
lumenairy.propagatorssubpackage holding all diffraction propagators -- existing (propagation,asymptotic) and new (hfpi,gbd,hf,dispatch,subaperture). Existing import paths still work via thin re-export shims. -
hfpi-- Huygens-Fresnel Path Integration (Monte Carlo ray-based, handles cascaded diffraction). -
gbd-- Gaussian Beamlet Decomposition (deterministic ray-based, ~100x faster than HFPI on smooth systems). -
hf-- Van-Vleck-corrected deterministic HF. -
dispatch-- top-levelpropagate(E, ..., method='auto')smart selector. -
subaperture-- patch decomposition utilities. -
Bundle naming unification. New
PathBundle(HFPI) andBeamletBundle(GBD) sharepositions/directionsfield names with the existingRayBundle. -
REFERENCES.txtat the top level consolidates every external citation; inline citations have been removed from source. -
Import alias is now
import lumenairy as lathroughout the docs and tests (previouslyas op).
import lumenairy as la
E_out = la.propagate(E_in, z=1e-3, wavelength=1.31e-6, dx=4e-6,
method='auto')
The library is now organised into eight thematic subpackages
(backend, propagators, raytrace, elements, io,
analysis, sources, optimize). All previous top-level
import paths (from lumenairy.propagation import X,
from lumenairy.lenses import Y, etc.) continue to work via
thin re-export shims; no user-visible breakage.
What's new in 3.3.3
-
recommend_grid_for_prescription— design-time companion tocheck_grid_vs_aperturesthat returns a recommended(N, dx)given a prescription, wavelength, source waist, and (optionally) DOE order range / period / DOE-to-destination distance. The recommendation roundsNto a power of two by default and round-trips cleanly withcheck_grid_vs_apertures(no warnings fired at the recommended grid). -
scale_prescription(prescription, factor)— single-factor geometric self-similarity utility. Scales radii, thicknesses, aperture / semi-diameters, object distance, aspheric coefficients (A_n / factor**(n-1)), and coord-break decenters; preserves conics, glasses, tilts, and wavelength. F-number, NA, and magnification are invariant. Useful for swapping between mm and m scales without re-deriving every surface. -
Endpoint-anchored Chebyshev nodes in
fit_canonical_polynomials— opt-inendpoint_anchored=Truekwarg rescales the Chebyshev-Gauss roots so the outermost node sits exactly on the [-1, 1] boundary, lowering max error when the fit support is the full source / pupil box. Defaults toFalseto preserve existing numerics. -
Documented
integration_method='local_quadrature'inapply_real_lens_maslov— per-pixel v2-disk integration via Newton + Hessian (more rigorous than a global linear fit at the cost of one Newton solve per output pixel). Already existed in the library; the docstring now fully covers all three integration modes ('quadrature','local_quadrature','stationary_phase').
What's new in 3.3.2
- Embedded grating diffraction in
trace()andfit_canonical_polynomials— newsurface_diffractionkwarg pins a DOE / grating order at a specific surface inside a sequential prescription. Required to do LG-aberration-tensor or asymptotic-propagator analysis at non-zero diffraction orders (Dammann splitter corner orders, etc.). Applies the angular kick AND adds the DOE's linear-phase OPL contributionm * lambda * (x, y) / periodat the surface, so per-emitter (0, 0) pistons are correct even at corner orders.
What's new in 3.3.1
-
Pre-flight grid-vs-aperture check —
apply_real_lens,apply_real_lens_traced, andapply_real_lens_maslovnow inspect every surface'ssemi_diameteragainst the simulation grid's half-extent (N*dx/2) and emit aUserWarningif any surface exceeds the grid. This catches the silent-energy-loss case where the lens itself would have transmitted past the grid edge but the simulation truncates atN*dx/2, which otherwise manifests downstream as a uniform inward centroid bias and missing power. A new public helpercheck_grid_vs_apertures(prescription, N, dx, safety_factor=1.0)returns the offending surfaces explicitly for use in pre-flight scripts. Warning fires once per call site (Python's default warning filter dedups by source line). -
Quadoa Optikos
.qosimport/export (best-effort) —export_quadoa_qos/load_quadoa_qosadd round-trip support for a Quadoa-Optikos-style JSON system file. The schema (versionQUADOA_SCHEMA_VERSION = '1.0') captures every field a lumenairy prescription holds — radii (incl. biconic Y), conics, asphere coefficients (per axis), glasses, thicknesses, semi-diameters, aperture, stop index, wavelength, units. Round-trips lossless inside lumenairy; external Quadoa readability is unverified pending a reference.qos. The library now has full prescription I/O for Zemax (.zmx,.txt), Code V (.seq), and Quadoa Optikos (.qos).
What's new in 3.3.0
A new module lumenairy.asymptotic implementing the closed-form
phase-space (Maslov) diffraction propagator and Laguerre-Gaussian
aberration tensor. This is the
"missing middle tier" between expensive wave-leg merits
(StrehlMerit, RMSWavefrontMerit) and cheap ray-leg-only merits
(SphericalSeidelMerit, FocalLengthMerit): wave-leg-faithful
quantities (the named aberrations the diffraction integral sees) at
ray-leg-only evaluation cost.
-
fit_canonical_polynomials— Trace a 4-D Chebyshev-node grid through any prescription, fit Phi(s2, v2) and s1(s2, v2) as 4-variable Chebyshev tensor-product polynomials, expose analytic gradient evaluation. Sub-microwave residual on refractive systems. -
aberration_tensor— Evaluate the LG aberration tensor T_{k;n,m} at a chief-ray image point. Indices (p, ell) correspond directly to classical Seidel/Zernike aberrations: (1, 0) is defocus, (2, 0) is primary spherical, (1, +-1) is coma, (0, +-2) is astigmatism, etc. Closed-form Wick-contracted Gaussian moment, no quadrature. -
propagate_modal_asymptotic— Closed-form leading-order asymptotic propagator on a 2-D output grid. Reduces to Collins' ABCD in the source-dominated limit and Fourier-of-pupil in the pupil-dominated limit; interpolates smoothly with no caustic pathology. ~103-104 times faster per pixel than direct quadrature. -
LGAberrationMerit— NewMeritTermsubclass that drops intodesign_optimize. Specifies named aberration channels to suppress (defocus, spherical, coma, ...); evaluates the closed- form tensor in milliseconds per merit call. No wave leg required.merit = la.LGAberrationMerit( targets={(2, 0): 1.0, # primary spherical (1, 1): 1.0, # coma (sin) (1, -1): 1.0, # coma (cos) (0, 2): 1.0, (0, -2): 1.0}, # astigmatism field_points=[(0.0, 0.0), (5e-3, 0.0), (0.0, 5e-3)], )
-
LG / HG basis utilities —
lg_polynomial,hg_polynomial,evaluate_lg_mode,evaluate_hg_mode,decompose_lg,decompose_hg,lg_seidel_label. -
Wick moment utilities —
gaussian_moment_2d,gaussian_moment_table_2d. Closed-form 2-D Gaussian moments for complex-symmetric covariances.
Validation: 32 new physics-faithful tests in
validation/test_asymptotic.py(LG orthonormality to 1e-14, Wick / Isserlis identities, fit round-trip, modal propagator PSF peak location, end-to-end LGAberrationMerit). Full existing test suite re-runs green: no regressions. No breaking API changes.
What's new in 3.2.15
-
apply_doe_phase_traced— new ray-trace primitive for splitting aRayBundleat a thin grating / DOE plane into one or more diffraction orders. Applies the grating-equation direction-cosine shiftL_new = L + m_x * lambda / period_x(and same on y) per ray, recomputesNfrom the unit-norm constraint, and flags evanescent orders (L'^2 + M'^2 > 1)alive=Falsewith a newRAY_EVANESCENT = 5error code. Two calling conventions: scalar orders return a same-shape bundle; 1-D order arrays return a bundle replicatedn_orders * n_raysin order-major layout, ready for a singletrace()call through the post-grating optics. Use case: ray-trace through a Dammann splitter or any thin grating in a sequential prescription.Validation: 6 new tests in
test_raytrace.pycover zero-order no-op, the grating equation, unit-norm preservation, evanescent flagging, order-major layout, and free-space round-trip; 32/32 raytrace + 17/17 optimize tests pass.
What's new in 3.2.14
A focused performance pass on the highest-traffic paths. All behaviour preserved (full validation suite still 16/16 PASS, 298 assertions); changes are transparent caches or opt-in toggles.
-
ASM transfer-function
Hcache keyed by(N_y, N_x, dy, dx, λ, z, bandlimit, dtype). Repeat propagations at the same geometry skip the chunked kernel build entirely. Measured 1.5× speedup at N=2048 on cache-hit (the H build is ~30-50% of total ASM call time on 2k+ grids). Tunables:set_asm_cache_size(...),clear_asm_caches(). -
Frequency-grid + band-limit caches complement the H cache so even on H-miss the kernel reconstruction skips spatial-frequency vector recomputation.
-
Multi-slot pyFFTW plan cache (was single-slot per direction → 8 entries by default). No more thrashing when calls oscillate between two shapes (JonesField stacking, Maslov inner work, batched vs scalar grids). Tunable:
set_fft_plan_cache_size(n). -
Single-precision (complex64) toggles:
set_default_complex_dtype(np.complex64)flips the library default for fields allocated when callers pass real-valued inputs. All propagators preserve the caller's complex dtype; the kernel-phase mod-2π folding keeps single-precision ASM accurate at the float32 noise floor. 2.18× speedup on N=2048 ASM, ~2× memory headroom. -
angular_spectrum_propagate_batch(E_3d, ...)runs a stack of fields(B, Ny, Nx)through one fused FFT pair across the trailing two axes.JonesField.propagatestacks[Ex, Ey]and routes through it for grids ≥ 512 (below that, dispatch overhead exceeds the benefit and the H cache already serves the second component for free). -
Numba-fused aspheric-sum kernel in
surface_sag_general. The legacy aspheric loop allocated one fresh N×N array per coefficient term; the new path walksh_sqonce and accumulates all terms in a single threaded pass. 4.75× speedup at N=2048 with 5 aspheric coefficients. Pure spheres unaffected; CuPy stays on the legacy path. -
Memory-leaner refraction step in
apply_real_lens(Fresnel / slant_correction branches): gradient and intermediate arrays freed eagerly. Drops peak transient memory at N=8192 from ~5 GB to ~1.5 GB. Math unchanged.
What's new in 3.2.13
- Validation suite expanded by ~70 new physics + interop test
cases. 16 files, 298 Harness assertions across topic suites,
all PASS. Highlights: ASM linearity / reciprocity / D4σ growth;
thin-vs-real lens interop; doublet ABCD-EFL ↔ paraxial focus
agreement; Strehl-Maréchal; Parseval; Zernike linearity; Malus
- crossed polarizers + circular S₃; Köhler imaging smoke;
Cauchy-Schwarz on mutual coherence; Keplerian |M|=f₁/f₂;
end-to-end singlet wave-vs-trace consistency;
propagate_through_systemmatches manualapply_real_lens+ASM.
- crossed polarizers + circular S₃; Köhler imaging smoke;
Cauchy-Schwarz on mutual coherence; Keplerian |M|=f₁/f₂;
end-to-end singlet wave-vs-trace consistency;
What's new in 3.2.10 - 3.2.12
These were UI-focused releases that did not change core
library behaviour. See Optical_Propagation_Library_UI/CHANGELOG.md
for the GUI-specific feature additions (workspace tabs, Welcome
dock, embedded Python REPL, persistent status-bar metrics,
drag-and-drop file open, keyboard shortcuts for workspaces, compact
mode, etc.). Core API unchanged.
What's new in 3.2.3
wave_propagator='fresnel'andwave_propagator='rayleigh_sommerfeld'added toapply_real_lens(and threaded throughapply_real_lens_traced). Together with the pre-existing'asm'(default) and'sas', the through-glass propagator can now be switched to any of the four physically-sensible choices. ASM remains the right tool for the mm-scale glass distances typical of lenses; the others are exposed for research and pipelines that want one propagator used consistently throughout. Fresnel and SAS resample back to the inputdxautomatically. RS preserves pitch and agrees with ASM to ~1e-13 at typical through-glass distances. Unknownwave_propagatorvalues now raiseValueError.
What's new in 3.2.2
lens_maslov.pyretired;apply_real_lens_maslovnow lives insidelenses.pyalongsideapply_real_lensandapply_real_lens_traced. Was conceptually a third real-lens wave-optics pipeline all along, not a separate subsystem. Public API unchanged:op.apply_real_lens_maslovandfrom lumenairy.lenses import apply_real_lens_maslovboth still work. Only the legacyfrom lumenairy.lens_maslov import ...path broke; nothing in the library or validation suite used it.
What's new in 3.2.1
-
SAS integration hooks — the Scalable Angular Spectrum propagator added in 3.2.0 is now a first-class peer of ASM/Fresnel in three more places:
propagate_through_system({'type': 'propagate', ..., 'method': 'sas'})— SAS alongside'asm'and'fresnel'as a per-element method. Pipeline auto-resamples back todxso downstream elements keep their coordinates.apply_real_lens(..., wave_propagator='sas')(+ forwarded throughapply_real_lens_traced) — swap the through-glass propagator.JonesField.sas_propagate(z, wavelength, pad=2, skip_final_phase=False)— polarization-aware SAS wrapper that applies the scalar SAS kernel toExandEyand updatesself.dx/self.dyto the new output pitch.
What's new in 3.2.0
-
Scalable Angular Spectrum (SAS) propagator (
scalable_angular_spectrum_propagate). Three-FFT kernel from Heintzmann-Loetgering-Wechsler 2023: ASM-minus-Fresnel precompensation phase + band-limit filter + Fresnel chirp + FFT + optional final quadratic phase. Output pitch islambda*z/(pad*N*dx)— a zoom-out that avoids the impractical-N trap of plain ASM at long z. The paper's closed-formz_limitcheck warns when exceeded. Includes a Fresnel-style1/(i·λ·z)·dx²amplitude prefactor for power conservation (the reference PyTorch notebook is amplitude-agnostic). Validated againstfresnel_propagate/fraunhofer_propagateat moderate / far-field z in their respective limits. -
CODE V
.seqimport/export (export_codev_seq+load_codev_seq). Canonical CODE V sequence syntax (LEN NEW/DIM M|MM|IN/WL/S<i>/RDY/THI/GLA/CON/STO/APE). Bit-exact round-trip for radii, thicknesses, conic, glass, stop index, and aperture. -
BSDF surface scatter model (new module
bsdf.py) withLambertianBSDF,GaussianBSDF,HarveyShackBSDF. Common interface:evaluate(inc, scat),sample(inc, n, rng),total_integrated_scatter(). Attached toSurfacevia a newbsdffield. Helpersample_scatter_rays(surface, incident, n_per_ray)spawns aRayBundleof scattered rays for Monte Carlo stray-light propagation through the rest of a system. -
Jones pupil spatial-map visualization (
plot_jones_pupil+compute_jones_pupil).compute_jones_pupil(apply_fn, N, dx, wavelength)probes a polarization-capable system with orthogonal x/y plane-wave inputs and returns the full(Ny, Nx, 2, 2)Jones matrix.plot_jones_pupil(J, ...)produces the canonical 2x4 grid (amplitude + phase for each of Jxx/Jxy/Jyx/Jyy) with phase masked below an amplitude threshold.
What's new in 3.1.11
- Stop-aware
seidel_coefficients— newstop_indexandfield_anglekwargs. When the declared stop is not at surface 0, the chief ray's initial conditions are now derived from the pre-stop ABCD so thaty_chief = 0at the stop by construction. Default usesfind_stop(which falls back to surface 0 when no surface is flaggedis_stop=True). refocus(result, delta_z, wavelength=None)— closed-form image-space transfer of a traced bundle.through_focus_rmsuses it internally for a 5-20x speedup on focus sweeps.find_stop,compute_pupils,find_lenses,lens_abcd,LensInfo,PupilInfo— new stop / pupil / per-lens paraxial helpers.lens_abcdaccepts a prescription dict, surface-list slice, singleSurface, or aLensInfo(with the originalsurfacespassed as a kwarg, for re-analysis at a different wavelength).- GPU path for
apply_real_lens(opt-inuse_gpu=True).apply_real_lens_tracedforwardsamp_use_gputo its two internalapply_real_lenscalls.
What's new in 3.1.10
apply_real_lens(use_gpu=False)— opt-in CuPy backend for the full phase-screen + ASM-through-glass pipeline. Default isFalse(unchanged CPU behaviour). When enabled, per-surface sag arrays, phase screens, and ASM propagation all run on GPU.apply_real_lens_traced(amp_use_gpu=False)— new kwarg pipesuse_gputhrough to the internalapply_real_lenscalls that build the amp + amp(pw) arrays. The rest of the traced pipeline (ray trace, Newton, assembly) stays CPU; results are pulled back automatically at the amp-block exit. Combines cleanly with the existinguse_gpu=Truekwarg for the Newton inversion.surface_sag_general/surface_sag_biconicnow array-API polymorphic (accept NumPy or CuPy inputs transparently).
What's new in 3.1.9
lens_abcd(lens, wavelength)+find_lenses(surfaces, wavelength)— paraxial characterisation of individual lens elements. Accepts prescription dicts, surface-list slices, or single surfaces. Auto-detects cemented-doublet grouping. Returns EFL, BFL, FFL, principal planes, and the underlying ABCD for composition.compute_pupils(surfaces, wavelength)— paraxial entrance / exit pupil positions and radii, from the stop surface's ABCD sub-system images. Foundation for future chief-ray aiming and rigorous reference-sphere OPD.RayBundle.error_codeper-ray diagnostic field recording why each dead ray was killed (RAY_TIR,RAY_APERTURE, etc.).trace_summarynow prints the breakdown.- Glass indices pre-resolved once per
trace()call; tiny per-call speedup, meaningful at high repeated-trace counts (aim iteration, focus sweeps, optimisation loops).
What's new in 3.1.8
trace(output_filter='last')eliminates per-surfaceRayBundle.copy()allocations when only the final bundle is consumed. Saves ~1.4 GB perapply_real_lens_tracedcall on a 6-surface doublet at N=32768. Wired intoapply_real_lens_tracedautomatically.refocus(result, delta_z)— closed-form image-space transfer of a traced bundle.through_focus_rmsrewritten to use it, giving ~5-20x speedup on focus sweeps.is_stopfield onSurface+find_stop(surfaces)helper. Aperture-stop surface can be explicitly flagged (Zemax loaders populate it from the STOP keyword); helpers for pupil calculation and chief-ray aiming are the natural next steps.- Seidel
S4(Petzval) double-assignment fixed; dead-code in_paraxial_traceremoved. Numerical output unchanged.
What's new in 3.1.7
-
apply_real_lens_maslov— a third thick-lens propagator complementingapply_real_lensandapply_real_lens_traced. Fits a 4-variable Chebyshev tensor-product polynomial to the ray-traced canonical map (s1(s2, v2),OPD(s2, v2)) and evaluates the Maslov phase-space diffraction integral by stationary-phase (recommended, closed-form per-pixel), uniform Tukey quadrature (extended-source regime), or Hessian-oriented local quadrature (asymptotic corrections beyond leading stationary phase). Caustic-safe by construction, no critical-sampling constraint, analytically differentiable w.r.t. the polynomial coefficients. SeeCHANGELOG.mdfor validation numbers and regime guidance. -
apply_real_lens_tracedspeedup kwargs (all opt-in, default physics unchanged):-
fast_analytic_phase=True— skip the full ASM-through-glass reference-phase pass in favour of a per-pixel sum of sag phase screens. ~25 % wall-time savings whenparallel_amp=False. Introduces <10 nm OPL phase error on typical refractive prescriptions. -
newton_fit='polynomial'(new default) or'spline'— the polynomial path uses a 2-D Chebyshev tensor-product fit instead ofscipy.interpolate.RectBivariateSplinefor the entrance->exit map. Implements combined value+gradient evaluation and an optional Numba@njit(parallel=True)fastpath: ~12x faster than the spline path on the Newton hot loop (4M-sample isolated benchmark). For smooth refractive systems (all Seidel and higher-order aberrations are polynomials), same or better accuracy with closed-form analytic derivatives; flip back to'spline'for high-order freeforms or sharp non-polynomial surface features. -
use_gpu=True— dispatches the polynomial evaluator and Newton inversion to GPU via CuPy (amp/amp(pw)/ray-trace/final assembly stay CPU-only). Requiresnewton_fit='polynomial'and cupy installed. Output is bit-equivalent to CPU (0 % RMS error). Modest absolute speedup at typical workloads (~1.4x vs numba CPU on 1-4M Newton samples); best for iterated design-optimisation workflows or very large grids.
-
-
Optional
numbadependency for the polynomial-Newton CPU fastpath.requirements.txtnow lists it under "optional"; the library falls back to a pure-NumPy path when numba is missing. -
apply_real_lens_traceddefaultray_subsamplebumped 1 → 8. At typical production grid sizes (N=2048 and above) this gives hundreds to thousands of spline / polynomial samples across every lens aperture — far above the internal safety floor. Small-grid users who would drop below 32 samples across aperture get a clear error message from the existingon_undersample='error'guardrail.
What's new in 3.1.6
- Zarr storage reliability fix for Windows + Python 3.14.
append_planeandwrite_sim_metadatano longer raiseFileExistsErrorwhen reopening an existing zarr store. SeeCHANGELOG.mdfor the root-cause breakdown; no API change for callers.
What's new in 3.1.5
- Zemax loaders preserve object-space distance.
load_zmx_prescriptionandload_zemax_prescription_txtnow return a newobject_distancekey on the prescription dict, computed as the sum ofDISZvalues from the STOP (or SURF 0 if no STOP) up to the first active refractive surface. This recovers design-intended obj-space geometry (coordinate breaks, field-reference planes, MLA mount surfaces, etc.) that earlier loader versions dropped. Wave-optics driver scripts should propagate their source field by this distance before invoking the first lens operator; failing to do so collapses the obj-space geometry and produces a defocus-like blur at the image plane proportional to the dropped distance (observed on the Design 51 .zmx: 96.67 mm of dropped air gap → ~235 µm defocus blur at the metasurface plane when the distance was not re-injected).
What's new in 3.1.4
apply_real_lens_traced(..., tilt_aware_rays=...)default changed fromTruetoFalse. The "Tier 1 input-aware ray launch" added in 3.1.2 mixed reference frames between the traced OPL and the plane-wave-reference analytic lens phase used in thepreserve_input_phase=Truesubtraction. On single-mode plane-wave-like inputs the mismatch was small; on multi-mode inputs (post-DOE fields, compound superpositions) it produced materially wrong output fields. The plane-wave launch (tilt_aware_rays=False) is reference-consistent and correct for any input the wave model can represent. SeeCHANGELOG.mdfor the full reasoning.- Paraxial-magnification Newton initial guess: measured from the central finite-difference slope of the already-computed forward map (zero extra compute) instead of the hard-coded 1.10 multiplier. For compound-system callers this saves several Newton iterations per pixel; for singlets the improvement is marginal but the guess is always at least as good as before.
inversion_method='backward_trace'opt-in onapply_real_lens_traced(experimental). Replaces the forward-trace + Newton-spline-inversion with a direct backward ray trace from a coarse subsample of the exit grid through a reversed prescription. Validated to agree with the Newton path to sub-pm on single-ray tests and ~30 nm OPD RMS end-to-end at N=1024 with a ~3x speedup. Default stays'newton'while the backward path is validated on a wider set of prescriptions.
What's new in 3.1.3
- Multi-mode-safe
_sample_local_tilts: amplitude-weighted Gaussian smoothing of the tilt field (newsmooth_sigma_pxkwarg, default 4) before clipping, so post-DOE / interferometric inputs toapply_real_lens_traceddegenerate gracefully to a collimated launch instead of injecting aliased per-pixel tilts that collapse the output field at large N. - Complex-dtype flexibility:
apply_real_lens,apply_real_lens_traced,apply_mirror, andangular_spectrum_propagatepreserve the caller's complex dtype (complex128 or complex64) end-to-end. Kernel-phase and per-surface phase screens always compute in float64 + modulo-2-pi reduction before casting, so complex64 mode avoids the ~0.02-rad-per-Fourier-pixel precision floor that would otherwise swamp large-z ASM propagations. New top-level exportDEFAULT_COMPLEX_DTYPE. - FFT backend refresh: pyFFTW now uses a single-slot per-direction
plan cache with in-place aligned buffers (no more 30 s TTL alloc
churn) and a per-plan
threading.Lockso parallel callers share the cache safely. Cleanreset_fft_backend()support. - Parallel amp + amp(pw) (new
parallel_amp=Truedefault) runs the two internalapply_real_lenscalls insideapply_real_lens_tracedon aThreadPoolExecutor. FFT-serialised, non-FFT work overlapped. Auto-disables when available RAM is tight (parallel_amp_min_free_gb). - Amplitude-masked Newton (
newton_amp_mask_rel=1e-4): skip coarse-grid Newton pixels where the analytic amplitude is below threshold, since they'd be multiplied by ~zero in the final assembly anyway. Big speedup on post-DOE / sparse fields; mask self-disables on dense fields. - Numexpr-fused phase screen in
apply_real_lens(optional dependency[perf]):ne.evaluate('E * exp(-1j*k0*opd)', out=E)eliminates ~50 GB of complex128 intermediates per surface at N=32768 and threads the operation. Numpy fallback preserved. - Decenter-aliased entrance grids in
apply_real_lens:Xs, Ys, h_sqalias the axis-centred grids when decenter is zero (the common case), saving three float64 NxN allocations per surface. - New top-level export
NUMEXPR_AVAILABLEfor runners that want to gate tunables on the fast path being importable.
What's new in 3.1
- Progress hooks (
progress.py) — optionalprogress=callbackkwarg onapply_real_lens,apply_real_lens_traced, andpropagate_through_system. Drive a progress bar from any script or GUI; callback signature is(stage, fraction, message).ProgressScalerlets long pipelines nest sub-tasks inside a parent budget. 'real_lens_traced'element type forpropagate_through_systemso the hybrid wave/ray lens model is reachable through the unified element-list API.- Codegen promoted to public API —
generate_simulation_script,generate_script_from_zmx, andgenerate_script_from_txtare now exported from the top-levellumenairynamespace. remove_wavefront_modesacceptsweights=for intensity-weighted piston / tilt / defocus fits; crucial on vignetted or annular pupils.- Freeform sags ray-traceable —
Surface.freeformplumbed through_surface_sag_xy,_surface_sag_derivatives_xy, andsurfaces_from_prescriptionso XY-polynomial / Zernike / Chebyshev freeforms work in both wave and ray paths. make_singlet/make_doubletalways emit biconic keys (set toNone) for diff-friendly, round-trippable prescriptions.optical_table.py+.htmlremoved — the bundled HTML simulator was unreferenced and its launcher functions were never exported.
What's new in 3.0
apply_real_lens_traced— high-accuracy hybrid wave/ray lens model. Sub-nanometer OPD agreement with the geometric ray trace on cemented doublets and other multi-surface curved-interface systems. UsesRectBivariateSplineover the entrance grid + vectorised Newton inversion of the entrance→exit mapping; orders of magnitude better than the analytic thin-element model where it matters. Amplitude fromapply_real_lens(full ASM-through-glass), phase from ray-traced OPL.- Exit-vertex OPL correction —
apply_real_lens_tracednow transfers rays from the last surface's sag to the flat exit vertex plane before computing OPD, using the signed parametric distance (not absolute value) so both concave and convex rear surfaces are handled correctly. Previously, off-axis rays ended atz = sag(h) ≠ 0while on-axis rays ended atz = 0, injecting systematic defocus (43 % on doublets) or catastrophic sign errors (200,000× on negative meniscus lenses). Doublet focus error: 10 mm → 0.000 mm. Negative meniscus residual: 33,742 nm → 0.17 nm. - Critical raytrace OPL bookkeeping fix —
_intersect_surfacenow accounts for the small "vertex-plane → actual-sag-intersection" leg in the right medium. Singlet wave-vs-geom residuals dropped 17×–130×; every consumer ofraytrace.tracebenefits automatically. - Biconic / cylindrical / toroidal surfaces —
surface_sag_biconic,make_cylindrical,make_biconic, plusradius_y/conic_y/aspheric_coeffs_ykeys on any prescription surface. All downstream consumers (ray tracer, OPD analysis, bothapply_real_lensvariants, Seidel, ABCD) handle anamorphic surfaces transparently. - Zernike decomposition —
zernike_decompose(opd_map, dx, aperture, n_modes)fits OSA-normalised Zernikes via Householder QR with column pivoting (numerically stable for high-order, partial-pupil cases). Round-trips to ~1e-12 precision. Pluszernike_reconstruct,zernike_polynomial, OSA index helpers. - Hybrid wave/ray design optimizer (
lumenairy.optimize) — refine a lens prescription against geometric and/or wave-based merit terms (focal length, Seidel S₁, Strehl ratio, RMS wavefront, spot size, chromatic focal shift). Wrapsscipy.optimize(L-BFGS-B by default;lmroutes through Householder-QR-based Levenberg- Marquardt). Pure-geometric optimization runs sub-second; wave-based metrics scale with grid size. - OPD-extraction Nyquist tooling —
check_opd_sampling()helper + built-inRuntimeWarninginwave_opd_1d/wave_opd_2dwhen sampling near or below the Nyquist edge for the lens's converging wavefront. Optionalf_refparameter divides out a reference sphere before unwrap for users who want coarser grids. - SciPy FFT default —
USE_SCIPY_FFT = True,SCIPY_FFT_WORKERS = -1by default. All wave-propagation calls now multithreaded; 2-4× speedup with zero memory overhead. pyFFTW is opt-in. slant_correctiondefault reverted toFalse— empirical validation showed paraxial(n2−n1)·sagis equal-or-better for almost every test case because the angular-spectrum propagation between surfaces already encodes the obliquity. Slant correction remains available as opt-in.- Validation suite —
validation/real_lens_opd/now compares three methods (paraxial / slant / ray-traced) on 21 reference lenses with matching Zemax LDE +.zmxexports for cross-verification.
Overview
lumenairy provides a physically accurate, modular toolkit for
simulating free-space coherent optics. It handles everything from basic
Gaussian beam propagation to multi-surface real lens modeling with glass
dispersion, metasurface design, and full Jones-vector polarization.
Features are implemented with well-tested physics (verified against textbook formulas and audited for sign conventions), SI units throughout, and optional GPU / multi-threaded FFT acceleration.
Key Features
Propagation
- Angular Spectrum Method (ASM) — exact, band-limited, with rectangular anti-aliasing filter
- Tilted / off-axis ASM — for beams with a non-zero carrier angle
- Single-FFT Fresnel — paraxial, changes output grid spacing
- Fraunhofer (far-field) — simplest far-field computation
- Rayleigh-Sommerfeld — convolution with the free-space Green's function, exact near-field diffraction without band-limiting approximation
- Scalable Angular Spectrum (SAS) — Heintzmann-Loetgering-Wechsler 2023
three-FFT kernel with variable output pitch (
λz/(pad·N·dx)); the right tool when z is long enough that plain ASM needs an impractically large grid
Lenses
-
Thin lens (paraxial, non-paraxial, aplanatic, local-only)
-
Spherical singlet — exact OPD through thick glass
-
Aspheric singlet — conic + even polynomial coefficients
-
Multi-surface real lens (
apply_real_lens) — default fast wave model. Split-step refraction with ASM between surfaces; optional Fresnel transmission, bulk absorption, slant correction, Seidel correction. -
Hybrid wave/ray real lens (
apply_real_lens_traced) — per-pixel ray-traced OPL + wave amplitude envelope; sub-nm OPD on cemented doublets and other multi-surface curved-interface systems. -
Phase-space / Maslov real lens (
apply_real_lens_maslov) — Chebyshev fit of the ray-traced canonical map + Maslov integral. Caustic-safe and differentiable; pair withapply_real_lens_maslov_jaxfor autodiff design optimisation. -
Pluggable through-glass propagator (
wave_propagator=kwarg onapply_real_lens/apply_real_lens_traced):'asm'(default),'sas','fresnel','rayleigh_sommerfeld'.See the Appendix for the underlying physics / accuracy trade-offs, or
help(la.apply_real_lens)for the per-function decision guide. -
Biconic / cylindrical / toroidal elements via
make_biconicandmake_cylindricalplus optionalradius_y/conic_ykeys on any prescription surface -
GRIN rod lens — gradient-index parabolic profile
-
Axicon — conical lens (Bessel-beam generator)
Geometric Ray Tracing
- Sequential 3-D ray tracer — vectorised Snell's law with exact conic/aspheric surface intersection (Newton iteration)
- Surface types — sphere, conic, aspheric (Zemax standard sag), flat, mirror
- ABCD matrix extraction — EFL, BFL, FFL from paraxial marginal ray
- Seidel aberrations — per-surface third-order coefficients (S1–S5)
- Spot diagrams — with RMS/GEO radius, Airy disc overlay
- Ray fan plots — transverse ray aberration vs normalised pupil
- OPD analysis — wavefront error fans
- Through-focus — RMS spot vs defocus with best-focus finder
- Ray generators — fans, grids, concentric rings, single rays
- Diffraction-order shift —
apply_doe_phase_tracedsplits a ray bundle at a thin grating / DOE into one or more orders (single or array), with evanescent flagging viaRAY_EVANESCENT - Prescription compatible — same prescription dicts as
apply_real_lens - System compatible —
raytrace_system()accepts the same element list aspropagate_through_system()for instant wave-optics ↔ ray-optics switching
Mirrors
- Flat or curved (spherical / conic) with optional aperture
Apertures and Masks
- Hard apertures — circular, rectangular, annular
- Soft apertures — Gaussian
- Arbitrary complex masks — for SLMs, metasurfaces, custom DOEs
Wavefront
- Zernike aberrations on a pupil —
apply_zernike_aberrationfor generating wavefronts from Zernike coefficients - Zernike decomposition of OPD maps —
zernike_decompose/zernike_reconstructusing Householder QR with column pivoting (numerically stable, OSA-normalised, RMS coefficients in meters) - Zernike basis primitives —
zernike_polynomial(n, m, rho, theta),zernike_basis_matrix,zernike_index_to_nm,zernike_nm_to_index - OPD extraction from wave fields —
wave_opd_1d,wave_opd_2dwith Nyquist sampling warnings, optional reference-sphere subtraction - Sampling-rule helper —
check_opd_samplingreports the Nyquist margin and recommends grid sizing for clean OPD extraction - Low-order mode removal —
remove_wavefront_modes(piston / tilt / defocus least-squares fit and subtract) - Turbulence phase screens — Kolmogorov and von Karman statistics
Polarization (Jones calculus)
JonesFieldclass — wraps (Ex, Ey) with all standard propagators (ASM, Fresnel, Fraunhofer, tilted ASM, SAS as of 3.2.1) and all non-polarizing element methods (thin/spherical/real lens, aperture, mirror, mask)- Polarization elements — polarizers, waveplates, rotators, arbitrary Jones matrices
- Polarized sources — linear, circular, elliptical
- Analysis — Stokes parameters, degree of polarization, polarization ellipse
- Jones-pupil spatial map (3.2.0) —
compute_jones_pupil(apply_fn, ...)probes a system with orthogonal x/y inputs to extract the full 2×2 exit-pupil Jones matrix, andplot_jones_pupilrenders it as the canonical 2×4 amplitude + phase grid
Beam Sources
- Fundamental Gaussian (TEM00)
- Hermite-Gauss modes (HG_{mn})
- Laguerre-Gauss modes (LG_{pl}) with OAM
- Tilted plane wave — off-axis collimated source at arbitrary field angles
- Point source — diverging spherical wave from (x0, y0, z0)
- Multi-field source generator — batch-produce tilted plane waves for field analysis
Beam Analysis
- Centroid (center of mass)
- D4sigma (ISO 11146) beam diameter
- Power-in-bucket (circular or rectangular)
- Strehl ratio
- PSF, OTF, MTF (including radial MTF profiles)
- Sampling-condition diagnostics
- Chromatic focal shift — per-wavelength EFL/BFL and axial colour PV
- Polychromatic Strehl — weighted Strehl average across wavelengths
High-NA Vector Diffraction (Richards-Wolf)
richards_wolf_focus— compute (Ex, Ey, Ez) vectorial focal field from a scalar or Jones-vector pupil, handling NA > 0.5 where scalar diffraction breaks down (longitudinal Ez component)debye_wolf_psf— intensity PSF |E|^2 including all polarisation components- Supports arbitrary input polarisation (x, y, circular, or custom Jones)
- Multi-z-plane evaluation for 3-D focal-volume tomography
Partial Coherence / Extended-Source Imaging
koehler_image— Koehler condenser illumination model: integrates coherent sub-images over the condenser NA to produce a partially- coherent imageextended_source_image— arbitrary source angle distribution with per-direction weightsmutual_coherence— compute Gamma(r1, r2) from a field ensemble
Detector / Wavefront-Sensor Simulation
apply_detector— pixel-integrate a field onto a detector grid with Poisson shot noise, Gaussian read noise, dark current, full-well saturation, and quantum efficiencyshack_hartmann— simulate a Shack-Hartmann wavefront sensor: sub-aperture extraction, lenslet focusing, centroid detection, and wavefront reconstruction via slope integration
Diffractive Optical Elements
- Periodic phase mask tiling (for DOEs, gratings)
- Microlens array
- Dammann grating IFTA design — generates uniform spot arrays
Glass Catalog
- Refractive index lookup via the refractiveindex.info database
- Includes Schott, Ohara, fused silica, silicon, CaF₂, etc.
- Cached material objects for fast repeated lookups
Lens Prescriptions
- Build singlets and cemented doublets by glass name + geometry
- Thorlabs catalog presets — LA1050-C, LA1509-C, AC254-050-C, AC254-100-C, etc.
- Zemax
.zmxparser — import real lens prescriptions from Zemax files - Zemax
.txtparser — import Zemax prescription-report text files - Zemax
.zmx/.txtexporters — round-trip designs back to Zemax - CODE V
.seqimport / export — round-trips prescriptions through the canonical CODE V sequence syntax (units M/MM/IN, spherical + conic surfaces, stop flag, aperture; unknown directives skipped on import)
Stray-Light / BSDF (3.2.0)
- Three BSDF models with a common
evaluate/sample/total_integrated_scatterinterface:LambertianBSDF(uniform diffuse),GaussianBSDF(small-angle lobe around specular),HarveyShackBSDF(three-parameter ABC model for polished microroughness with optional wavelength scaling) Surface.bsdffield — attach a BSDF to any sequential-system surfacesample_scatter_rays(surface, incident, n_per_ray)— spawn aRayBundleof scattered rays sampled from the surface's BSDF for Monte Carlo stray-light propagation through the remainder of a system
User Library
- Persistent material catalog — save custom glasses (fixed index or from refractiveindex.info) for reuse across sessions and scripts
- Lens library — save and load prescription dicts (singlets, doublets, custom designs, Thorlabs catalog lenses)
- Phase mask library — save mathematical expressions (e.g. spiral phase plates), pre-computed arrays, or glass-block definitions
- All stored as JSON in
~/.lumenairy/library/ - Saved materials auto-register in
GLASS_REGISTRYon import
Phase Retrieval
- Gerchberg-Saxton — phase-only CGH design between source and target
- Error Reduction — coherent diffractive imaging
- Hybrid Input-Output (HIO) — Fienup's feedback algorithm
Prescription → Simulation Script (codegen)
generate_simulation_script— turn a prescription dict into a standalone, runnable Python script that importslumenairy, defines the prescription inline, builds an element list, and propagates a source through it. Useful for archiving a simulation alongside a design, sending a reproducible reference to a collaborator, or dropping the generated code into a Jupyter notebook as a starting point.generate_script_from_zmx/generate_script_from_txt— one-call path from a.zmxfile or Zemax prescription text export straight to a simulation script.- Styles:
'unrolled'(one call per element, easy to edit) or'system'(singlepropagate_through_systemcall, compact). - Toggle
include_analysisandinclude_plottingto control how much post-propagation code is emitted.
I/O
- CSV phase file read/write (with metadata header)
- FITS file read/write (optional, requires
astropy) - HDF5 file read/write (optional, requires
h5py) — single fields, multi-plane propagation datasets, and polarized JonesFields with hierarchical groups, compression, and rich metadata attributes
Plotting (optional, requires matplotlib)
- Field visualizations — intensity (log/linear), phase (with low-intensity masking), combined intensity + phase panels
- Cross-sections — 1D cuts through any axis with optional phase overlay
- Multi-plane grids — automatic layout for propagation simulation results with per-plane labels and z-positions
- PSF / MTF — 2D PSF plots and radial MTF profiles (with optional diffraction-limit overlay)
- Polarization — 4-panel Stokes parameter maps and polarization ellipse overlays on intensity images
- Beam profile — 1D intensity cross-section with D4σ markers and optional Gaussian fit
System Propagation
propagate_through_system()— pass a field through an ordered list of elements with one function callraytrace_system()— geometric ray-trace the same element list for quick cross-validation against wave-optics
Through-focus / Tolerancing
through_focus_scan— propagate the exit field across a range of axial planes and tabulate Strehl, peak intensity, D4σ spot, RMS radius, encircled energy at each planefind_best_focus— optimise the metric of choice across the scansingle_plane_metrics— full set of beam metrics at a single zdiffraction_limited_peak— reference for Strehl computationstolerancing_sweep— apply a list ofPerturbation(decenter, tilt, form-error) and compare best-focus Strehl/spot for eachmonte_carlo_tolerancing— random perturbation draws from user-specified distributions, aggregate Strehl statistics
Hybrid Wave/Ray Design Optimization (lumenairy.optimize)
DesignParameterization— flat-vector ↔ prescription dict mapping with arbitrary path-based free variables and boundsMeritTermbuilding blocks:FocalLengthMerit,BackFocalLengthMerit,SphericalSeidelMerit,StrehlMerit,RMSWavefrontMerit,SpotSizeMerit,ChromaticFocalShiftMerit,LGAberrationMerit(closed-form named aberration suppression via the LG aberration tensor — see Phase-space asymptotic propagator)design_optimize— main driver wrappingscipy.optimize(L-BFGS-B / SLSQP / trust-constr /lm). Wave leg only runs when a wave-based merit term needs it; pure-geometric optimization is sub-second for typical lenseslmmethod routes throughscipy.optimize.least_squares, which uses Householder QR with column pivoting under the hood
Phase-space asymptotic propagator (lumenairy.asymptotic)
fit_canonical_polynomials— 4-variable Chebyshev tensor-product fit ofPhi(s2, v2)ands1(s2, v2)from a ray-traced grid; sub- microwave residuals on refractive systemsaberration_tensor— closed-form Laguerre-Gaussian aberration tensor whose indices correspond directly to classical Seidel/Zernike aberrations (defocus, spherical, coma, astigmatism, trefoil, ...)propagate_modal_asymptotic— closed-form leading-order Maslov propagator; Collins-ABCD in source-dominated limit, Fourier-of-pupil in pupil-dominated limit; ~10³-10⁴× faster per pixel than direct Maslov quadraturesolve_envelope_stationary— Newton-solve the chief-ray envelope-stationary equation directly on the Chebyshev fit- LG / HG basis utilities —
lg_polynomial,hg_polynomial,evaluate_lg_mode,evaluate_hg_mode,decompose_lg,decompose_hg,lg_seidel_label - Wick moment utilities —
gaussian_moment_2d,gaussian_moment_table_2dfor 2-D Gaussian moments under complex- symmetric covariance
Installation
Development install (editable)
From the project root (the Optical_Propagation_Library/ directory),
install the package in editable mode:
cd Optical_Propagation_Library
pip install -e .
This makes lumenairy importable from anywhere and any edits
to the source files take effect immediately without reinstalling.
Standard install
cd Optical_Propagation_Library
pip install .
Manual (no install)
If you prefer not to install the package, you can place your scripts
next to the Optical_Propagation_Library directory and add it to
sys.path:
import sys
sys.path.insert(0, 'Optical_Propagation_Library')
import lumenairy as la
Usage
Once installed (or on sys.path):
import lumenairy as la
# or
from lumenairy import angular_spectrum_propagate, JonesField
Dependencies
Canonical source: pyproject.toml [project.dependencies] and
[project.optional-dependencies].
Required
numpy— core numericsscipy— Zernike decomposition (Householder QR),design_optimize, multi-threaded FFTs (USE_SCIPY_FFT = True)matplotlib— all plotting utilitiespsutil— accurate memory detection (falls back to a 4 GB default without it)
Optional (install via extras)
[glass]—refractiveindex>=1.0. Extended glass-catalog lookups (Hikari, Sumita, formula-3 CDGM). v4.16.1 moved this from hard dep to an optional extras group; minimal installs transparently fall back to the bundled Sellmeier (and v4.16.2+ formula-3) evaluators for the 46+ glasses with bundled coefficients.[fft]—pyfftw>=0.13. Multi-threaded CPU FFT; extra ~10–20% on top of SciPy FFT.[gpu]—cupy>=11.0. GPU acceleration on NVIDIA CUDA or AMD ROCm (auto-detected; passuse_gpu=Trueto supported functions). Install the wheel that matches your hardware:pip install cupy-cuda12x(CUDA 12.x) orpip install cupy-rocm-6-1(ROCm 6.x). See the Installation wiki page for the full toolkit matrix and known-good GPU families.[fits]—astropy>=5.0. FITS file I/O.[hdf5]—h5py>=3.0+filelock>=3.0. HDF5 field storage.[zarr]—zarr>=3.0+filelock>=3.0. Zarr storage backend (alternative to HDF5).[jax]—jax>=0.4.20. Automatic differentiation (CPU).[jax-gpu]—jax[cuda12]>=0.4.20. JAX with CUDA wheels.[perf]—numexpr>=2.8. Fused phase-screen multiply (1.5–2× speedup at large N).[numba]—numba>=0.58. JIT fastpath for traced-lens Newton-fit.[multi_objective]—pymoo>=0.6. NSGA-II fordesign_optimize_multi_objective.[gui]—PySide6+pyvista+pyvistaqt+h5py. LumenAiry Designer GUI.[all]— every optional accelerator bundled together.
Install the core (no glass extension):
pip install lumenairy
Add the glass-catalog extension (Hikari / Sumita / formula-3 CDGM):
pip install lumenairy[glass]
Kitchen-sink install:
pip install lumenairy[all]
Quick start: which function should I use?
The library is wide -- pick a question and the table tells you where to start.
I want to propagate a field through free space
| Situation | Use |
|---|---|
| Don't know which propagator to pick | la.propagate(E, z=z, wavelength=wl, dx=dx) -- smart auto-dispatch |
| Standard near/mid-field | la.angular_spectrum_propagate(E, z, wl, dx) |
| Need a specific output grid pitch (focal-plane zoom, MFT) | la.fresnel_propagate_mft(...) or la.fraunhofer_propagate_mft(...) |
| Long-z (zlambda/(Ndx) > 1, plain ASM would need an oversized grid) | la.scalable_angular_spectrum_propagate(...) |
| Far-field | la.fraunhofer_propagate(E, z, wl, dx) |
| Through one ideal thin lens | la.apply_thin_lens(E, f=f, wavelength=wl, dx=dx) |
I have a lens prescription (Zemax .zmx, Thorlabs catalog, or a dict)
| Situation | Use |
|---|---|
| Default fast wave model | la.apply_real_lens(E, prescription=presc, wavelength=wl, dx=dx) |
| Sub-nm OPD on cemented doublets or multi-surface curved-interface systems | la.apply_real_lens_traced(E, prescription=presc, wavelength=wl, dx=dx) |
| Inside a JAX-autodiff design loop, or near a caustic | la.apply_real_lens_maslov(...) / la.apply_real_lens_maslov_jax(...) |
| Just want a spot diagram | la.trace_prescription(presc, wl, num_rings=8) |
| Paraxial EFL / BFL / first-order data | la.first_order_data(presc, wl) |
| Element list shared between wave and ray paths | la.propagate_through_system(...) and la.raytrace_system(...) |
I have a folded design (mirrors + coord-breaks)
| Situation | Use |
|---|---|
| Build world-frame surfaces from the prescription | la.world_surfaces_from_prescription(presc) |
| Ray-trace through folds | la.trace_world(rays, wsurfs, wl) |
| Paraxial image-plane position in world coordinates | la.paraxial_focus_world(wsurfs, wl) |
I need to analyze the output field
| Situation | Use |
|---|---|
| PSF + MTF | la.compute_psf(pupil, wl, f, dx), la.compute_mtf(psf) |
| Strehl ratio from a PSF (peak-ratio) | la.strehl_ratio(E, E_ref, dx) |
| Strehl from an RMS estimate (Marechal closed form) | la.strehl_marechal(rms_waves) |
| Strehl from a pupil (exact small-aberration) | la.strehl_phase_integral(pupil) |
| OPD / Zernike decomposition | la.wave_opd_2d(...), la.zernike_decompose(opd, dx, ap) |
| Off-axis WFE at one field point | la.eval_image_plane_wfe(presc, wl, field=(Hx, Hy)) |
| Full WFE-vs-field grid | la.field_grid_wfe(presc, wl, field_max_m, n_field) |
| Field-resolved distortion / sensitivity / per-surface footprint | la.distortion_vs_field, la.sensitivity_ranking, la.footprint_per_surface |
| Field-curvature / astigmatism vs field (real-ray) | la.field_aberration_sweep(...) |
| Off-axis Seidel coefficients | la.seidel_field_sweep(surfaces, wl, field_heights) |
I want to optimize / tolerance a design
| Situation | Use |
|---|---|
| Hybrid wave/ray optimisation with composable merit terms | la.design_optimize(parameterization, merit_terms) |
| Tolerance sensitivity ranking | la.sensitivity_ranking(merit_fn, x0) |
| Through-focus / Monte-Carlo tolerancing | la.through_focus_scan(...), la.monte_carlo_tolerancing(...) |
Three minimal end-to-end examples
# 1. Free-space propagation, smart dispatch -- works for any geometry.
import lumenairy as la
E, x, y = la.create_gaussian_beam(N=512, dx=2e-6, wavelength=1.31e-6, sigma=50e-6)
E_focus = la.angular_spectrum_propagate(E, z=1e-3, wavelength=1.31e-6, dx=2e-6)
print('centroid =', la.beam_centroid(E_focus, 2e-6))
# 2. A Thorlabs lens, end-to-end.
presc = la.thorlabs_lens('AC254-100-C')
E_out = la.apply_real_lens(E, prescription=presc, wavelength=1.31e-6, dx=2e-6)
# 3. Ray-trace the same lens for a spot RMS.
result = la.trace_prescription(presc, wavelength=1.31e-6, num_rings=8)
print(f'spot RMS = {la.spot_rms(result)[0]*1e6:.2f} um')
That's enough to start. For longer recipes -- folded designs,
Zernike decomposition, optimisation loops, polychromatic PSFs,
HDF5 storage, plotting -- see the Cookbook section near the end
of this README.
Cookbook
The deep recipe / use-case walkthrough is now at
docs/cookbook.md. Examples in
examples/ are runnable scripts that cover most of
the same surface.
Project Layout
Optical_Propagation_Library/ # project root
README.md # this file
LICENSE # MIT license
CHANGELOG.md # version-by-version release notes
pyproject.toml # build / install configuration
requirements.txt # runtime dependencies
lumenairy/ # the importable package
__init__.py # public API re-exports (272 symbols)
propagation.py # ASM, tilted ASM, Fresnel, Fraunhofer,
# Rayleigh-Sommerfeld, Scalable ASM
# (SciPy-FFT default, multithreaded)
lenses.py # Thin/thick/aspheric/real lenses +
# apply_real_lens_traced (hybrid) +
# apply_real_lens_maslov (phase-space),
# surface_sag_general / _biconic,
# cylindrical / GRIN / axicon
glass.py # Glass catalog + refractiveindex.info
coatings.py # Thin-film stack (TMM) + QW / broadband
# AR coating designs
bsdf.py # Surface-scatter BSDF models
# (Lambertian, Gaussian, Harvey-Shack)
elements.py # Mirrors, apertures, masks, Zernike
# aberrations, turbulence phase screens
sources.py # Gaussian, HG, LG, top-hat, annular,
# Bessel, LED, fiber, tilted plane,
# point source, multi-field generator
freeform.py # XY-poly / Zernike / Chebyshev
# freeform surface sags
analysis.py # Centroid, D4σ, Strehl, PSF/OTF/MTF,
# OPD extraction (1D/2D),
# Zernike decomposition (QR-based),
# check_opd_sampling, chromatic
doe.py # Gratings, MLA, Dammann, FITS / phase
# file I/O
interferometry.py # Interferogram synthesis + PSI
phase_retrieval.py # Gerchberg-Saxton, HIO, ER
prescriptions.py # Singlet, doublet, biconic, cylindrical,
# Thorlabs catalog, Zemax I/O, CODE V I/O
system.py # Sequential element-list propagator
# (accepts method='asm'|'fresnel'|'sas')
raytrace.py # Geometric ray tracer (Snell, ABCD,
# Seidel, pupils, find_lenses,
# refocus, through_focus_rms, spot,
# ray fan, OPD fan, error codes;
# biconic-aware; OPL fix)
through_focus.py # Strehl, best-focus, single-plane
# metrics, tolerancing, MC analysis
optimize.py # Hybrid wave/ray design optimizer
# (DesignParameterization, 12+ MeritTerms,
# scipy.optimize wrapper; DE, basin-hop)
vector_diffraction.py # Richards-Wolf high-NA vectorial focus
coherence.py # Koehler/extended-source partially-
# coherent imaging, mutual coherence
detector.py # Detector pixel model (shot/read noise,
# dark current, full-well saturation),
# Shack-Hartmann WFS simulator
polarization.py # Jones vector / JonesField (with SAS
# propagate), waveplates, Stokes
ghost.py # Ghost-path analysis for a multi-
# surface lens
multiconfig.py # Multi-configuration / afocal
# (Keplerian, beam expander)
rcwa.py # 1-D rigorous coupled-wave analysis
storage.py # Unified HDF5/Zarr auto-dispatch backend
hdf5_io.py # Back-compat re-export shim for storage
memory.py # RAM budget and batch-size helpers
user_library.py # Persistent user material/lens/mask library
codegen.py # Auto-generate sim scripts from Zemax
plotting.py # Matplotlib plots (field, PSF, MTF,
# Stokes, polarization ellipses,
# Jones pupil 2×4 grid)
progress.py # ProgressCallback + ProgressScaler
_backends.py # CPU / thread helpers
validation/ # topic-based validation suite
_harness.py # Shared Harness class
run_all.py # discovers test_*.py + runs in
# fresh subprocesses (per-file PASS/FAIL
# + aggregate summary)
test_propagation.py # ASM, Fresnel, Fraunhofer, R-S, SAS,
# tilted, sampling
test_lenses.py # thin/real/biconic/cyl/asph/GRIN/axicon
# + Maslov/traced, wave_propagator switch
test_raytrace.py # Snell, ABCD, Seidel, pupils, refocus,
# through_focus, spot/fans/off-axis
test_analysis.py # Zernike, Strehl, MTF, Airy, Gaussian-
# ABCD, OPD metrics, overlap invariance
test_sources.py # 10 beam generators
test_elements.py # apertures, mirror, turbulence, Zernike
test_polarization.py # HWP, QWP, Stokes, Jones, Jones pupil
test_advanced_diffraction.py # Richards-Wolf + Koehler + mutual
test_optimize.py # all merits + L-BFGS / DE / basin-hop
test_io.py # HDF5 / Zemax / CODE V / user_library
test_features.py # coatings / DOE / RCWA / freeform /
# ghost / interferometry / multi-config
# + BSDF
test_detector.py # detector / Shack-Hartmann / GS
test_glass_tolerancing.py # dispersion / achromat / tolerances
test_integration.py # API exports / compositions / memory
# / plotting smoke
test_subsample.py # apply_real_lens_traced subsample
# guardrail + ProcessPool + scaling
test_validation_lens.py # known-answer lens harness
real_lens_opd/ # 3-method OPD comparison sweep
run_validation.py # paraxial / slant / ray-traced
results/ # per-case PNGs + report.md/csv
zemax_prescriptions/ # matching .txt/.zmx for cross-check
through_focus_smoke/ # quick Strehl + tolerancing demo
Conventions
- Time dependence:
exp(-i*omega*t)throughout - Units: SI meters for all spatial quantities
- Sign convention for radii: positive = center of curvature to the right of the surface (standard optics / Zemax convention)
- Grid: always square (N × N), centered at the origin
Appendix: Physics references
Citation-heavy notes that don't belong in the main flow. The short version of "what the propagators are" is in the Quick start tables at the top of this file; everything below is here for users who want the underlying physics.
The library is designed around the Angular Spectrum Method, which is exact
(within sampling limits) for free-space propagation. All band-limiting uses
the Matsushima-Shimobaba (2009) rectangular criterion for anti-aliasing.
The Scalable ASM (scalable_angular_spectrum_propagate) uses the
Heintzmann-Loetgering-Wechsler (2023) three-FFT kernel for long-z
propagation where the plain ASM grid would need to be impractically
large.
Real-lens accuracy strategy
Two complementary models are provided:
apply_real_lens(analytic thin-element) — split-step refraction with ASM between surfaces. Each refracting surface is treated as a phase screen computed from the exact surface sag (conic + polynomial aspheric + biconic if specified), and the wave propagates through the glass between surfaces in the in-medium wavelengthlambda/n. Captures diffraction during in-glass propagation; sub-100-nm RMS agreement with geometric ray trace on most singlets. Has a hard accuracy ceiling on cemented doublets and other multi-surface curved-interface systems because the wave model treats each glass region as a vertex-to-vertex uniform slab while real rays cross interior interfaces at z = sag(h).apply_real_lens_traced(hybrid wave/ray) — bypasses that ceiling by computing the exit-pupil OPD from a geometric ray trace (per-pixel Newton inversion of the entrance→exit map via cubic splines) and combining with a wave-optics amplitude envelope. Sub-nanometer OPD agreement with the geometric ray trace when properly sampled, at the cost of ~10–30 s on N=4096 grids.apply_real_lens_maslov(phase-space / Maslov integral) — traces a Chebyshev-node grid of rays from entrance to exit, fits a 4-variable Chebyshev tensor-product polynomial to the canonical maps1(s2, v2)andOPD(s2, v2), then evaluates the Maslov integralE(s2) = int E_in(s1(s2,v2)) exp(2 pi i OPD(s2,v2)) |det(ds1/dv2)| d^2 v2at each output pixel. Caustic-safe (handles multi-valued ray maps that break_traced), no critical-sampling constraint, and analytically differentiable -- the JAX twinapply_real_lens_maslov_jaxis the right tool inside an autodiff optimisation loop.
A critical OPL-bookkeeping fix in raytrace._intersect_surface (the
small "vertex-plane → actual-sag-intersection" leg now correctly
accumulates n·path in the right medium) cut singlet wave-vs-geom
residuals by 17×–130× and brought the geometric reference itself into
agreement with sequential ray tracers used elsewhere in the optics
community.
Sampling rule for OPD extraction
Extracting OPD from a converging-wavefront simulation requires
dx ≤ λ·f / aperture so that np.unwrap doesn't lose cycles at the
pupil edge. check_opd_sampling() reports the Nyquist margin and
flags marginal sampling. wave_opd_1d / wave_opd_2d accept a
focal_length argument to emit a RuntimeWarning when the sampling
is risky, and an f_ref argument to subtract a reference sphere
before unwrap (allows coarser grids at the cost of needing the
focal length up front).
License
MIT License — see LICENSE file.
Acknowledgments
The Dammann grating design function (makedammann2d) is a Python port by
Andrew Traverso of the original Octave/MATLAB implementation by Daniel Marks.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lumenairy-5.4.1.tar.gz.
File metadata
- Download URL: lumenairy-5.4.1.tar.gz
- Upload date:
- Size: 2.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e4ef10653a95ccacb316b9da7d7ff4704e4f7b51d5fb580b2165061f7c50d85
|
|
| MD5 |
8c9fe1a301347a0d299e88b2a8e4bc70
|
|
| BLAKE2b-256 |
977cd4cece0e451369a6fc30a25ff519fe70aa899e20081caf8f742f8142c8ec
|
Provenance
The following attestation bundles were made for lumenairy-5.4.1.tar.gz:
Publisher:
publish.yml on travaj24/LumenAiry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lumenairy-5.4.1.tar.gz -
Subject digest:
2e4ef10653a95ccacb316b9da7d7ff4704e4f7b51d5fb580b2165061f7c50d85 - Sigstore transparency entry: 1630370104
- Sigstore integration time:
-
Permalink:
travaj24/LumenAiry@b9acd9f24bf69ecf2281e33796fe06514f3c34d8 -
Branch / Tag:
refs/tags/v5.4.1 - Owner: https://github.com/travaj24
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b9acd9f24bf69ecf2281e33796fe06514f3c34d8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file lumenairy-5.4.1-py3-none-any.whl.
File metadata
- Download URL: lumenairy-5.4.1-py3-none-any.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60dbb4049c771585f83bd56efaa54ab5ed6cf54289bdef5fcbbd557836b8296a
|
|
| MD5 |
8891919885c62457b9e21c5691dc541b
|
|
| BLAKE2b-256 |
36c90e504cc326f7c39ba276cd9236a29621906298903ba2b03aa6da79f27c76
|
Provenance
The following attestation bundles were made for lumenairy-5.4.1-py3-none-any.whl:
Publisher:
publish.yml on travaj24/LumenAiry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lumenairy-5.4.1-py3-none-any.whl -
Subject digest:
60dbb4049c771585f83bd56efaa54ab5ed6cf54289bdef5fcbbd557836b8296a - Sigstore transparency entry: 1630370108
- Sigstore integration time:
-
Permalink:
travaj24/LumenAiry@b9acd9f24bf69ecf2281e33796fe06514f3c34d8 -
Branch / Tag:
refs/tags/v5.4.1 - Owner: https://github.com/travaj24
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b9acd9f24bf69ecf2281e33796fe06514f3c34d8 -
Trigger Event:
release
-
Statement type: