Source code for optika.materials._materials

from __future__ import annotations
import abc
import dataclasses
import astropy.units as u
import named_arrays as na
import optika
from ._layers import Layer

__all__ = [
    "AbstractMaterial",
    "Vacuum",
    "AbstractMirror",
    "Mirror",
    "MeasuredMirror",
]


[docs] @dataclasses.dataclass(eq=False, repr=False) class AbstractMaterial( optika.mixins.Printable, optika.mixins.Transformable, optika.mixins.Shaped, ): """An interface describing a generalized optical material."""
[docs] @abc.abstractmethod def index_refraction( self, rays: optika.rays.AbstractRayVectorArray, ) -> na.ScalarLike: """ the index of refraction of this material for the given input rays Parameters ---------- rays input rays used to evaluate the index of refraction """
[docs] @abc.abstractmethod def attenuation( self, rays: optika.rays.AbstractRayVectorArray, ) -> na.ScalarLike: """ the attenuation coefficient of the given rays Parameters ---------- rays input rays to calculate the attenuation coefficient for """
[docs] @abc.abstractmethod def efficiency( self, rays: optika.rays.AbstractRayVectorArray, normal: na.AbstractCartesian3dVectorArray, ) -> na.ScalarLike: """ The fraction of light that passes through the interface. Parameters ---------- rays the input rays to calculate the efficiency for normal the vector perpendicular to the optical surface """
@property @abc.abstractmethod def is_mirror(self) -> bool: """ flag controlling whether this material reflects or transmits light """
[docs] @dataclasses.dataclass(eq=False, repr=False) class Vacuum( AbstractMaterial, ): """Empty space, the default material.""" @property def shape(self) -> dict[str, int]: return dict() @property def transformation(self) -> None: return None
[docs] def index_refraction( self, rays: optika.rays.RayVectorArray, ) -> na.ScalarLike: return 1
[docs] def attenuation( self, rays: optika.rays.RayVectorArray, ) -> na.ScalarLike: return 0 / u.mm
[docs] def efficiency( self, rays: optika.rays.RayVectorArray, normal: na.AbstractCartesian3dVectorArray, ) -> na.ScalarLike: return 1
@property def is_mirror(self) -> bool: return False
[docs] @dataclasses.dataclass(eq=False, repr=False) class AbstractMirror( AbstractMaterial, ): """An interface describing a generalized reflective mirror.""" @property @abc.abstractmethod def substrate(self) -> None | Layer: """ A layer representing the substrate supporting the reflective surface. """ @property def transformation(self) -> None: return None
[docs] def index_refraction( self, rays: optika.rays.RayVectorArray, ) -> na.ScalarLike: return rays.index_refraction
[docs] def attenuation( self, rays: optika.rays.RayVectorArray, ) -> na.ScalarLike: return rays.attenuation
[docs] def efficiency( self, rays: optika.rays.RayVectorArray, normal: na.AbstractCartesian3dVectorArray, ) -> na.ScalarLike: return 1
@property def is_mirror(self) -> bool: return True
[docs] @dataclasses.dataclass(eq=False, repr=False) class Mirror( AbstractMirror, ): """A perfect mirror material.""" substrate: None | Layer = None """A layer representing the substrate supporting the reflective surface.""" @property def shape(self) -> dict[str, int]: return na.broadcast_shapes( optika.shape(self.substrate), )
[docs] @dataclasses.dataclass(eq=False, repr=False) class MeasuredMirror( AbstractMirror, ): """ A mirror where the reflectivity has been measured by an external source as a function of wavelength. Examples -------- Create a mirror where the reflectivity is a Gaussian centered at 304 Angstroms. .. jupyter-execute:: import numpy as np import matplotlib.pyplot as plt import astropy.units as u import astropy.visualization import named_arrays as na import optika # Define the mean and standard deviation of the reflectivity peak. center = 304 * u.AA width = 10 * u.AA # Define a grid of wavelengths wavelength_min = center - 3 * width wavelength_max = center + 3 * width wavelength = na.linspace( start=wavelength_min, stop=wavelength_max, axis="wavelength", num=11, ) # Define an array of simulated reflectivity measurements efficiency = na.FunctionArray( inputs=na.SpectralDirectionalVectorArray( wavelength=wavelength, direction=na.Cartesian3dVectorArray(0, 0, 1), ), outputs=np.exp(-np.square((wavelength - center) / width) / 2), ) # Create an instance of a MeasuredMirror object mirror = optika.materials.MeasuredMirror(efficiency) # Define a new grid of wavelengths at which to evaluate the interpolated # reflectivity wavelength_interp = na.linspace( start=wavelength_min, stop=wavelength_max, axis="wavelength", num=1001, ) # Evaluate the interpolated reflectivity efficiency_interp = mirror.efficiency( rays=optika.rays.RayVectorArray( wavelength=wavelength_interp, direction=na.Cartesian3dVectorArray(0, 0, 1), ), normal=na.Cartesian3dVectorArray(0, 0, 1), ) # Plot the interpolated reflectivity vs the measured reflectivity with astropy.visualization.quantity_support(): fig, ax = plt.subplots() na.plt.plot(wavelength_interp, efficiency_interp, label="interpolated"); na.plt.scatter(efficiency.inputs.wavelength, efficiency.outputs, label="measured"); ax.set_xlabel(f"wavelength ({ax.xaxis.get_label().get_text()})"); ax.set_ylabel(f"reflectivity"); ax.legend(); """ efficiency_measured: na.FunctionArray[ na.SpectralDirectionalVectorArray, na.AbstractScalar ] = dataclasses.MISSING """ A function array that maps wavelengths and incidence angles to the measured reflectivity. """ substrate: None | Layer = None """A layer representing the substrate supporting the reflective surface.""" serial_number: None | str | na.AbstractArray = None """A unique number associated with this material""" @property def shape(self) -> dict[str, int]: axis_wavelength = self.efficiency_measured.inputs.wavelength.axes shape = optika.shape(self.efficiency_measured.outputs) for ax in axis_wavelength: shape.pop(ax, None) return na.broadcast_shapes( shape, optika.shape(self.substrate), optika.shape(self.serial_number), )
[docs] def efficiency( self, rays: optika.rays.RayVectorArray, normal: na.AbstractCartesian3dVectorArray, ) -> na.ScalarLike: measurement = self.efficiency_measured wavelength = measurement.inputs.wavelength direction = measurement.inputs.direction efficiency = measurement.outputs if direction.size != 1: # pragma: nocover raise ValueError( "Interpolating over different incidence angles is not supported." ) if wavelength.ndim != 1: # pragma: nocover raise ValueError( f"wavelength must be one dimensional, got shape {wavelength.shape}" ) return na.interp( x=rays.wavelength, xp=wavelength, fp=efficiency, )