From cacb3c57606cb1a6cc95f79276c1ca6257176e4c Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 17 Mar 2026 19:31:45 -0600 Subject: [PATCH 01/10] Added `SequentialSystem.spot_diagram()` method to plot spot diagrams for each field angle. --- .github/workflows/notebooks.yml | 15 ++ docs/tutorials/prime_focus.ipynb | 242 +++++++++---------------------- optika/_tests/test_systems.py | 9 ++ optika/systems.py | 162 ++++++++++++++++++--- 4 files changed, 229 insertions(+), 199 deletions(-) create mode 100644 .github/workflows/notebooks.yml diff --git a/.github/workflows/notebooks.yml b/.github/workflows/notebooks.yml new file mode 100644 index 00000000..29f1fec2 --- /dev/null +++ b/.github/workflows/notebooks.yml @@ -0,0 +1,15 @@ +name: Notebooks + +on: + push: + branches: + - main + pull_request: + +jobs: + nbstripout: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pip install nbstripout + - run: find . -name "*.ipynb" -exec nbstripout --verify {} + diff --git a/docs/tutorials/prime_focus.ipynb b/docs/tutorials/prime_focus.ipynb index d7ddf3e5..70d3839d 100644 --- a/docs/tutorials/prime_focus.ipynb +++ b/docs/tutorials/prime_focus.ipynb @@ -20,17 +20,17 @@ }, { "cell_type": "code", - "execution_count": null, "id": "cf080c2d", "metadata": {}, - "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import astropy.units as u\n", "import astropy.visualization\n", "import named_arrays as na\n", "import optika" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -47,18 +47,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "1c197b7fb02addaa", - "metadata": { - "ExecuteTime": { - "end_time": "2024-10-09T23:05:37.392486Z", - "start_time": "2024-10-09T23:05:36.986489Z" - } - }, - "outputs": [], + "metadata": {}, "source": [ "radius_aperture = 100 * u.mm" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -72,13 +67,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "0aea0035", "metadata": {}, - "outputs": [], "source": [ "f_number = 5" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -90,19 +85,14 @@ }, { "cell_type": "code", - "execution_count": null, "id": "ad022a0b65576089", - "metadata": { - "ExecuteTime": { - "end_time": "2024-10-09T23:05:42.415570Z", - "start_time": "2024-10-09T23:05:42.409067Z" - } - }, - "outputs": [], + "metadata": {}, "source": [ "focal_length = f_number * radius_aperture\n", "focal_length" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -116,13 +106,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "initial_id", "metadata": {}, - "outputs": [], "source": [ "sag_primary = optika.sags.ParabolicSag(-focal_length)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -137,13 +127,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "7b7de780", "metadata": {}, - "outputs": [], "source": [ "material_primary = optika.materials.Mirror()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -157,13 +147,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "e0ba7c6f", "metadata": {}, - "outputs": [], "source": [ "aperture_primary = optika.apertures.CircularAperture(radius_aperture)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -179,13 +169,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "abcac277", "metadata": {}, - "outputs": [], "source": [ "translation_primary = na.transformations.Cartesian3dTranslation(z=focal_length)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -200,10 +190,8 @@ }, { "cell_type": "code", - "execution_count": null, "id": "35571848", "metadata": {}, - "outputs": [], "source": [ "primary = optika.surfaces.Surface(\n", " name=\"primary\",\n", @@ -213,7 +201,9 @@ " transformation=translation_primary,\n", " is_pupil_stop=True,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -241,13 +231,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "9f1037f0", "metadata": {}, - "outputs": [], "source": [ "width_pixel = 15 * u.um" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -261,13 +251,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "cb45e493", "metadata": {}, - "outputs": [], "source": [ "num_pixel = na.Cartesian2dVectorArray(256, 256)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -281,13 +271,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "c88e3a2d", "metadata": {}, - "outputs": [], "source": [ "axis_pixel = na.Cartesian2dVectorArray(\"detector_x\", \"detector_y\")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -302,13 +292,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "26a421bf", "metadata": {}, - "outputs": [], "source": [ "rotation_sensor = na.transformations.Cartesian3dRotationY(180 * u.deg)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -322,10 +312,8 @@ }, { "cell_type": "code", - "execution_count": null, "id": "7dc66809", "metadata": {}, - "outputs": [], "source": [ "sensor = optika.sensors.ImagingSensor(\n", " name=\"sensor\",\n", @@ -335,7 +323,9 @@ " transformation=rotation_sensor,\n", " is_field_stop=True,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -357,10 +347,8 @@ }, { "cell_type": "code", - "execution_count": null, "id": "8b028be7", "metadata": {}, - "outputs": [], "source": [ "pupil = na.Cartesian2dVectorLinearSpace(\n", " start=-1,\n", @@ -369,7 +357,9 @@ " num=5,\n", " centers=True,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -383,10 +373,8 @@ }, { "cell_type": "code", - "execution_count": null, "id": "4900bd5f", "metadata": {}, - "outputs": [], "source": [ "field = na.Cartesian2dVectorLinearSpace(\n", " start=-1,\n", @@ -395,7 +383,9 @@ " num=5,\n", " centers=True,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -409,13 +399,13 @@ }, { "cell_type": "code", - "execution_count": null, "id": "553741ef", "metadata": {}, - "outputs": [], "source": [ "wavelength = 500 * u.nm" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -430,17 +420,17 @@ }, { "cell_type": "code", - "execution_count": null, "id": "2823c560", "metadata": {}, - "outputs": [], "source": [ "grid_input = optika.vectors.ObjectVectorArray(\n", " wavelength=wavelength,\n", " field=field,\n", " pupil=pupil,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -458,10 +448,8 @@ }, { "cell_type": "code", - "execution_count": null, "id": "2e9d5a7d", "metadata": {}, - "outputs": [], "source": [ "system = optika.systems.SequentialSystem(\n", " surfaces=[\n", @@ -470,7 +458,9 @@ " sensor=sensor,\n", " grid_input=grid_input,\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -484,12 +474,10 @@ }, { "cell_type": "code", - "execution_count": null, "id": "ee191ea4", "metadata": { "scrolled": false }, - "outputs": [], "source": [ "# plot the system\n", "with astropy.visualization.quantity_support():\n", @@ -504,7 +492,9 @@ " color=\"black\",\n", " zorder=10,\n", " )" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "raw", @@ -516,117 +506,17 @@ "Spot Diagrams\n", "-------------\n", "\n", - "To plot a diagram of the different spots,\n", - "we need to load the final position of the rays from the simulation\n", - "and plot them using :mod:`matplotlib`.\n", - "\n", - "We can access the final position of the rays on the sensor using the\n", - ":attr:`optika.systems.SequentialSystem.rayfunction_default` attribute.\n", - "`rayfunction_default` is an instance of a :class:`named_arrays.FunctionArray`,\n", - "an array that is a composition two other arrays: `input` and `output`." + "We can plot a spot diagram for each field angle using the\n", + ":meth:`~optika.systems.SequentialSystem.spot_diagram` method." ] }, { "cell_type": "code", - "execution_count": null, "id": "92a20217", "metadata": {}, + "source": "fig, ax = system.spot_diagram()", "outputs": [], - "source": [ - "position = system.rayfunction_default.outputs.position.to(u.um)" - ] - }, - { - "cell_type": "raw", - "id": "5f7ad4fc", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "It can be helpful to subtract off the centroid of each PSF to compare them" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9b173d56", - "metadata": {}, - "outputs": [], - "source": [ - "position_relative = position - position.mean(pupil.axes)" - ] - }, - { - "cell_type": "raw", - "id": "b50316e2", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "To easily plot an array of PSFs for each field position,\n", - "we can use :func:`named_arrays.plt.scatter`,\n", - "which allows broadcasting over arrays of \n", - ":class:`matplotlib.axes.Axes` instances." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1f6b2547", - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "with astropy.visualization.quantity_support():\n", - " fig, ax = na.plt.subplots(\n", - " axis_rows=field.axis.y,\n", - " axis_cols=field.axis.x,\n", - " nrows=field.num,\n", - " ncols=field.num,\n", - " sharex=True,\n", - " sharey=True,\n", - " figsize=(6, 6),\n", - " constrained_layout=True,\n", - " )\n", - " na.plt.scatter(\n", - " position_relative.x,\n", - " position_relative.y,\n", - " ax=ax,\n", - " s=5,\n", - " )\n", - " \n", - " ax_lower = ax[{field.axis.y: +0}]\n", - " ax_upper = ax[{field.axis.y: ~0}]\n", - " ax_left = ax[{field.axis.x: +0}]\n", - " ax_right = ax[{field.axis.x: ~0}]\n", - " \n", - " na.plt.set_aspect(\"equal\", ax=ax)\n", - " na.plt.set_xlabel(f\"$x$ ({position.x.unit:latex_inline})\", ax=ax_lower)\n", - " na.plt.set_ylabel(f\"$y$ ({position.y.unit:latex_inline})\", ax=ax_left)\n", - " \n", - " angle = system.rayfunction_default.inputs.field.to(u.arcmin)\n", - " angle_x = angle.x.mean(set(angle.axes) - {field.axis.x,})\n", - " angle_y = angle.y.mean(set(angle.axes) - {field.axis.y,})\n", - " na.plt.text(\n", - " x=0.5,\n", - " y=1,\n", - " s=angle_x.to_string_array(),\n", - " ax=ax_upper,\n", - " transform=na.plt.transAxes(ax_upper),\n", - " ha=\"center\",\n", - " va=\"bottom\",\n", - " )\n", - " na.plt.text(\n", - " x=1.05,\n", - " y=0.5,\n", - " s=angle_y.to_string_array(),\n", - " ax=ax_right,\n", - " transform=na.plt.transAxes(ax_right),\n", - " ha=\"left\",\n", - " va=\"center\",\n", - " )" - ] + "execution_count": null } ], "metadata": { diff --git a/optika/_tests/test_systems.py b/optika/_tests/test_systems.py index 14093dc5..1b75052c 100644 --- a/optika/_tests/test_systems.py +++ b/optika/_tests/test_systems.py @@ -1,3 +1,4 @@ +import matplotlib.pyplot as plt import pytest import numpy as np import astropy.units as u @@ -228,6 +229,14 @@ def test_rayfunction_default(self, a: optika.systems.AbstractSequentialSystem): assert isinstance(rayfunction.outputs, optika.rays.RayVectorArray) assert a.axis_surface not in rayfunction.shape + def test_spot_diagram(self, a: optika.systems.AbstractSequentialSystem): + fig, axs = a.spot_diagram() + assert isinstance(fig, plt.Figure) + + for ax in axs.ndarray.flat: + assert isinstance(ax, plt.Axes) + assert ax.has_data() + _objects = [ None, diff --git a/optika/systems.py b/optika/systems.py index a5511152..1f5281de 100644 --- a/optika/systems.py +++ b/optika/systems.py @@ -8,9 +8,12 @@ import dataclasses import functools import astropy.units as u +import astropy.visualization import numpy as np import numpy.typing as npt import matplotlib.axes +import matplotlib.cm +import matplotlib.pyplot as plt import named_arrays as na import optika @@ -100,7 +103,7 @@ def object_is_at_infinity(self) -> bool: @abc.abstractmethod def surfaces(self) -> Sequence[optika.surfaces.AbstractSurface]: """ - a sequence of surfaces representing this optical system. + A sequence of surfaces representing this optical system. At least one of these surfaces needs to be marked as the pupil surface, and if the object surface is not marked as the field stop, one of these @@ -511,42 +514,22 @@ def _denormalize_grid( object_is_at_infinity = self.object_is_at_infinity if object_is_at_infinity: - field = rayfunction_stops.outputs.direction.xy + field = optika.angles(rayfunction_stops.outputs.direction) pupil = rayfunction_stops.outputs.position.xy else: field = rayfunction_stops.outputs.position.xy - pupil = rayfunction_stops.outputs.direction.xy + pupil = optika.angles(rayfunction_stops.outputs.direction) if normalized_field: min_field = field.min(axis=(axis_field, axis_pupil)) ptp_field = field.ptp(axis=(axis_field, axis_pupil)) result.field = ptp_field * (result.field + 1) / 2 + min_field - if object_is_at_infinity: - direction = na.Cartesian3dVectorArray( - x=result.field.x, - y=result.field.y, - z=np.sqrt( - 1 - np.square(result.field.x) - np.square(result.field.y) - ), - ) - result.field = optika.angles(direction) - if normalized_pupil: min_pupil = pupil.min(axis=(axis_field, axis_pupil)) ptp_pupil = pupil.ptp(axis=(axis_field, axis_pupil)) result.pupil = ptp_pupil * (result.pupil + 1) / 2 + min_pupil - if not object_is_at_infinity: - direction = na.Cartesian3dVectorArray( - x=result.pupil.x, - y=result.pupil.y, - z=np.sqrt( - 1 - np.square(result.pupil.x) - np.square(result.pupil.y) - ), - ) - result.pupil = optika.angles(direction) - return result def _calc_rayfunction_input( @@ -1034,6 +1017,139 @@ def plot( return result + def spot_diagram( + self, + figsize: tuple[float, float] = (8, 6), + s: float = 5, + cmap: None | str | plt.Colormap = None, + ) -> tuple[ + plt.Figure, + plt.Axes, + ]: + """ + Create a spot diagram of the rays at each field point to inspect + the performance of this optical system. + + Parameters + ---------- + figsize + The size of the returned figure in inches. + s + The marker size in points squared. + cmap + The colormap used to map scalar data to colors. + """ + shape = self.shape + + grid = self.grid_input + + wavelength = na.as_named_array(grid.wavelength) + field = grid.field + pupil = grid.pupil + + axis_wavelength = set(wavelength.shape) - set(shape) + axis_wavelength = tuple(axis_wavelength) + if len(axis_wavelength) > 1: # pragma: nocover + raise ValueError( + f"Expected one or zero wavelength axes, got {len(axis_wavelength)}." + ) + + axis_field_x = tuple(field.x.shape) + if len(axis_field_x) != 1: # pragma: nocover + raise ValueError( + "The horizontal field array must be one-dimensional, " + f"got {self.grid_input.field.x.shape=}." + ) + axis_field_x = axis_field_x[0] + + axis_field_y = tuple(field.y.shape) + if len(axis_field_y) != 1: # pragma: nocover + raise ValueError( + "The vertical field array must be one-dimensional, " + f"got {self.grid_input.field.y.shape=}." + ) + axis_field_y = axis_field_y[0] + + axis_field = (axis_field_x, axis_field_y) + + axis_pupil = set(pupil.shape) - set(shape) + axis_pupil = axis_pupil - set(axis_wavelength) - set(axis_field) + + rays = self.rayfunction_default.outputs + position = rays.position.to(u.um) + position_relative = position - position.mean(axis_pupil) + + with astropy.visualization.quantity_support(): + fig, ax = na.plt.subplots( + axis_rows=axis_field_y, + axis_cols=axis_field_x, + nrows=field.y.shape[axis_field_y], + ncols=field.x.shape[axis_field_x], + sharex=True, + sharey=True, + figsize=figsize, + constrained_layout=True, + origin="lower", + ) + + colorizer = plt.Colorizer( + cmap=cmap, + norm=plt.Normalize( + vmin=wavelength.ndarray.value.min(), + vmax=wavelength.ndarray.value.max(), + ), + ) + + na.plt.scatter( + position_relative.x, + position_relative.y, + ax=ax, + s=s, + where=rays.unvignetted, + c=self.grid_input.wavelength.value, + colorizer=colorizer, + ) + + ax_lower = ax[{axis_field_y: +0}] + ax_upper = ax[{axis_field_y: ~0}] + ax_left = ax[{axis_field_x: +0}] + ax_right = ax[{axis_field_x: ~0}] + + na.plt.set_xlabel(f"$x$ ({position.x.unit:latex_inline})", ax=ax_lower) + na.plt.set_ylabel(f"$y$ ({position.y.unit:latex_inline})", ax=ax_left) + + angle = self.rayfunction_default.inputs.field + + angle_x = angle.x + angle_y = angle.y + + na.plt.text( + x=0.5, + y=1, + s=angle_x.to_string_array(), + ax=ax_upper, + transform=na.plt.transAxes(ax_upper), + ha="center", + va="bottom", + ) + na.plt.text( + x=1.05, + y=0.5, + s=angle_y.to_string_array(), + ax=ax_right, + transform=na.plt.transAxes(ax_right), + ha="left", + va="center", + ) + + plt.colorbar( + mappable=matplotlib.cm.ScalarMappable(colorizer=colorizer), + ax=ax.ndarray, + label=f"wavelength ({wavelength.unit:latex_inline})" + ) + + return fig, ax + @dataclasses.dataclass(eq=False, repr=False) class SequentialSystem( From f5d5b3d4cbc0730e3263f588e235612f53f21925 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 17 Mar 2026 19:35:21 -0600 Subject: [PATCH 02/10] black --- optika/systems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optika/systems.py b/optika/systems.py index 1f5281de..06bf79af 100644 --- a/optika/systems.py +++ b/optika/systems.py @@ -1145,7 +1145,7 @@ def spot_diagram( plt.colorbar( mappable=matplotlib.cm.ScalarMappable(colorizer=colorizer), ax=ax.ndarray, - label=f"wavelength ({wavelength.unit:latex_inline})" + label=f"wavelength ({wavelength.unit:latex_inline})", ) return fig, ax From b29167fd7ca8c8ec18344af33d6a6400e1fcbad7 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 17 Mar 2026 19:39:38 -0600 Subject: [PATCH 03/10] clear output --- docs/tutorials/prime_focus.ipynb | 132 +++++++++++++++---------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/docs/tutorials/prime_focus.ipynb b/docs/tutorials/prime_focus.ipynb index 70d3839d..9405bc48 100644 --- a/docs/tutorials/prime_focus.ipynb +++ b/docs/tutorials/prime_focus.ipynb @@ -20,17 +20,17 @@ }, { "cell_type": "code", + "execution_count": null, "id": "cf080c2d", "metadata": {}, + "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import astropy.units as u\n", "import astropy.visualization\n", "import named_arrays as na\n", "import optika" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -47,13 +47,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "1c197b7fb02addaa", "metadata": {}, + "outputs": [], "source": [ "radius_aperture = 100 * u.mm" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -67,13 +67,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "0aea0035", "metadata": {}, + "outputs": [], "source": [ "f_number = 5" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -85,14 +85,14 @@ }, { "cell_type": "code", + "execution_count": null, "id": "ad022a0b65576089", "metadata": {}, + "outputs": [], "source": [ "focal_length = f_number * radius_aperture\n", "focal_length" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -106,13 +106,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "initial_id", "metadata": {}, + "outputs": [], "source": [ "sag_primary = optika.sags.ParabolicSag(-focal_length)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -127,13 +127,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "7b7de780", "metadata": {}, + "outputs": [], "source": [ "material_primary = optika.materials.Mirror()" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -147,13 +147,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "e0ba7c6f", "metadata": {}, + "outputs": [], "source": [ "aperture_primary = optika.apertures.CircularAperture(radius_aperture)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -169,13 +169,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "abcac277", "metadata": {}, + "outputs": [], "source": [ "translation_primary = na.transformations.Cartesian3dTranslation(z=focal_length)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -190,8 +190,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "35571848", "metadata": {}, + "outputs": [], "source": [ "primary = optika.surfaces.Surface(\n", " name=\"primary\",\n", @@ -201,9 +203,7 @@ " transformation=translation_primary,\n", " is_pupil_stop=True,\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -231,13 +231,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "9f1037f0", "metadata": {}, + "outputs": [], "source": [ "width_pixel = 15 * u.um" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -251,13 +251,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "cb45e493", "metadata": {}, + "outputs": [], "source": [ "num_pixel = na.Cartesian2dVectorArray(256, 256)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -271,13 +271,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "c88e3a2d", "metadata": {}, + "outputs": [], "source": [ "axis_pixel = na.Cartesian2dVectorArray(\"detector_x\", \"detector_y\")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -292,13 +292,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "26a421bf", "metadata": {}, + "outputs": [], "source": [ "rotation_sensor = na.transformations.Cartesian3dRotationY(180 * u.deg)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -312,8 +312,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "7dc66809", "metadata": {}, + "outputs": [], "source": [ "sensor = optika.sensors.ImagingSensor(\n", " name=\"sensor\",\n", @@ -323,9 +325,7 @@ " transformation=rotation_sensor,\n", " is_field_stop=True,\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -347,8 +347,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "8b028be7", "metadata": {}, + "outputs": [], "source": [ "pupil = na.Cartesian2dVectorLinearSpace(\n", " start=-1,\n", @@ -357,9 +359,7 @@ " num=5,\n", " centers=True,\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -373,8 +373,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "4900bd5f", "metadata": {}, + "outputs": [], "source": [ "field = na.Cartesian2dVectorLinearSpace(\n", " start=-1,\n", @@ -383,9 +385,7 @@ " num=5,\n", " centers=True,\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -399,13 +399,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "553741ef", "metadata": {}, + "outputs": [], "source": [ "wavelength = 500 * u.nm" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -420,17 +420,17 @@ }, { "cell_type": "code", + "execution_count": null, "id": "2823c560", "metadata": {}, + "outputs": [], "source": [ "grid_input = optika.vectors.ObjectVectorArray(\n", " wavelength=wavelength,\n", " field=field,\n", " pupil=pupil,\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -448,8 +448,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "2e9d5a7d", "metadata": {}, + "outputs": [], "source": [ "system = optika.systems.SequentialSystem(\n", " surfaces=[\n", @@ -458,9 +460,7 @@ " sensor=sensor,\n", " grid_input=grid_input,\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -474,10 +474,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "ee191ea4", - "metadata": { - "scrolled": false - }, + "metadata": {}, + "outputs": [], "source": [ "# plot the system\n", "with astropy.visualization.quantity_support():\n", @@ -492,9 +492,7 @@ " color=\"black\",\n", " zorder=10,\n", " )" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "raw", @@ -512,11 +510,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "92a20217", "metadata": {}, - "source": "fig, ax = system.spot_diagram()", "outputs": [], - "execution_count": null + "source": [ + "fig, ax = system.spot_diagram()" + ] } ], "metadata": { @@ -536,7 +536,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.13.3" } }, "nbformat": 4, From e517c8e6bbae8903fe305b56554475584d99f8cd Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 17 Mar 2026 19:42:00 -0600 Subject: [PATCH 04/10] bugfix --- optika/rulings/_rulings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optika/rulings/_rulings.py b/optika/rulings/_rulings.py index 52bbed0c..8190e903 100644 --- a/optika/rulings/_rulings.py +++ b/optika/rulings/_rulings.py @@ -1066,8 +1066,8 @@ def efficiency( result = np.where( i == 0, - x=1 - ((2 * a / np.pi) - np.square(a / np.pi)) * b, - y=(2 / np.square(i * np.pi)) * (1 - np.cos(i * a * u.rad)) * b, + 1 - ((2 * a / np.pi) - np.square(a / np.pi)) * b, + (2 / np.square(i * np.pi)) * (1 - np.cos(i * a * u.rad)) * b, ) return result From 49b8fe99908436dee41019c52aeb010092bd6373 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Thu, 19 Mar 2026 13:23:48 -0600 Subject: [PATCH 05/10] black --- optika/materials/matrices.py | 1 - 1 file changed, 1 deletion(-) diff --git a/optika/materials/matrices.py b/optika/materials/matrices.py index 090e96c7..c7b65b4d 100644 --- a/optika/materials/matrices.py +++ b/optika/materials/matrices.py @@ -10,7 +10,6 @@ import optika from . import snells_law - __all__ = [ "refraction", "propagation", From 4f8282a329720c7f05ba5a00a8b7a1d791a91871 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Thu, 19 Mar 2026 13:26:01 -0600 Subject: [PATCH 06/10] black --- optika/_tests/test_mixins.py | 1 - optika/_tests/test_surfaces.py | 1 - optika/apertures/_apertures_test.py | 1 - optika/chemicals/_tests/test_chemicals.py | 1 - 4 files changed, 4 deletions(-) diff --git a/optika/_tests/test_mixins.py b/optika/_tests/test_mixins.py index 1c0dfd12..b7fcf344 100644 --- a/optika/_tests/test_mixins.py +++ b/optika/_tests/test_mixins.py @@ -8,7 +8,6 @@ import named_arrays as na import optika - transformation_parameterization = [ None, na.transformations.Cartesian3dTranslation(x=5 * u.mm), diff --git a/optika/_tests/test_surfaces.py b/optika/_tests/test_surfaces.py index 076c353f..6945e1ea 100644 --- a/optika/_tests/test_surfaces.py +++ b/optika/_tests/test_surfaces.py @@ -6,7 +6,6 @@ from . import test_mixins from . import test_propagators - surfaces = [ optika.surfaces.Surface(), optika.surfaces.Surface( diff --git a/optika/apertures/_apertures_test.py b/optika/apertures/_apertures_test.py index 45b329f2..d410993b 100644 --- a/optika/apertures/_apertures_test.py +++ b/optika/apertures/_apertures_test.py @@ -7,7 +7,6 @@ import optika.rays._tests.test_ray_vectors from .._tests import test_mixins - active_parameterization = [ True, False, diff --git a/optika/chemicals/_tests/test_chemicals.py b/optika/chemicals/_tests/test_chemicals.py index 80cf50e3..f20c2d6a 100644 --- a/optika/chemicals/_tests/test_chemicals.py +++ b/optika/chemicals/_tests/test_chemicals.py @@ -7,7 +7,6 @@ import optika from optika._tests import test_mixins - _wavelength = [ 500 * u.nm, na.geomspace(10, 10000, axis="wavelength", num=101) * u.AA, From b1d38e67c6a5f4e46ac9bd6edb6eb1ee9e81b6fd Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Thu, 19 Mar 2026 13:29:40 -0600 Subject: [PATCH 07/10] more fixes to where --- optika/rulings/_rulings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/optika/rulings/_rulings.py b/optika/rulings/_rulings.py index 8190e903..bfc1df99 100644 --- a/optika/rulings/_rulings.py +++ b/optika/rulings/_rulings.py @@ -602,13 +602,13 @@ def efficiency( result = np.where( i % 2 == 0, - x=0, - y=np.square(2 * np.sin(np.pi * gamma / 2 * u.rad) / (i * np.pi)), + 0, + np.square(2 * np.sin(np.pi * gamma / 2 * u.rad) / (i * np.pi)), ) result = np.where( i == 0, - x=np.square(np.cos(np.pi * gamma / 2 * u.rad)), - y=result, + np.square(np.cos(np.pi * gamma / 2 * u.rad)), + result, ) return result @@ -904,8 +904,8 @@ def efficiency( result = np.where( i % 2 == 0, - x=np.square(a * np.sin(np.square(np.pi) * gamma / 4 * u.rad)), - y=np.square(a * np.cos(np.square(np.pi) * gamma / 4 * u.rad)), + np.square(a * np.sin(np.square(np.pi) * gamma / 4 * u.rad)), + np.square(a * np.cos(np.square(np.pi) * gamma / 4 * u.rad)), ) return result From 386ce7afd25e3581f923776159958eb890559289 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 20 Mar 2026 11:00:45 -0600 Subject: [PATCH 08/10] where --- optika/sensors/materials/_materials.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optika/sensors/materials/_materials.py b/optika/sensors/materials/_materials.py index d5ca52bb..3e1d13ae 100644 --- a/optika/sensors/materials/_materials.py +++ b/optika/sensors/materials/_materials.py @@ -601,9 +601,9 @@ def _discrete_gamma( ) x = np.where( - condition=vmr != 0, - x=x, - y=mean, + vmr != 0, + x, + mean, ) unit_x = x.unit From 7fded4085b89ccebb49b4e9f43a19d05e1b298b3 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 20 Mar 2026 11:13:50 -0600 Subject: [PATCH 09/10] nbstripout --- docs/tutorials/prime_focus.ipynb | 86 ++++++++++++++++---------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/tutorials/prime_focus.ipynb b/docs/tutorials/prime_focus.ipynb index 9405bc48..5c918b7c 100644 --- a/docs/tutorials/prime_focus.ipynb +++ b/docs/tutorials/prime_focus.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "raw", - "id": "61a8a8fd", + "id": "0", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -21,7 +21,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cf080c2d", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -34,7 +34,7 @@ }, { "cell_type": "raw", - "id": "2eb334e0", + "id": "2", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -48,7 +48,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1c197b7fb02addaa", + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -57,7 +57,7 @@ }, { "cell_type": "raw", - "id": "6ba582b8", + "id": "4", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -68,7 +68,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0aea0035", + "id": "5", "metadata": {}, "outputs": [], "source": [ @@ -77,7 +77,7 @@ }, { "cell_type": "markdown", - "id": "b43d57178da3a0a", + "id": "6", "metadata": {}, "source": [ "So the focal length of the primary is then" @@ -86,7 +86,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ad022a0b65576089", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -96,7 +96,7 @@ }, { "cell_type": "raw", - "id": "dd2be92c6360815d", + "id": "8", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -107,7 +107,7 @@ { "cell_type": "code", "execution_count": null, - "id": "initial_id", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -116,7 +116,7 @@ }, { "cell_type": "raw", - "id": "5e1e69b4", + "id": "10", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -128,7 +128,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7b7de780", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -137,7 +137,7 @@ }, { "cell_type": "raw", - "id": "89a173b1", + "id": "12", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -148,7 +148,7 @@ { "cell_type": "code", "execution_count": null, - "id": "e0ba7c6f", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -157,7 +157,7 @@ }, { "cell_type": "raw", - "id": "bf796a46", + "id": "14", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -170,7 +170,7 @@ { "cell_type": "code", "execution_count": null, - "id": "abcac277", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -179,7 +179,7 @@ }, { "cell_type": "raw", - "id": "af968c6a", + "id": "16", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -191,7 +191,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35571848", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -207,7 +207,7 @@ }, { "cell_type": "raw", - "id": "0f592ceb", + "id": "18", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -218,7 +218,7 @@ }, { "cell_type": "raw", - "id": "63416de6", + "id": "19", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -232,7 +232,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9f1037f0", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -241,7 +241,7 @@ }, { "cell_type": "raw", - "id": "83997b47", + "id": "21", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -252,7 +252,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cb45e493", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -261,7 +261,7 @@ }, { "cell_type": "raw", - "id": "d2454e8a", + "id": "23", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -272,7 +272,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c88e3a2d", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -281,7 +281,7 @@ }, { "cell_type": "raw", - "id": "8e1d61c7", + "id": "25", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -293,7 +293,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26a421bf", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -302,7 +302,7 @@ }, { "cell_type": "raw", - "id": "ffc2686b", + "id": "27", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -313,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7dc66809", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -329,7 +329,7 @@ }, { "cell_type": "raw", - "id": "7f3fa93e", + "id": "29", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -348,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8b028be7", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -363,7 +363,7 @@ }, { "cell_type": "raw", - "id": "04d4b71c", + "id": "31", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -374,7 +374,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4900bd5f", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -389,7 +389,7 @@ }, { "cell_type": "raw", - "id": "3664820e", + "id": "33", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -400,7 +400,7 @@ { "cell_type": "code", "execution_count": null, - "id": "553741ef", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -409,7 +409,7 @@ }, { "cell_type": "raw", - "id": "d41cf5e7", + "id": "35", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -421,7 +421,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2823c560", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -434,7 +434,7 @@ }, { "cell_type": "raw", - "id": "c98aedeb", + "id": "37", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -449,7 +449,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2e9d5a7d", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -464,7 +464,7 @@ }, { "cell_type": "raw", - "id": "4c18b2b8", + "id": "39", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -475,7 +475,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ee191ea4", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -496,7 +496,7 @@ }, { "cell_type": "raw", - "id": "d8f00304", + "id": "41", "metadata": { "raw_mimetype": "text/restructuredtext" }, @@ -511,7 +511,7 @@ { "cell_type": "code", "execution_count": null, - "id": "92a20217", + "id": "42", "metadata": {}, "outputs": [], "source": [ From b5ee22fe4f9ca63ac652596049ff007cf8bb471b Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 20 Mar 2026 14:11:07 -0600 Subject: [PATCH 10/10] Add examples --- docs/_templates/function_custom.rst | 4 +++- docs/tutorials/prime_focus.ipynb | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/_templates/function_custom.rst b/docs/_templates/function_custom.rst index d9010b98..888395c1 100644 --- a/docs/_templates/function_custom.rst +++ b/docs/_templates/function_custom.rst @@ -2,4 +2,6 @@ .. currentmodule:: {{ module }} -.. autofunction:: {{ fullname }} \ No newline at end of file +.. autofunction:: {{ fullname }} + + .. autolink-examples:: {{ fullname }} \ No newline at end of file diff --git a/docs/tutorials/prime_focus.ipynb b/docs/tutorials/prime_focus.ipynb index 5c918b7c..f2f97e21 100644 --- a/docs/tutorials/prime_focus.ipynb +++ b/docs/tutorials/prime_focus.ipynb @@ -505,7 +505,8 @@ "-------------\n", "\n", "We can plot a spot diagram for each field angle using the\n", - ":meth:`~optika.systems.SequentialSystem.spot_diagram` method." + ":meth:`~optika.systems.SequentialSystem.spot_diagram` method of\n", + ":class:`~optika.systems.SequentialSystem`." ] }, {