Source code for optika.sags._spherical
import abc
import dataclasses
import numpy as np
import astropy.units as u
import named_arrays as na
import optika
from ._abc import AbstractSag
__all__ = [
"AbstractSphericalSag",
"SphericalSag",
]
[docs]
@dataclasses.dataclass(eq=False, repr=False)
class AbstractSphericalSag(
AbstractSag,
):
"""
Base class for all sag functions that can be approximated as a sphere.
"""
@property
@abc.abstractmethod
def radius(self) -> u.Quantity | na.AbstractScalar:
"""
The radius of curvature of this sphere.
"""
@property
def curvature(self) -> u.Quantity | na.AbstractScalar:
"""
The curvature of the spherical surface.
Equal to the reciprocal of :attr:`radius`.
"""
return 1 / self.radius
[docs]
@dataclasses.dataclass(eq=False, repr=False)
class SphericalSag(
AbstractSphericalSag,
):
r"""
A spherical sag function.
The sag (:math:`z` coordinate) of a spherical surface is calculated using
the expression
.. math::
z(x, y) = \frac{c (x^2 + y^2)}{1 + \sqrt{1 - c^2 (x^2 + y^2)}}
where :math:`c` is the :attr:`curvature`,
and :math:`x,y`, are the 2D components of the evaluation point.
Examples
--------
Plot a slice through the sag surface
.. jupyter-execute::
import matplotlib.pyplot as plt
import astropy.units as u
import astropy.visualization
import named_arrays as na
import optika
sag = optika.sags.SphericalSag(
radius=na.linspace(100, 300, axis="radius", num=3) * u.mm,
)
position = na.Cartesian3dVectorArray(
x=na.linspace(-90, 90, axis="x", num=101) * u.mm,
y=0 * u.mm,
z=0 * u.mm
)
z = sag(position)
with astropy.visualization.quantity_support():
plt.figure()
plt.gca().set_aspect("equal")
na.plt.plot(position.x, z, axis="x", label=sag.radius)
plt.legend(title="radius")
"""
radius: u.Quantity | na.AbstractScalar = np.inf * u.mm
"""The radius of curvature of this sphere."""
@property
def shape(self) -> dict[str, int]:
return na.broadcast_shapes(
optika.shape(self.radius),
optika.shape(self.transformation),
optika.shape(self.parameters_slope_error),
optika.shape(self.parameters_roughness),
optika.shape(self.parameters_microroughness),
)
def __call__(
self,
position: na.AbstractCartesian3dVectorArray,
) -> na.AbstractScalar:
"""
Evaluate the sag function at the given position
Parameters
----------
position
point where the sag function will be calculated
"""
c = self.curvature
transformation = self.transformation
if transformation is not None:
position = transformation.inverse(position)
shape = na.shape_broadcasted(c, position)
c = na.broadcast_to(c, shape)
position = na.broadcast_to(position, shape)
r2 = np.square(position.x) + np.square(position.y)
sz = c * r2 / (1 + np.sqrt(1 - np.square(c) * r2))
return sz
[docs]
def normal(
self,
position: na.AbstractCartesian3dVectorArray,
) -> na.Cartesian3dVectorArray:
c = self.curvature
unit = 1 / c.unit
transformation = self.transformation
if transformation is not None:
position = transformation.inverse(position)
c = c.value
x = position.x.to(unit).value
y = position.y.to(unit).value
nx = c * x
ny = c * y
nz = na.numexpr.evaluate("-sqrt(1 - nx**2 - ny**2)")
return na.Cartesian3dVectorArray(nx, ny, nz)
[docs]
def intercept(
self,
rays: optika.rays.AbstractRayVectorArray,
) -> optika.rays.AbstractRayVectorArray:
if self.transformation is not None:
rays = self.transformation.inverse(rays)
r = self.radius
unit = r.unit
r = r.value
o = rays.position.to(unit).value
u = rays.direction
p = o.replace(z=o.z - r)
px = p.x # noqa: F841
py = p.y # noqa: F841
pz = p.z # noqa: F841
ux = u.x # noqa: F841
uy = u.y # noqa: F841
uz = u.z # noqa: F841
position = na.numexpr.evaluate(
"o + u * ("
" -(ux * px + uy * py + uz * pz)"
" - sign(r * uz) * sqrt("
" (ux * px + uy * py + uz * pz)**2"
" - (px**2 + py**2 + pz**2 - r**2)"
" )"
")"
)
result = rays.replace(position=position << unit)
if self.transformation is not None:
result = self.transformation(result)
return result