diff --git a/optika/_tests/test_apertures.py b/optika/_tests/test_apertures.py index 86dd5ed..e9bfbba 100644 --- a/optika/_tests/test_apertures.py +++ b/optika/_tests/test_apertures.py @@ -186,3 +186,44 @@ def test_half_width(self, a: optika.apertures.RectangularAperture): ) assert isinstance(a.half_width, types_valid) assert np.all(a.half_width >= 0) + + +class AbstractTestAbstractRegularPolygonalAperture( + AbstractTestAbstractPolygonalAperture, +): + + def test_radius(self, a: optika.apertures.AbstractRegularPolygonalAperture): + assert isinstance(na.as_named_array(a.radius), na.AbstractScalar) + + def test_num_vertices(self, a: optika.apertures.AbstractRegularPolygonalAperture): + assert isinstance(a.num_vertices, int) + + +class AbstractTestAbstractOctagonalAperture( + AbstractTestAbstractRegularPolygonalAperture, +): + pass + + +@pytest.mark.parametrize( + argnames="a", + argvalues=[ + optika.apertures.OctagonalAperture( + radius=radius, + samples_wire=21, + active=active, + inverted=inverted, + transformation=transformation, + kwargs_plot=kwargs_plot, + ) + for radius in radius_parameterization + for active in active_parameterization + for inverted in inverted_parameterization + for transformation in transform_parameterization + for kwargs_plot in test_plotting.kwargs_plot_parameterization + ], +) +class TestOctagonalAperture( + AbstractTestAbstractOctagonalAperture, +): + pass diff --git a/optika/apertures.py b/optika/apertures.py index 0c33a23..abef4d0 100644 --- a/optika/apertures.py +++ b/optika/apertures.py @@ -13,6 +13,9 @@ __all__ = [ "AbstractAperture", "CircularAperture", + "AbstractRegularPolygonalAperture", + "AbstractOctagonalAperture", + "OctagonalAperture", ] @@ -292,6 +295,37 @@ class AbstractPolygonalAperture( Base class for any type of polygonal aperture """ + def __call__( + self, + position: na.AbstractCartesian3dVectorArray, + ) -> na.AbstractScalar: + vertices = self.vertices + active = self.active + inverted = self.inverted + if self.transformation is not None: + position = self.transformation.inverse(position) + + shape = na.shape_broadcasted(vertices, active, inverted, position) + + vertices = na.broadcast_to(vertices, shape) + active = na.broadcast_to(active, shape) + inverted = na.broadcast_to(inverted, shape) + position = na.broadcast_to(position, shape) + + result = False + for v in range(vertices.shape["vertex"]): + vert_j = vertices[dict(vertex=v - 1)] + vert_i = vertices[dict(vertex=v)] + slope = (vert_j.y - vert_i.y) / (vert_j.x - vert_i.x) + condition_1 = (vert_i.y > position.y) != (vert_j.y > position.y) + condition_2 = position.x < ((position.y - vert_i.y) / slope + vert_i.x) + result = result ^ (condition_1 & condition_2) + + result[inverted] = ~result[inverted] + result[~active] = True + + return result + @property def bound_lower(self) -> na.AbstractCartesian3dVectorArray: return self.vertices.min(axis="vertex") @@ -468,3 +502,65 @@ def vertices(self): if self.transformation is not None: result = self.transformation(result) return result + + +@dataclasses.dataclass(eq=False, repr=False) +class AbstractRegularPolygonalAperture( + AbstractPolygonalAperture, +): + @property + @abc.abstractmethod + def radius(self) -> na.ScalarLike: + """ + the radial distance from the origin to each vertex + """ + + @property + @abc.abstractmethod + def num_vertices(self) -> int: + """ + Number of vertices in this polygon + """ + + @property + def vertices(self) -> na.AbstractCartesian3dVectorArray: + radius = self.radius + unit = na.unit(radius) + angle = na.linspace( + start=0 * u.deg, + stop=360 * u.deg, + axis="vertex", + num=self.num_vertices, + endpoint=False, + ) + result = na.Cartesian3dVectorArray( + x=radius * np.cos(angle).value, + y=radius * np.sin(angle).value, + z=0, + ) + if unit is not None: + result.z = result.z * unit + if self.transformation is not None: + result = self.transformation(result) + return result + + +@dataclasses.dataclass(eq=False, repr=False) +class AbstractOctagonalAperture( + AbstractRegularPolygonalAperture, +): + @property + def num_vertices(self) -> int: + return 8 + + +@dataclasses.dataclass(eq=False, repr=False) +class OctagonalAperture( + AbstractOctagonalAperture, +): + radius: float | u.Quantity | na.AbstractScalar = 0 * u.mm + samples_wire: int = 101 + active: bool | na.AbstractScalar = True + inverted: bool | na.AbstractScalar = False + transformation: None | na.transformations.AbstractTransformation = None + kwargs_plot: None | dict = None