From 6ce65374544616f8685295889226b5baa55f06ce Mon Sep 17 00:00:00 2001 From: jacobdparker Date: Tue, 30 Jun 2026 16:04:44 -0600 Subject: [PATCH 1/2] Add `grid()` interior sampling to `RectangularAperture` and `CircularSectorAperture` `wire()` samples an aperture's boundary; `grid()` is its interior counterpart, mapping a grid of normalized coordinates in `[-1, 1]` onto points that fill the aperture. This is the primitive needed to sample a stop's interior with a grid of rays -- rather than inferring the field of view from the boundary extremes -- e.g. to robustly center a `SequentialSystem`'s field/pupil sampling. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0157xmSAJzcePsXEpaHmMhY4 --- optika/apertures/_apertures.py | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/optika/apertures/_apertures.py b/optika/apertures/_apertures.py index 71f36074..3a85c90b 100644 --- a/optika/apertures/_apertures.py +++ b/optika/apertures/_apertures.py @@ -593,6 +593,40 @@ def wire(self, num: None | int = None) -> na.Cartesian3dVectorArray: result = self.transformation(result) return result + def grid( + self, + normalized: na.AbstractCartesian2dVectorArray, + ) -> na.Cartesian3dVectorArray: + """ + Map a grid of normalized coordinates in :math:`[-1, 1]` onto points + filling the interior of the aperture. + + Unlike :meth:`wire`, which samples the boundary, this samples the + interior, and is useful for filling the aperture with rays. The + ``x`` component is mapped to the radius (vertex to rim) and the ``y`` + component to the polar angle (``angle_start`` to ``angle_stop``). + + Parameters + ---------- + normalized + A grid of normalized coordinates, where each component is in the + interval :math:`[-1, 1]`. + """ + unit_radius = na.unit(self.radius) + radius = self.radius * (normalized.x + 1) / 2 + angle = ( + self.angle_start + + (self.angle_stop - self.angle_start) * (normalized.y + 1) / 2 + ) + result = na.Cartesian3dVectorArray( + x=radius * np.cos(angle), + y=radius * np.sin(angle), + z=0 * unit_radius if unit_radius is not None else 0, + ) + if self.transformation is not None: + result = self.transformation(result) + return result + @dataclasses.dataclass(eq=False, repr=False) class EllipticalAperture( @@ -985,6 +1019,34 @@ def vertices(self): result.z = result.z * unit return result + def grid( + self, + normalized: na.AbstractCartesian2dVectorArray, + ) -> na.Cartesian3dVectorArray: + """ + Map a grid of normalized coordinates in :math:`[-1, 1]` onto points + filling the interior of the aperture. + + Unlike :meth:`wire`, which samples the boundary, this samples the + interior, and is useful for filling the aperture with rays. + + Parameters + ---------- + normalized + A grid of normalized coordinates, where each component is in the + interval :math:`[-1, 1]`. + """ + half_width = na.asanyarray(self.half_width, like=na.Cartesian2dVectorArray()) + unit = na.unit(half_width.x) + result = na.Cartesian3dVectorArray( + x=half_width.x * normalized.x, + y=half_width.y * normalized.y, + z=0 * unit if unit is not None else 0, + ) + if self.transformation is not None: + result = self.transformation(result) + return result + @dataclasses.dataclass(eq=False, repr=False) class AbstractRegularPolygonalAperture( From 8f6c57eefb10a545d04935116a08876e4472340e Mon Sep 17 00:00:00 2001 From: jacobdparker Date: Thu, 2 Jul 2026 16:03:59 -0600 Subject: [PATCH 2/2] Cover the transformation branch of the aperture grid() methods Adds test_grid_transformation exercising grid() on a transformed RectangularAperture and CircularSectorAperture, covering the two lines codecov flagged. Co-Authored-By: Claude Opus 4.8 --- optika/apertures/_apertures_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/optika/apertures/_apertures_test.py b/optika/apertures/_apertures_test.py index bdf01db7..11a45ab8 100644 --- a/optika/apertures/_apertures_test.py +++ b/optika/apertures/_apertures_test.py @@ -373,6 +373,27 @@ def test_decentered(self, a: optika.apertures.RectangularAperture): assert not np.any(b(point_outside)) +def test_grid_transformation(): + # grid() must apply the aperture's transformation to the sampled points + shift = na.transformations.Cartesian3dTranslation(x=5 * u.mm) + center = na.Cartesian2dVectorArray(x=0.0, y=0.0) + for aperture in ( + optika.apertures.RectangularAperture( + half_width=na.Cartesian2dVectorArray(10, 4) * u.mm, + transformation=shift, + ), + optika.apertures.CircularSectorAperture( + radius=125 * u.mm, + angle_start=90 * u.deg, + angle_stop=180 * u.deg, + transformation=shift, + ), + ): + grid = aperture.grid(center) + assert isinstance(grid, na.AbstractCartesian3dVectorArray) + assert np.all(aperture(grid)) + + class AbstractTestAbstractRegularPolygonalAperture( AbstractTestAbstractPolygonalAperture, ):