Skip to content

Commit d57b0e8

Browse files
authored
Optimized ParabolicSag.intercept() using numexpr. (#143)
1 parent a27a6a3 commit d57b0e8

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

optika/_tests/test_sags.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ def test_intercept(
8080

8181
assert np.allclose(a(result.position), result.position.z)
8282

83+
result_expected = optika.sags.AbstractSag.intercept(a, rays)
84+
85+
assert np.allclose(result, result_expected)
86+
8387

8488
@pytest.mark.parametrize(
8589
argnames="a",

optika/sags.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,101 @@ def normal(
727727
"position / sqrt((_x / r)**2 + (_y / r)**2 + 1) / r",
728728
)
729729

730+
def intercept(
731+
self,
732+
rays: optika.rays.AbstractRayVectorArray,
733+
) -> optika.rays.RayVectorArray:
734+
r"""
735+
Compute the intercept of a ray with this parabolic surface.
736+
737+
Parameters
738+
----------
739+
rays
740+
The rays to find the intercept for.
741+
742+
Notes
743+
-----
744+
745+
The equation for a paraboloid is
746+
747+
.. math::
748+
749+
z(x, y) = \frac{x^2 + y^2}{4 f},
750+
751+
where :math:`x`, :math:`y`, and :math:`z` are points on the paraboloid
752+
and :math:`f` is the focal length of the paraboloid.
753+
The equation for a line is
754+
755+
.. math::
756+
757+
\mathbf{x} = \mathbf{o} + d \mathbf{u},
758+
759+
where :math:`\mathbf{o}` is the starting point of the line,
760+
:math:`\mathbf{u}` is a unit vector pointing in the direction of the line,
761+
and :math:`d` is the distance from the origin to the intercept with
762+
the paraboloid.
763+
Combining these equations gives
764+
765+
.. math::
766+
767+
o_z + d u_z = \frac{(o_x + d u_x)^2 + (o_y + d u_y)^2}{4 f},
768+
769+
which can then be solved for :math:`d` using the quadratic equation,
770+
771+
.. math::
772+
773+
d = \frac{-o_x u_x - o_y u_y + 2 f u_z
774+
- \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)
775+
+ 4 f (o_z (u_x^2 + u_y^2) - o_x u_x u_z + f u_z^2}
776+
}{u_x^2 + u_y^2}.
777+
778+
If the line is parallel to the :math:`z` axis, then the above equation
779+
is singular and we need to solve the corresponding linear equation to find
780+
781+
.. math::
782+
783+
d = \frac{o_x^2 + o_y^2 - 4 f o_z}{4 f u_z}.
784+
"""
785+
786+
if self.transformation is not None:
787+
rays = self.transformation.inverse(rays)
788+
789+
f = self.focal_length
790+
791+
unit = f.unit
792+
793+
f = f.value # noqa: F841
794+
795+
o = rays.position.to(unit).value
796+
797+
u = rays.direction.value
798+
799+
ox = o.x # noqa: F841
800+
oy = o.y # noqa: F841
801+
oz = o.z # noqa: F841
802+
803+
ux = u.x # noqa: F841
804+
uy = u.y # noqa: F841
805+
uz = u.z # noqa: F841
806+
807+
position = na.numexpr.evaluate(
808+
"o + u * where("
809+
" (ux**2 + uy**2) > 1e-10,"
810+
" (-ox * ux - oy * uy + 2 * f * uz - sign(f * uz) * sqrt("
811+
" -(oy * ux)**2 - (ox * uy)**2 + 2 * oy * uy * (ox * ux - 2 * f * uz)"
812+
" + 4 * f * (oz * (ux**2 + uy**2) - ox * ux * uz + f * uz**2)"
813+
" )) / (ux**2 + uy**2),"
814+
" (ox**2 + oy**2 - 4 * f * oz) / (4 * f * uz),"
815+
")"
816+
)
817+
818+
result = rays.replace(position=position << unit)
819+
820+
if self.transformation is not None:
821+
result = self.transformation(result)
822+
823+
return result
824+
730825

731826
@dataclasses.dataclass(eq=False, repr=False)
732827
class ToroidalSag(

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ classifiers = [
1515
]
1616
dependencies = [
1717
"astropy!=6.1.5",
18-
"named-arrays>=0.27.0",
18+
"named-arrays>=0.27.1",
1919
"pymupdf",
2020
"joblib",
2121
]

0 commit comments

Comments
 (0)