Source code for physical_apertures.rotated

"""Rotation of an existing physical aperture around a specified pivot."""

from __future__ import annotations

import math
from typing import TYPE_CHECKING, Any

from optiland.physical_apertures.base import BaseAperture

if TYPE_CHECKING:
    from optiland._types import BEArray


[docs] class RotatedAperture(BaseAperture): """Rotate an aperture counterclockwise about +z. Args: aperture: The aperture to rotate. angle: Rotation angle in radians. center_x: Pivot x-coordinate, defaulting to the surface origin. center_y: Pivot y-coordinate, defaulting to the surface origin. Raises: ValueError: If the angle or pivot coordinates are not finite. """ def __init__( self, aperture: BaseAperture, angle: float, center_x: float = 0.0, center_y: float = 0.0, ) -> None: if not all(math.isfinite(value) for value in (angle, center_x, center_y)): raise ValueError("Aperture angle and pivot coordinates must be finite") self.aperture = aperture self.angle = angle self.center_x = center_x self.center_y = center_y @property def extent(self) -> tuple[float, float, float, float]: """Return an enclosing rotated bounding box, including unbounded shapes.""" x0, x1, y0, y1 = self.aperture.extent x0, x1 = x0 - self.center_x, x1 - self.center_x y0, y1 = y0 - self.center_y, y1 - self.center_y c, s = math.cos(self.angle), math.sin(self.angle) def project(a: float, b: float, origin: float) -> tuple[float, float]: # A zero coefficient contributes nothing, even for infinite bounds. # Project intervals instead of corners to avoid inf - inf and 0*inf. xs = (a * x0, a * x1) if a else (0.0, 0.0) ys = (b * y0, b * y1) if b else (0.0, 0.0) return origin + min(xs) + min(ys), origin + max(xs) + max(ys) return (*project(c, -s, self.center_x), *project(s, c, self.center_y))
[docs] def contains(self, x: BEArray, y: BEArray) -> BEArray: """Test points after inverse rotation into the child aperture's frame.""" c, s = math.cos(self.angle), math.sin(self.angle) x, y = x - self.center_x, y - self.center_y return self.aperture.contains( self.center_x + c * x + s * y, self.center_y - s * x + c * y )
[docs] def scale(self, scale_factor: float) -> None: """Scale the child aperture and pivot together, retaining the angle.""" self.aperture.scale(scale_factor) self.center_x *= scale_factor self.center_y *= scale_factor
[docs] def to_dict(self) -> dict[str, Any]: """Persist the child definition, angle and pivot in native aperture data.""" return { **super().to_dict(), "aperture": self.aperture.to_dict(), "angle": self.angle, "center_x": self.center_x, "center_y": self.center_y, }
[docs] @classmethod def from_dict(cls, data: dict[str, Any]) -> RotatedAperture: """Restore a rotation using the shared native aperture dispatch.""" return cls( BaseAperture.from_dict(data["aperture"]), data["angle"], data["center_x"], data["center_y"], )