Source code for optika.surfaces

"""
Optical interfaces used to focus light.

The building block of an optical system.
"""

from typing import TypeVar, Generic
import abc
import dataclasses
import numpy.typing as npt
import matplotlib.axes
from astropy import units as u
import named_arrays as na
import optika
from ezdxf.addons.r12writer import R12FastStreamWriter

__all__ = [
    "AbstractSurface",
    "Surface",
]

SagT = TypeVar(
    "SagT",
    bound=None | optika.sags.AbstractSag,
)
MaterialT = TypeVar(
    "MaterialT",
    bound=None | optika.materials.AbstractMaterial,
)
ApertureT = TypeVar(
    "ApertureT",
    bound=None | optika.apertures.AbstractAperture,
)
ApertureMechanicalT = TypeVar(
    "ApertureMechanicalT",
    bound=None | optika.apertures.AbstractAperture,
)
RulingsT = TypeVar(
    "RulingsT",
    bound=None | optika.rulings.AbstractRulings,
)


[docs] @dataclasses.dataclass(eq=False, repr=False) class AbstractSurface( optika.mixins.DxfWritable, optika.mixins.Plottable, optika.mixins.Printable, optika.mixins.Transformable, optika.mixins.Shaped, optika.propagators.AbstractLightPropagator, Generic[SagT, MaterialT, ApertureT, ApertureMechanicalT, RulingsT], ): """ Interface describing a single optical interface. """ @property @abc.abstractmethod def name(self) -> str: """ The human-readable name of this surface. """ @property @abc.abstractmethod def sag(self) -> SagT: """ The sag profile of this surface. """ @property @abc.abstractmethod def material(self) -> MaterialT: """ The optical material type of this surface. """ @property @abc.abstractmethod def aperture(self) -> ApertureT: """ The region of this surface which allows light to propagate. """ @property @abc.abstractmethod def aperture_mechanical(self) -> ApertureMechanicalT: """ The shape of the physical substrate containing this optical surface. """ @property @abc.abstractmethod def rulings(self) -> RulingsT: """ The optional ruling profile of this surface. """ @property @abc.abstractmethod def is_field_stop(self) -> bool: """ A flag controlling whether this surface should act as the field stop for the system """ @property @abc.abstractmethod def is_pupil_stop(self) -> bool: """ A flag controlling whether this surface should act as the pupil stop for the system """ @property def is_stop(self) -> bool: """ If this surface is pupil stop or the field stop, return :obj:`True`. """ return self.is_field_stop or self.is_pupil_stop
[docs] def propagate_rays( self, rays: optika.rays.RayVectorArray, # material: None | optika.materials.AbstractMaterial = None, ) -> optika.rays.RayVectorArray: r""" Refract, reflect, and/or diffract the given rays off of this surface Parameters ---------- rays a set of input rays that will interact with this surface """ sag = self.sag material = self.material aperture = self.aperture rulings = self.rulings transformation = self.transformation if transformation is not None: rays = transformation.inverse(rays) rays_1 = sag.propagate_rays(rays) position_1 = rays_1.position normal = sag.normal(position_1) if rulings is not None: rays_1 = rulings.incident_effective( rays=rays_1, normal=normal, ) wavelength_1 = rays_1.wavelength a = rays_1.direction intensity_1 = rays_1.intensity n1 = rays_1.index_refraction position_2 = position_1 n2 = material.index_refraction(rays_1) r = n1 / n2 wavelength_2 = wavelength_1 / r b = optika.materials.snells_law( direction=a, index_refraction=n1, index_refraction_new=n2, is_mirror=material.is_mirror, normal=normal, ) efficiency = material.efficiency(rays_1, normal) if rulings is not None: efficiency = efficiency * rulings.efficiency(rays_1, normal) intensity_2 = intensity_1 * efficiency attenuation_2 = material.attenuation(rays_1) rays_2 = dataclasses.replace( rays_1, wavelength=wavelength_2, position=position_2, direction=b, intensity=intensity_2, attenuation=attenuation_2, index_refraction=n2, ) if aperture is not None: rays_2 = aperture.clip_rays(rays_2) if transformation is not None: rays_2 = transformation(rays_2) return rays_2
[docs] def plot( self, ax: None | matplotlib.axes.Axes | na.ScalarArray[npt.NDArray] = None, transformation: None | na.transformations.AbstractTransformation = None, components: None | tuple[str, ...] = None, **kwargs, ) -> dict[str, na.AbstractScalar]: sag = self.sag aperture = self.aperture aperture_mechanical = self.aperture_mechanical transformation_self = self.transformation kwargs_plot = self.kwargs_plot if transformation is not None: if transformation_self is not None: transformation = transformation @ transformation_self else: if transformation_self is not None: transformation = transformation_self if kwargs_plot is not None: kwargs = kwargs | kwargs_plot result = dict() if aperture is not None: result["aperture"] = aperture.plot( ax=ax, transformation=transformation, components=components, sag=sag, **kwargs, ) if aperture_mechanical is not None: result["aperture_mechanical"] = aperture_mechanical.plot( ax=ax, transformation=transformation, components=components, sag=sag, **kwargs, ) return result
def _write_to_dxf( self, dxf: R12FastStreamWriter, unit: u.Unit, transformation: None | na.transformations.AbstractTransformation = None, **kwargs, ) -> None: if self.transformation is not None: if transformation is not None: transformation = transformation @ self.transformation else: transformation = self.transformation super()._write_to_dxf( dxf=dxf, unit=unit, transformation=transformation, ) if self.aperture is not None: self.aperture._write_to_dxf( dxf=dxf, unit=unit, transformation=transformation, sag=self.sag, ) if self.aperture_mechanical is not None: self.aperture_mechanical._write_to_dxf( dxf=dxf, unit=unit, transformation=transformation, sag=self.sag, )
[docs] @dataclasses.dataclass(eq=False, repr=False) class Surface( AbstractSurface[SagT, MaterialT, ApertureT, ApertureMechanicalT, RulingsT], ): """ Representation of a single optical interface. Composition of a sag profile, material type, aperture, and ruling specification (all optional). Examples -------- Define a spherical mirror, with a rectangular aperture, :math:`z=50 \\; \\text{mm}` from the origin. Reflect a grid of collimated rays off of this mirror and measure their position at the origin. .. jupyter-execute:: import matplotlib.pyplot as plt import astropy.units as u import astropy.visualization import named_arrays as na import optika # define a spherical reflective surface 50 mm from the origin mirror = optika.surfaces.Surface( sag=optika.sags.SphericalSag(radius=-100 * u.mm), material=optika.materials.Mirror(), aperture=optika.apertures.RectangularAperture(30 * u.mm), transformation=na.transformations.Cartesian3dTranslation(z=50 * u.mm), ) # define a detector surface at the origin to capture the reflected rays detector=optika.surfaces.Surface() # define a grid of collimated input rays rays_input = optika.rays.RayVectorArray( position=na.Cartesian3dVectorArray( x=na.linspace(-25, 25, axis="pupil_x", num=5) * u.mm, y=na.linspace(-25, 25, axis="pupil_y", num=5) * u.mm, z=0 * u.mm, ), direction=na.Cartesian3dVectorArray(0, 0, 1), ) # propagate the rays through the mirror and detector surfaces rays_mirror = mirror.propagate_rays(rays_input) rays_detector = detector.propagate_rays(rays_mirror) # stack the 3 sets of rays into a single object # for easier plotting rays = [ rays_input, rays_mirror, rays_detector, ] rays = na.stack(rays, axis="surface") # plot the rays and surface with astropy.visualization.quantity_support(): fig, ax = plt.subplots() ax.set_aspect("equal") components_plot = ("z", "y") na.plt.plot(rays.position, axis="surface", components=components_plot, color="tab:blue"); mirror.plot(ax=ax, components=components_plot, color="black"); """ name: None | str = None """The human-readable name of the surface.""" sag: SagT = None """The sag profile of this surface.""" material: MaterialT = None """The optical material type of this surface.""" aperture: ApertureT = None """The region of this surface which allows light to propagate.""" aperture_mechanical: ApertureMechanicalT = None """The shape of the physical substrate containing this optical surface.""" rulings: RulingsT = None """The optional ruling profile of this surface.""" is_field_stop: bool = False """Whether this surface is the field stop of an optical system.""" is_pupil_stop: bool = False """Whether this surface is the pupil stop of an optical system.""" transformation: None | na.transformations.AbstractTransformation = None """The transformation between system coordinates and this surface.""" kwargs_plot: None | dict = None """Additional keyword arguments to pass to the :meth:`plot` function.""" def __post_init__(self): if self.sag is None: self.sag = optika.sags.NoSag() if self.material is None: self.material = optika.materials.Vacuum() @property def shape(self) -> dict[str, int]: return na.broadcast_shapes( optika.shape(self.name), optika.shape(self.sag), optika.shape(self.material), optika.shape(self.aperture), optika.shape(self.rulings), optika.shape(self.transformation), )