Source code for optika.sags._parabolic
import dataclasses
import numpy as np
import astropy.units as u
import named_arrays as na
import optika
from ._conic import AbstractConicSag
__all__ = [
"ParabolicSag",
]
[docs]
@dataclasses.dataclass(eq=False, repr=False)
class ParabolicSag(
AbstractConicSag,
):
"""A parabolic sag profile."""
focal_length: u.Quantity | na.AbstractScalar = np.inf * u.mm
"""The focal length of this parabola."""
@property
def shape(self) -> dict[str, int]:
return na.broadcast_shapes(
optika.shape(self.focal_length),
optika.shape(self.transformation),
optika.shape(self.parameters_slope_error),
optika.shape(self.parameters_roughness),
optika.shape(self.parameters_microroughness),
)
@property
def radius(self) -> u.Quantity | na.AbstractScalar:
return 2 * self.focal_length
@property
def conic(self) -> int:
return -1
[docs]
def normal(
self,
position: na.AbstractCartesian3dVectorArray,
) -> na.AbstractCartesian3dVectorArray:
r = self.radius
unit = r.unit
transformation = self.transformation
if transformation is not None:
position = transformation.inverse(position)
r = r.value
position = position.to(unit).value
position.z = -r
_x = position.x
_y = position.y
return na.numexpr.evaluate(
"position / sqrt((_x / r)**2 + (_y / r)**2 + 1) / r",
)
[docs]
def intercept(
self,
rays: optika.rays.AbstractRayVectorArray,
) -> optika.rays.RayVectorArray:
r"""
Compute the intercept of a ray with this parabolic surface.
Parameters
----------
rays
The rays to find the intercept for.
Notes
-----
The equation for a paraboloid is
.. math::
z(x, y) = \frac{x^2 + y^2}{4 f},
where :math:`x`, :math:`y`, and :math:`z` are points on the paraboloid
and :math:`f` is the focal length of the paraboloid.
The equation for a line is
.. math::
\mathbf{x} = \mathbf{o} + d \mathbf{u},
where :math:`\mathbf{o}` is the starting point of the line,
:math:`\mathbf{u}` is a unit vector pointing in the direction of the line,
and :math:`d` is the distance from the origin to the intercept with
the paraboloid.
Combining these equations gives
.. math::
o_z + d u_z = \frac{(o_x + d u_x)^2 + (o_y + d u_y)^2}{4 f},
which can then be solved for :math:`d` using the quadratic equation,
.. math::
d = \frac{-o_x u_x - o_y u_y + 2 f u_z
- \text{sgn}(f u_z) \sqrt{-o_y^2 u_x^2 - o_x^2 u_y^2 + 2 o_y u_y (o_x u_x - 2 f u_z)
+ 4 f (o_z (u_x^2 + u_y^2) - o_x u_x u_z + f u_z^2}
}{u_x^2 + u_y^2}.
If the line is parallel to the :math:`z` axis, then the above equation
is singular and we need to solve the corresponding linear equation to find
.. math::
d = \frac{o_x^2 + o_y^2 - 4 f o_z}{4 f u_z}.
"""
if self.transformation is not None:
rays = self.transformation.inverse(rays)
f = self.focal_length
unit = f.unit
f = f.value # noqa: F841
o = rays.position.to(unit).value
u = rays.direction.value
ox = o.x # noqa: F841
oy = o.y # noqa: F841
oz = o.z # noqa: F841
ux = u.x # noqa: F841
uy = u.y # noqa: F841
uz = u.z # noqa: F841
position = na.numexpr.evaluate(
"o + u * where("
" (ux**2 + uy**2) > 1e-10,"
" (-ox * ux - oy * uy + 2 * f * uz - sign(f * uz) * sqrt("
" -(oy * ux)**2 - (ox * uy)**2 + 2 * oy * uy * (ox * ux - 2 * f * uz)"
" + 4 * f * (oz * (ux**2 + uy**2) - ox * ux * uz + f * uz**2)"
" )) / (ux**2 + uy**2),"
" (ox**2 + oy**2 - 4 * f * oz) / (4 * f * uz),"
")"
)
result = rays.replace(position=position << unit)
if self.transformation is not None:
result = self.transformation(result)
return result