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.
- owl_widgets-0.1.0/.gitignore +9 -0
- owl_widgets-0.1.0/LICENSE +21 -0
- owl_widgets-0.1.0/PKG-INFO +196 -0
- owl_widgets-0.1.0/README.md +181 -0
- owl_widgets-0.1.0/THIRD_PARTY_NOTICES.md +11 -0
- owl_widgets-0.1.0/notebooks/apertures.py +294 -0
- owl_widgets-0.1.0/notebooks/keplerian_telescope.py +291 -0
- owl_widgets-0.1.0/notebooks/optics_basics.py +226 -0
- owl_widgets-0.1.0/pyproject.toml +24 -0
- owl_widgets-0.1.0/src/owl/__init__.py +59 -0
- owl_widgets-0.1.0/src/owl/aperture_study.py +250 -0
- owl_widgets-0.1.0/src/owl/bench.py +248 -0
- owl_widgets-0.1.0/src/owl/core.py +530 -0
- owl_widgets-0.1.0/src/owl/cross_section.py +44 -0
- owl_widgets-0.1.0/src/owl/frontend.py +9 -0
- owl_widgets-0.1.0/src/owl/static/PLOTLY-LICENSE.txt +9 -0
- owl_widgets-0.1.0/src/owl/static/bench.css +313 -0
- owl_widgets-0.1.0/src/owl/static/bench.js +637 -0
- owl_widgets-0.1.0/src/owl/static/plotly.min.js +3620 -0
- owl_widgets-0.1.0/src/owl/static/plotly_views.js +299 -0
- owl_widgets-0.1.0/src/owl/static/studies.css +382 -0
- owl_widgets-0.1.0/src/owl/static/studies.js +1183 -0
- owl_widgets-0.1.0/src/owl/stop_study.py +303 -0
- owl_widgets-0.1.0/src/owl/studies.py +178 -0
- owl_widgets-0.1.0/src/owl/wave.py +250 -0
- owl_widgets-0.1.0/tests/test_aperture_core.py +294 -0
- owl_widgets-0.1.0/tests/test_aperture_study.py +140 -0
- owl_widgets-0.1.0/tests/test_aperture_widgets.py +103 -0
- owl_widgets-0.1.0/tests/test_bench.py +278 -0
- owl_widgets-0.1.0/tests/test_core.py +330 -0
- owl_widgets-0.1.0/tests/test_frontend.mjs +1325 -0
- owl_widgets-0.1.0/tests/test_stop_study.py +156 -0
- owl_widgets-0.1.0/tests/test_studies.py +246 -0
- owl_widgets-0.1.0/tests/test_studies_frontend.mjs +892 -0
|
@@ -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.
|