Source code for esis.optics._instruments._instruments

from __future__ import annotations
import abc
import dataclasses
import functools
import numpy as np
import astropy.units as u
import named_arrays as na
import optika
import esis

__all__ = [
    "AbstractInstrument",
    "Instrument",
]


[docs] @dataclasses.dataclass(eq=False, repr=False) class AbstractInstrument( optika.mixins.Printable, optika.mixins.Rollable, optika.mixins.Yawable, optika.mixins.Pitchable, ): """An interface describing the entire optical system.""" @property @abc.abstractmethod def name(self) -> str: """The human-readable name of the instrument.""" @property @abc.abstractmethod def axis_channel(self): """The name of the logical axis corresponding to changing camera channel.""" @property @abc.abstractmethod def front_aperture(self) -> None | esis.optics.abc.AbstractFrontAperture: """A model of the front aperture plate.""" @property @abc.abstractmethod def central_obscuration(self) -> None | esis.optics.abc.AbstractCentralObscuration: """A model of the central obscuration.""" @property @abc.abstractmethod def primary_mirror(self) -> None | esis.optics.abc.AbstractPrimaryMirror: """A model of the primary mirror.""" @property @abc.abstractmethod def field_stop(self) -> None | esis.optics.abc.AbstractFieldStop: """A model of the field stop.""" @property @abc.abstractmethod def grating(self) -> None | esis.optics.abc.AbstractGrating: """A model of the diffraction grating array.""" @property @abc.abstractmethod def filter(self) -> None | esis.optics.abc.AbstractFilter: """A model of the thin-film filters.""" @property @abc.abstractmethod def camera(self) -> None | esis.optics.Camera: """A model of the camera and sensors.""" @property @abc.abstractmethod def wavelength(self) -> None | u.Quantity | na.AbstractScalar: """ A default grid of wavelengths to trace through the system. Can be either in normalized coordinates (in the range :math:`-1` to :math:`+1`) or in physical coordinates (with units of length). See Also -------- :attr:`wavelength_physical`: This value converted into in physical coordinates. """ @property @abc.abstractmethod def field(self) -> None | na.AbstractCartesian2dVectorArray: """A default grid of field positions to trace through the system.""" @property @abc.abstractmethod def pupil(self): """A default grid of pupil positions to trace through the system.""" @property @abc.abstractmethod def kwargs_plot(self): """Extra keyword arguments used to plot the optical system.""" @property def angle_grating_input(self) -> na.AbstractScalar: """ The angle between the grating normal and the direction of the incident light. This is the incidence angle :math:`theta_i` in the `diffraction grating equation <https://en.wikipedia.org/wiki/Diffraction_grating>`_. """ fs = self.field_stop.surface grating = self.grating.surface position = na.Cartesian3dVectorArray() * u.mm normal_surface = grating.sag.normal(position) normal_rulings = grating.rulings.spacing_(position, normal_surface).normalized transformation = grating.transformation.inverse @ fs.transformation wire = np.moveaxis( a=fs.aperture.wire(), source="wire", destination="wire_grating_input", ) wire = transformation(wire) return np.arctan2( wire @ normal_rulings, wire @ normal_surface, ) @property def angle_grating_output(self) -> na.AbstractScalar: """ The angle between the grating normal and the direction of the diffracted light. This is an analogue to the diffracted angle in the `diffraction grating equation <https://en.wikipedia.org/wiki/Diffraction_grating>`_. """ detector = self.camera.surface grating = self.grating.surface position = na.Cartesian3dVectorArray() * u.mm normal_surface = grating.sag.normal(position) normal_rulings = grating.rulings.spacing_(position, normal_surface).normalized transformation = grating.transformation.inverse @ detector.transformation wire = np.moveaxis( a=detector.aperture.wire(), source="wire", destination="wire_grating_output", ) wire = transformation(wire) return np.arctan2( wire @ normal_rulings, wire @ normal_surface, ) @property def _wavelength_test_grid(self) -> na.AbstractScalar: position = na.Cartesian3dVectorArray() * u.mm grating = self.grating.surface normal = grating.sag.normal(position) m = grating.rulings.diffraction_order d = grating.rulings.spacing_(position, normal).length a = self.angle_grating_input b = self.angle_grating_output result = np.abs((np.sin(a) + np.sin(b)) * d / m) return result.to(u.AA) @property def wavelength_min(self) -> u.Quantity | na.AbstractScalar: """The minimum wavelength permitted through the system.""" return self._wavelength_test_grid.min( axis=("wire_grating_input", "wire_grating_output"), ) @property def wavelength_max(self) -> u.Quantity | na.AbstractScalar: """The maximum wavelength permitted through the system.""" return self._wavelength_test_grid.max( axis=("wire_grating_input", "wire_grating_output"), ) @property def wavelength_physical(self) -> na.ScalarArray: """The value of :attr:`wavelength` converted to physical units if needed.""" wavelength = self.wavelength if na.unit_normalized(wavelength).is_equivalent(u.dimensionless_unscaled): wavelength_min = self.wavelength_min wavelength_max = self.wavelength_max wavelength_range = wavelength_max - wavelength_min wavelength = wavelength_range * (wavelength + 1) / 2 + wavelength_min return wavelength @functools.cached_property def system(self) -> optika.systems.SequentialSystem: """ Convert this model into an instance of :class:`optika.systems.SequentialSystem`. This is a cached property that is only computed once. """ surfaces = [] surfaces += [self.front_aperture.surface] surfaces += [self.central_obscuration.surface] surfaces += [self.primary_mirror.surface] surfaces += [self.field_stop.surface] surfaces += [self.grating.surface] surfaces += [self.filter.surface] result = optika.systems.SequentialSystem( surfaces=surfaces, sensor=self.camera.surface, grid_input=optika.vectors.ObjectVectorArray( wavelength=self.wavelength_physical, field=self.field, pupil=self.pupil, ), transformation=self.transformation, kwargs_plot=self.kwargs_plot, ) return result
[docs] @dataclasses.dataclass(eq=False, repr=False) class Instrument( AbstractInstrument, ): """ An object which represents the entire optical system. A composition of the optical elements and a grid of input rays. Designed to resolve the optical elements into an instance of :class:`optika.systems.SequentialSystem` for performance modeling. """ name: str = "ESIS" """The human-readable name of the instrument.""" axis_channel: str = "channel" """The name of the logical axis corresponding to changing camera channel.""" front_aperture: None | esis.optics.FrontAperture = None """A model of the front aperture plate.""" central_obscuration: None | esis.optics.CentralObscuration = None """A model of the central obscuration.""" primary_mirror: None | esis.optics.PrimaryMirror = None """A model of the primary mirror.""" field_stop: None | esis.optics.FieldStop = None """A model of the field stop.""" grating: None | esis.optics.Grating = None """A model of the diffraction grating array.""" filter: None | esis.optics.Filter = None """A model of the thin-film filters.""" camera: None | esis.optics.Camera = None """A model of the camera and sensors.""" wavelength: None | u.Quantity | na.AbstractScalar = None """A default grid of wavelengths to trace through the system.""" field: None | na.AbstractCartesian2dVectorArray = None """A default grid of field positions to trace through the system.""" pupil: None | na.AbstractCartesian2dVectorArray = None """A default grid of pupil positions to trace through the system.""" pitch: u.Quantity | na.AbstractScalar = 0 * u.deg """The pitch angle of the instrument.""" yaw: u.Quantity | na.AbstractScalar = 0 * u.deg """The yaw angle of the instrument.""" roll: u.Quantity | na.AbstractScalar = 0 * u.deg """The roll angle of the instrument.""" kwargs_plot: None | dict = None """Extra keyword arguments used to plot the optical system."""