owl-widgets 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. owl_widgets-0.1.0/.gitignore +9 -0
  2. owl_widgets-0.1.0/LICENSE +21 -0
  3. owl_widgets-0.1.0/PKG-INFO +196 -0
  4. owl_widgets-0.1.0/README.md +181 -0
  5. owl_widgets-0.1.0/THIRD_PARTY_NOTICES.md +11 -0
  6. owl_widgets-0.1.0/notebooks/apertures.py +294 -0
  7. owl_widgets-0.1.0/notebooks/keplerian_telescope.py +291 -0
  8. owl_widgets-0.1.0/notebooks/optics_basics.py +226 -0
  9. owl_widgets-0.1.0/pyproject.toml +24 -0
  10. owl_widgets-0.1.0/src/owl/__init__.py +59 -0
  11. owl_widgets-0.1.0/src/owl/aperture_study.py +250 -0
  12. owl_widgets-0.1.0/src/owl/bench.py +248 -0
  13. owl_widgets-0.1.0/src/owl/core.py +530 -0
  14. owl_widgets-0.1.0/src/owl/cross_section.py +44 -0
  15. owl_widgets-0.1.0/src/owl/frontend.py +9 -0
  16. owl_widgets-0.1.0/src/owl/static/PLOTLY-LICENSE.txt +9 -0
  17. owl_widgets-0.1.0/src/owl/static/bench.css +313 -0
  18. owl_widgets-0.1.0/src/owl/static/bench.js +637 -0
  19. owl_widgets-0.1.0/src/owl/static/plotly.min.js +3620 -0
  20. owl_widgets-0.1.0/src/owl/static/plotly_views.js +299 -0
  21. owl_widgets-0.1.0/src/owl/static/studies.css +382 -0
  22. owl_widgets-0.1.0/src/owl/static/studies.js +1183 -0
  23. owl_widgets-0.1.0/src/owl/stop_study.py +303 -0
  24. owl_widgets-0.1.0/src/owl/studies.py +178 -0
  25. owl_widgets-0.1.0/src/owl/wave.py +250 -0
  26. owl_widgets-0.1.0/tests/test_aperture_core.py +294 -0
  27. owl_widgets-0.1.0/tests/test_aperture_study.py +140 -0
  28. owl_widgets-0.1.0/tests/test_aperture_widgets.py +103 -0
  29. owl_widgets-0.1.0/tests/test_bench.py +278 -0
  30. owl_widgets-0.1.0/tests/test_core.py +330 -0
  31. owl_widgets-0.1.0/tests/test_frontend.mjs +1325 -0
  32. owl_widgets-0.1.0/tests/test_stop_study.py +156 -0
  33. owl_widgets-0.1.0/tests/test_studies.py +246 -0
  34. owl_widgets-0.1.0/tests/test_studies_frontend.mjs +892 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ __marimo__/
8
+ .DS_Store
9
+ .workmux.yaml
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OWL contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.5
2
+ Name: owl-widgets
3
+ Version: 0.1.0
4
+ Summary: OWL: composable optics widgets for Python notebooks
5
+ Project-URL: Homepage, https://github.com/galwiner/owl-widgets
6
+ Project-URL: Repository, https://github.com/galwiner/owl-widgets
7
+ Project-URL: Issues, https://github.com/galwiner/owl-widgets/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: anywidget>=0.9
12
+ Requires-Dist: numpy>=2
13
+ Requires-Dist: traitlets>=5
14
+ Description-Content-Type: text/markdown
15
+
16
+ # OWL — Optics Widget Library
17
+
18
+ Small, inspectable optics experiments in Python notebooks. Built on anywidget; inspired by [wigglystuff](https://github.com/koaning/wigglystuff)'s notebook-first, composable widgets. No wigglystuff dependency or copied implementation.
19
+
20
+ **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.
21
+
22
+ ## Run the notebook
23
+
24
+ From this checkout's `optics` directory:
25
+
26
+ ```sh
27
+ uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/optics_basics.py
28
+ uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/apertures.py
29
+ uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/keplerian_telescope.py
30
+ ```
31
+
32
+ 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.
33
+
34
+ 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.
35
+
36
+ - `optics_basics.py`: free propagation, thin lenses, ray focus versus Gaussian waist, wavelength tuning, mirrors, interfaces, and thick lenses.
37
+ - `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.
38
+ - `keplerian_telescope.py`: contrasts a two-positive-lens ray-optical afocal spacing ($f_{obj} + f_{eye}$) with the spacing that minimizes Gaussian-beam divergence.
39
+
40
+ Each experiment owns independent inputs. Study controls update their plots and the notebook's downstream tables and narrative together.
41
+
42
+ ## Compose a bench
43
+
44
+ ```python
45
+ import marimo as mo
46
+ from owl import Bench, FreeSpace, GaussianBeam, OpticalSystem, ThinLens
47
+
48
+ widget = Bench(
49
+ OpticalSystem((FreeSpace(60), ThinLens(50), FreeSpace(150))),
50
+ beam=GaussianBeam(waist_mm=0.07, wavelength_nm=633),
51
+ ray_span_mm=0.1,
52
+ )
53
+ bench = mo.ui.anywidget(widget)
54
+ bench
55
+ ```
56
+
57
+ In a **separate downstream marimo cell**, read `bench.value` to react to edits:
58
+
59
+ ```python
60
+ settings = bench.value["settings"]
61
+ system = OpticalSystem.from_dict(settings["system"])
62
+ beam = GaussianBeam.from_dict(settings["beam"])
63
+ profile = system.gaussian(beam)
64
+ profile.z_mm, profile.radius_mm, profile.q_mm
65
+ ```
66
+
67
+ `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.
68
+
69
+ ### Settings and measurements
70
+
71
+ - `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.
72
+ - `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.
73
+ - `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.
74
+ - `widget.to_dict()` / `Bench.from_dict(settings)`: detached JSON-compatible experiment settings, including `wave = {"grid_size": ..., "window_mm": ...}`.
75
+ - Assign a **new complete** `widget.settings` dictionary for programmatic updates; nested in-place edits do not trigger trait notifications.
76
+ - Invalid numerical edits retain the last valid calculation and expose `error`.
77
+ - View selection is local to each display and is not part of the saved optical experiment.
78
+ - Sliders preview locally while scrubbing and recalculate on release; number inputs support values outside the default slider ranges.
79
+
80
+ ### Apertures and linked cross sections
81
+
82
+ `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.
83
+
84
+ `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.
85
+
86
+ Create the source and linked display in the **same marimo cell**, then read values only downstream:
87
+
88
+ ```python
89
+ from owl import Aperture, CrossSection
90
+
91
+ source = Bench(
92
+ OpticalSystem((Aperture(0.3), FreeSpace(100))),
93
+ beam=GaussianBeam(0.15, 633),
94
+ grid_size=256,
95
+ window_mm=2,
96
+ )
97
+ source_view = mo.ui.anywidget(source)
98
+ section_view = mo.ui.anywidget(CrossSection(source))
99
+ mo.vstack([source_view, section_view])
100
+ ```
101
+
102
+ `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.
103
+
104
+ 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.
105
+
106
+ 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.
107
+
108
+ ## Aperture study widgets
109
+
110
+ 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.
111
+
112
+ ```python
113
+ from owl import ApertureFocusStudy, ApertureStudySpec, RayWaveStudy
114
+
115
+ focus = mo.ui.anywidget(ApertureFocusStudy(ApertureStudySpec(
116
+ grid_size=256,
117
+ window_mm=8,
118
+ ray_count=100_000,
119
+ )))
120
+ focus
121
+
122
+ comparison = mo.ui.anywidget(RayWaveStudy(ApertureStudySpec()))
123
+ comparison
124
+ ```
125
+
126
+ `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.
127
+
128
+ ```python
129
+ from owl import PupilMapStudy, StopRayStudy, StopStudySpec
130
+
131
+ stops = mo.ui.anywidget(StopRayStudy(StopStudySpec()))
132
+ stops
133
+
134
+ pupils = mo.ui.anywidget(PupilMapStudy(StopStudySpec()))
135
+ pupils
136
+ ```
137
+
138
+ 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.
139
+
140
+ ## Numerical interface
141
+
142
+ ```python
143
+ import numpy as np
144
+ from owl import FreeSpace, GaussianBeam, OpticalSystem, ThinLens
145
+
146
+ system = OpticalSystem((FreeSpace(100), ThinLens(50), FreeSpace(100)))
147
+ rays = system.trace(np.array([[0.1, -0.1], [0.0, 0.0]]))
148
+ profile = system.gaussian(GaussianBeam(0.25, 1000))
149
+ matrix = system.matrix()
150
+ ```
151
+
152
+ - Lengths: **mm**; source wavelength: **nm**; ray angles: **rad**.
153
+ - Rays use `(height, angle)` column vectors; matrix composition follows element encounter order.
154
+ - `RayTrace.rays` has shape `(samples, 2, rays)`; samples retain the input and each element exit, expanding thick lenses into entry/gap/exit.
155
+ - 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.
156
+ - `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.
157
+ - Gaussian input: waist at `z=0`, in air, `M²=1`; `q=z+i z_R`. Radius is the **1/e² intensity radius**.
158
+ - `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`.
159
+ - 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.
160
+ - 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.
161
+
162
+ ### Surfaces and return paths
163
+
164
+ 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.
165
+
166
+ 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.
167
+
168
+ `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.
169
+
170
+ ## Develop and validate
171
+
172
+ ```sh
173
+ uv run --no-project --refresh-package owl-widgets --with . --with pytest pytest tests
174
+ uvx --from marimo==0.24.0 marimo check --strict notebooks/optics_basics.py notebooks/apertures.py notebooks/keplerian_telescope.py
175
+
176
+ uvx --from marimo==0.24.0 marimo export html --sandbox notebooks/optics_basics.py -o /tmp/owl-optics-basics.html
177
+ uvx --from marimo==0.24.0 marimo export html --sandbox notebooks/apertures.py -o /tmp/owl-apertures.html
178
+
179
+ uv build
180
+ ```
181
+
182
+ Browser renderer tests (Node.js; first run downloads Chromium):
183
+
184
+ ```sh
185
+ npm exec --yes --package=@playwright/test -- playwright install chromium
186
+ npm exec --yes --package=@playwright/test -- node --test tests/test_frontend.mjs
187
+ npm exec --yes --package=@playwright/test -- node --test tests/test_studies_frontend.mjs
188
+ ```
189
+
190
+ 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.
191
+
192
+ ## Publication
193
+
194
+ Distribution name: `owl-widgets`; Python import: `owl`; working version: `0.1.0`. Licensed under the MIT License; see `LICENSE`.
195
+
196
+ **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.
@@ -0,0 +1,181 @@
1
+ # OWL — Optics Widget Library
2
+
3
+ Small, inspectable optics experiments in Python notebooks. Built on anywidget; inspired by [wigglystuff](https://github.com/koaning/wigglystuff)'s notebook-first, composable widgets. No wigglystuff dependency or copied implementation.
4
+
5
+ **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.
6
+
7
+ ## Run the notebook
8
+
9
+ From this checkout's `optics` directory:
10
+
11
+ ```sh
12
+ uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/optics_basics.py
13
+ uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/apertures.py
14
+ uvx --from marimo==0.24.0 marimo edit --sandbox notebooks/keplerian_telescope.py
15
+ ```
16
+
17
+ 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.
18
+
19
+ 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.
20
+
21
+ - `optics_basics.py`: free propagation, thin lenses, ray focus versus Gaussian waist, wavelength tuning, mirrors, interfaces, and thick lenses.
22
+ - `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.
23
+ - `keplerian_telescope.py`: contrasts a two-positive-lens ray-optical afocal spacing ($f_{obj} + f_{eye}$) with the spacing that minimizes Gaussian-beam divergence.
24
+
25
+ Each experiment owns independent inputs. Study controls update their plots and the notebook's downstream tables and narrative together.
26
+
27
+ ## Compose a bench
28
+
29
+ ```python
30
+ import marimo as mo
31
+ from owl import Bench, FreeSpace, GaussianBeam, OpticalSystem, ThinLens
32
+
33
+ widget = Bench(
34
+ OpticalSystem((FreeSpace(60), ThinLens(50), FreeSpace(150))),
35
+ beam=GaussianBeam(waist_mm=0.07, wavelength_nm=633),
36
+ ray_span_mm=0.1,
37
+ )
38
+ bench = mo.ui.anywidget(widget)
39
+ bench
40
+ ```
41
+
42
+ In a **separate downstream marimo cell**, read `bench.value` to react to edits:
43
+
44
+ ```python
45
+ settings = bench.value["settings"]
46
+ system = OpticalSystem.from_dict(settings["system"])
47
+ beam = GaussianBeam.from_dict(settings["beam"])
48
+ profile = system.gaussian(beam)
49
+ profile.z_mm, profile.radius_mm, profile.q_mm
50
+ ```
51
+
52
+ `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.
53
+
54
+ ### Settings and measurements
55
+
56
+ - `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.
57
+ - `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.
58
+ - `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.
59
+ - `widget.to_dict()` / `Bench.from_dict(settings)`: detached JSON-compatible experiment settings, including `wave = {"grid_size": ..., "window_mm": ...}`.
60
+ - Assign a **new complete** `widget.settings` dictionary for programmatic updates; nested in-place edits do not trigger trait notifications.
61
+ - Invalid numerical edits retain the last valid calculation and expose `error`.
62
+ - View selection is local to each display and is not part of the saved optical experiment.
63
+ - Sliders preview locally while scrubbing and recalculate on release; number inputs support values outside the default slider ranges.
64
+
65
+ ### Apertures and linked cross sections
66
+
67
+ `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.
68
+
69
+ `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.
70
+
71
+ Create the source and linked display in the **same marimo cell**, then read values only downstream:
72
+
73
+ ```python
74
+ from owl import Aperture, CrossSection
75
+
76
+ source = Bench(
77
+ OpticalSystem((Aperture(0.3), FreeSpace(100))),
78
+ beam=GaussianBeam(0.15, 633),
79
+ grid_size=256,
80
+ window_mm=2,
81
+ )
82
+ source_view = mo.ui.anywidget(source)
83
+ section_view = mo.ui.anywidget(CrossSection(source))
84
+ mo.vstack([source_view, section_view])
85
+ ```
86
+
87
+ `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.
88
+
89
+ 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.
90
+
91
+ 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.
92
+
93
+ ## Aperture study widgets
94
+
95
+ 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.
96
+
97
+ ```python
98
+ from owl import ApertureFocusStudy, ApertureStudySpec, RayWaveStudy
99
+
100
+ focus = mo.ui.anywidget(ApertureFocusStudy(ApertureStudySpec(
101
+ grid_size=256,
102
+ window_mm=8,
103
+ ray_count=100_000,
104
+ )))
105
+ focus
106
+
107
+ comparison = mo.ui.anywidget(RayWaveStudy(ApertureStudySpec()))
108
+ comparison
109
+ ```
110
+
111
+ `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.
112
+
113
+ ```python
114
+ from owl import PupilMapStudy, StopRayStudy, StopStudySpec
115
+
116
+ stops = mo.ui.anywidget(StopRayStudy(StopStudySpec()))
117
+ stops
118
+
119
+ pupils = mo.ui.anywidget(PupilMapStudy(StopStudySpec()))
120
+ pupils
121
+ ```
122
+
123
+ 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.
124
+
125
+ ## Numerical interface
126
+
127
+ ```python
128
+ import numpy as np
129
+ from owl import FreeSpace, GaussianBeam, OpticalSystem, ThinLens
130
+
131
+ system = OpticalSystem((FreeSpace(100), ThinLens(50), FreeSpace(100)))
132
+ rays = system.trace(np.array([[0.1, -0.1], [0.0, 0.0]]))
133
+ profile = system.gaussian(GaussianBeam(0.25, 1000))
134
+ matrix = system.matrix()
135
+ ```
136
+
137
+ - Lengths: **mm**; source wavelength: **nm**; ray angles: **rad**.
138
+ - Rays use `(height, angle)` column vectors; matrix composition follows element encounter order.
139
+ - `RayTrace.rays` has shape `(samples, 2, rays)`; samples retain the input and each element exit, expanding thick lenses into entry/gap/exit.
140
+ - 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.
141
+ - `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.
142
+ - Gaussian input: waist at `z=0`, in air, `M²=1`; `q=z+i z_R`. Radius is the **1/e² intensity radius**.
143
+ - `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`.
144
+ - 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.
145
+ - 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.
146
+
147
+ ### Surfaces and return paths
148
+
149
+ 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.
150
+
151
+ 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.
152
+
153
+ `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.
154
+
155
+ ## Develop and validate
156
+
157
+ ```sh
158
+ uv run --no-project --refresh-package owl-widgets --with . --with pytest pytest tests
159
+ uvx --from marimo==0.24.0 marimo check --strict notebooks/optics_basics.py notebooks/apertures.py notebooks/keplerian_telescope.py
160
+
161
+ uvx --from marimo==0.24.0 marimo export html --sandbox notebooks/optics_basics.py -o /tmp/owl-optics-basics.html
162
+ uvx --from marimo==0.24.0 marimo export html --sandbox notebooks/apertures.py -o /tmp/owl-apertures.html
163
+
164
+ uv build
165
+ ```
166
+
167
+ Browser renderer tests (Node.js; first run downloads Chromium):
168
+
169
+ ```sh
170
+ npm exec --yes --package=@playwright/test -- playwright install chromium
171
+ npm exec --yes --package=@playwright/test -- node --test tests/test_frontend.mjs
172
+ npm exec --yes --package=@playwright/test -- node --test tests/test_studies_frontend.mjs
173
+ ```
174
+
175
+ 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.
176
+
177
+ ## Publication
178
+
179
+ Distribution name: `owl-widgets`; Python import: `owl`; working version: `0.1.0`. Licensed under the MIT License; see `LICENSE`.
180
+
181
+ **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.
@@ -0,0 +1,11 @@
1
+ # Third-party notices
2
+
3
+ ## Plotly.js
4
+
5
+ OWL bundles Plotly.js v4.1.1 (`src/owl/static/plotly.min.js`) for offline interactive plotting. Plotly.js is distributed under the MIT License. Copyright 2012–2026 Plotly, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.