Skip to main content

OWL — Optics Widget Library

Small, inspectable optics experiments in Python notebooks. Built on anywidget; inspired by wigglystuff's notebook-first, composable widgets. No wigglystuff dependency or copied implementation.

Supported models: free space, ideal thin lenses, flat/curved refractive interfaces, flat/curved mirrors, thick lenses in air, circular hard apertures, paraxial rays, ideal Gaussian references, and scalar paraxial FFT diffraction. Bench supports all these elements, tunes wavelength/waist and wave sampling, and defaults to clipped rays over the x–z physical-intensity center cut. The trace selector also offers physical optics alone, rays alone, and the unclipped Gaussian reference; the transverse cross-section panel remains plane-selectable. Calculations live in Python, not duplicated in JavaScript.

Run the notebook

From this checkout's optics directory:

uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/optics_basics.py
uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/apertures.py
uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/keplerian_telescope.py

The notebook declares all its dependencies inline (PEP 723). Its [tool.uv.sources] entry resolves OWL from .., relative to the notebook, as an editable local package. No global installation or sys.path modifications are needed. Keep the checkout together: the notebook alone is not portable until OWL is published or its source points at an accessible pinned distribution.

After changing OWL's source, pass --reinstall-package owl-widgets so the sandbox rebuilds the local wheel; --refresh-package alone leaves an already-installed build in place.

  • optics_basics.py: free propagation, thin lenses, ray focus versus Gaussian waist, wavelength tuning, mirrors, interfaces, and thick lenses.
  • apertures.py: controlled Fresnel aperture sweeps, brightest-plane spots, launch-height-colored ray/wave comparisons, stop taxonomy, field-height-colored ray fans, relative illumination, and cat’s-eye pupil maps.
  • keplerian_telescope.py: contrasts a two-positive-lens ray-optical afocal spacing ($f_{obj} + f_{eye}$) with the spacing that minimizes Gaussian-beam divergence.

Each experiment owns independent inputs. Study controls update their plots and the notebook's downstream tables and narrative together.

Compose a bench

import marimo as mo
from owl import Bench, FreeSpace, GaussianBeam, OpticalSystem, ThinLens

widget = Bench(
    OpticalSystem((FreeSpace(60), ThinLens(50), FreeSpace(150))),
    beam=GaussianBeam(waist_mm=0.07, wavelength_nm=633),
    ray_span_mm=0.1,
)
bench = mo.ui.anywidget(widget)
bench

In a separate downstream marimo cell, read bench.value to react to edits:

settings = bench.value["settings"]
system = OpticalSystem.from_dict(settings["system"])
beam = GaussianBeam.from_dict(settings["beam"])
profile = system.gaussian(beam)
profile.z_mm, profile.radius_mm, profile.q_mm

Bench is an anywidget, with no marimo dependency. Other notebook hosts have not yet been integration-tested. All JavaScript/CSS ships in the wheel; no CDN or Node build is needed to use it.

Settings and measurements

  • bench.value["result"]: ray heights, sampled Gaussian radii, element positions, output-plane measurements, and wave results. result["rays"]["alive"] is a sample-by-ray survival mask; blocked rays stay blocked.
  • result["longitudinal"] contains the x–z physical-intensity center cut (|U|² normalized to the input peak). The bench shows it on a three-decade logarithmic scale with rays overlaid by default.
  • result["cross_sections"] contains planes, x_mm, window_mm, and warnings. Each plane has label, z_mm, intensity, and transmission; intensity[y][x] uses x_mm for both transverse axes. Planes retain the input and one exit per original element, in encounter order.
  • widget.to_dict() / Bench.from_dict(settings): detached JSON-compatible experiment settings, including wave = {"grid_size": ..., "window_mm": ...}.
  • Assign a new complete widget.settings dictionary for programmatic updates; nested in-place edits do not trigger trait notifications.
  • Invalid numerical edits retain the last valid calculation and expose error.
  • View selection is local to each display and is not part of the saved optical experiment.
  • Sliders preview locally while scrubbing and recalculate on release; number inputs support values outside the default slider ranges.

Apertures and linked cross sections

Aperture(radius_mm, x_mm=0, y_mm=0) is exported from owl: a circular hard opening, including its boundary. Meridional rays lie at transverse x=0; both aperture offsets affect clipping. Apertures do not modify the Gaussian q reference.

Bench(..., grid_size=128, window_mm=None) accepts keyword-only wave settings. Grid sizes are 64, 128, 256, or 512; window_mm is the full square width, with None selecting an automatic estimate. The bench exposes these sampling controls in its UI.

Create the source and linked display in the same marimo cell, then read values only downstream:

from owl import Aperture, CrossSection

source = Bench(
    OpticalSystem((Aperture(0.3), FreeSpace(100))),
    beam=GaussianBeam(0.15, 633),
    grid_size=256,
    window_mm=2,
)
source_view = mo.ui.anywidget(source)
section_view = mo.ui.anywidget(CrossSection(source))
mo.vstack([source_view, section_view])

CrossSection(source) observes the existing Bench calculation; it does not propagate a second field. Each display selects its own plane. Do not reconstruct the source from source_view.value in a reactive downstream cell. Close a discarded raw CrossSection widget to detach its observer.

Intensity is normalized to the input peak, not each plane's peak. The longitudinal center cut uses a three-decade logarithmic scale; transverse heatmaps use a fixed black-to-gold 0–1 scale, saturating above 1. The plane-selected transverse center slice (nearest y=0) shares a vertical scale across all planes and retains focused peaks above 1. Transmission is sampled integrated power divided by sampled input power, not a ray-count fraction.

The scalar Fresnel solver uses a periodic FFT window. Auto sizing follows the unclipped Gaussian and aperture extents, not all diffracted light. Increase window width to check wraparound and grid size to check hard-edge/phase sampling; increasing width at fixed grid also coarsens resolution. Hard circular masks require convergence checks. Warnings flag risks, and their absence is not proof of convergence. Interfaces omit Fresnel reflection losses.

Aperture study widgets

The aperture notebook uses small, independent study widgets so controls stay beside the plots they affect. ApertureFocusStudy() renders the four-case longitudinal/brightest-plane Fresnel comparison. RayWaveStudy() renders the matched ray-density/Fresnel comparison at the geometric image; passing fan rays are colored by launch height and blocked rays are red. ApertureStudy() remains available as a combined compatibility view.

from owl import ApertureFocusStudy, ApertureStudySpec, RayWaveStudy

focus = mo.ui.anywidget(ApertureFocusStudy(ApertureStudySpec(
    grid_size=256,
    window_mm=8,
    ray_count=100_000,
)))
focus

comparison = mo.ui.anywidget(RayWaveStudy(ApertureStudySpec()))
comparison

StopRayStudy() renders the aperture-stop, field-stop, and vignetting comparison, including field-height-colored ray fans, chief and marginal rays, geometric relative illumination, and stop coefficients/classification. PupilMapStudy() independently renders the field-dependent cat’s-eye pupil maps. StopStudy() remains the combined compatibility view.

from owl import PupilMapStudy, StopRayStudy, StopStudySpec

stops = mo.ui.anywidget(StopRayStudy(StopStudySpec()))
stops

pupils = mo.ui.anywidget(PupilMapStudy(StopStudySpec()))
pupils

Both are AnyWidgets with synced settings, result, and error. Assign complete settings dictionaries; invalid updates roll back atomically. ApertureStudySpec and StopStudySpec, plus calculate_aperture_study() and calculate_stop_study(), provide the same validated calculations without a frontend. Results use finite JSON and expose the arrays and metrics used by the plots, so downstream marimo cells can build live tables and explanations.

Numerical interface

import numpy as np
from owl import FreeSpace, GaussianBeam, OpticalSystem, ThinLens

system = OpticalSystem((FreeSpace(100), ThinLens(50), FreeSpace(100)))
rays = system.trace(np.array([[0.1, -0.1], [0.0, 0.0]]))
profile = system.gaussian(GaussianBeam(0.25, 1000))
matrix = system.matrix()
  • Lengths: mm; source wavelength: nm; ray angles: rad.
  • Rays use (height, angle) column vectors; matrix composition follows element encounter order.
  • RayTrace.rays has shape (samples, 2, rays); samples retain the input and each element exit, expanding thick lenses into entry/gap/exit.
  • Both trace types expose per-sample n (refractive index) and s (direction, +1/-1). z_mm is signed laboratory position and can decrease after reflection.
  • length_mm is total traveled geometric length, including glass thickness, not output z or index-weighted optical path length. Use output_z_mm, output_n, and output_direction for the output state.
  • Gaussian input: waist at z=0, in air, M²=1; q=z+i z_R. Radius is the 1/e² intensity radius.
  • GaussianTrace retains both sides of surfaces and internal thick-lens propagation. A sampled minimum is not an exact waist measurement; the notebook uses output q for exact free-continuation calculations in the output medium, with local wavelength beam.wavelength_mm / system.output_n.
  • Free-continuation distances follow the outgoing direction: q(d) = q_out + d, z(d) = output_z_mm + output_direction * d. Negative distances extrapolate the output field, not propagation back through earlier elements.
  • Bench rays are a parallel fan, not a sampled phase-space representation of the Gaussian beam. Both plot layers use the same physical units; axis extents auto-fit the selected layers.

Surfaces and return paths

Import Interface, Mirror, and ThickLens from owl. Flat surfaces use Python np.inf (JSON null); radius zero is invalid. An interface's positive radius places its center of curvature along the incident direction; a mirror's positive radius is concave. Interface(R, n1, n2) requires n1 to match the current medium. FreeSpace(d) travels a nonnegative distance in that medium; ThickLens(R1, R2, thickness, n) requires surrounding air.

Systems list elements in encounter order, not sorted laboratory z. Mirrors reverse direction but do not automatically revisit earlier optics. Explicitly append return-path gaps and surfaces: a reversed interface is Interface(-R, n2, n1), and a reversed thick lens is ThickLens(-R2, -R1, thickness, n) (swap and negate radii). Keep return-path indices consistent too.

StopStudy classifies the controlled thin-lens relay's pupil-plane, image-plane, and intermediate stops from its A/B coefficients. General multi-element automatic stop selection, nonparaxial physical optics, beamlets, dispersion, aberrations, polarization, and nonideal M² are not implemented yet. Changing wavelength does not change an ideal lens's focal length. Very strong lenses/tight beams can violate the paraxial assumption; the core validates numerical inputs, not physical applicability.

Develop and validate

uv run --no-project --refresh-package owl-widgets --with . --with pytest pytest tests
uvx --from marimo==0.24.0 marimo check --strict notebooks/optics_basics.py notebooks/apertures.py notebooks/keplerian_telescope.py

uvx --from marimo==0.24.0 marimo export html --sandbox notebooks/optics_basics.py -o /tmp/owl-optics-basics.html
uvx --from marimo==0.24.0 marimo export html --sandbox notebooks/apertures.py -o /tmp/owl-apertures.html

uv build

Browser renderer tests (Node.js; first run downloads Chromium):

npm exec --yes --package=@playwright/test -- playwright install chromium
npm exec --yes --package=@playwright/test -- node --test tests/test_frontend.mjs
npm exec --yes --package=@playwright/test -- node --test tests/test_studies_frontend.mjs

Browser tests exercise the real renderer with a model double, not a live marimo kernel. Python tests cover propagation and widget updates separately. Static HTML export runs the notebook but is not a substitute for a live kernel for Python-backed controls.

Publication

Distribution name: owl-widgets; Python import: owl; working version: 0.1.0. Licensed under the MIT License; see LICENSE.

Not published. The PyPI JSON endpoint returned 404 for owl-widgets when checked on 2026-10-12, suggesting the name is currently unclaimed; confirm again immediately before upload. Project, repository, and issue URLs are in pyproject.toml. Before release, test a live notebook round trip. Once a release exists, replace the inline local source with the pinned published dependency. Existing abcd/ notebooks are unchanged.

Metadata

Release files for owl-widgets 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for owl-widgets 0.1.0
File Size Uploaded Provenance
owl_widgets-0.1.0.tar.gz 1.6 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for owl-widgets 0.1.0
File Interpreter ABI Platform
owl_widgets-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 3.1 MB

Release files / owl_widgets-0.1.0.tar.gz

Download URL owl_widgets-0.1.0.tar.gz
Size 1.6 MB
Tags Source
SHA-256 checksum
How to use checksums
b40b624146014f98b6b05387299838ba8318da089f1551707c6f13a32b320185
BLAKE2b-256 checksum
How to use checksums
7d59aad73c8ce1e171d7b414d5f83f6ac31cf194d3c2c0030aa3e015c0d112d6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / owl_widgets-0.1.0-py3-none-any.whl

Download URL owl_widgets-0.1.0-py3-none-any.whl
Size 1.5 MB
Tags Python 3
SHA-256 checksum
How to use checksums
5fd3ffbfd944de63c93a5306b5658df31125dd6170bd397666ff28718d1cc42f
BLAKE2b-256 checksum
How to use checksums
9c64b111f0b147eaa638400dc192d7d0b0031d4f15055d3f6319e6473390433b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for owl-widgets 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page