AbstractSequentialSystem._solve_rays names the free direction of the stop solve by its two transverse components and recovers the third from the unit-norm constraint:
def zfunc(xy):
return np.sqrt(1 - np.square(xy.length))
That is a chart on the sphere, and no two-parameter chart of a sphere is regular everywhere. Wherever the chart is anchored, it has a pole, and near it the slope dz/d|xy| diverges. A Newton step then overshoots past unit magnitude, z becomes not-a-number, and since a NaN never satisfies np.abs(f) < max_abs_error the solver runs its full iteration budget and fails the whole batch rather than the one ray.
Both anchorings have a pole, in different places
Measured on two minimal systems, each with the pupil stop first and carrying a physical aperture, so that the free variable is the direction.
|
anchored to the world (main) |
anchored to the launch surface (#222) |
45 degree fold ahead of the stop, beam along world x |
fails |
solves |
| launch surface turned nearly edge-on to the beam |
solves |
fails |
So this is not a regression introduced by either. The pole moved.
For the second case the boundary is sharp. Tilting the launch surface, with the resulting slope in the last column:
| tilt |
axial component |
slope |
result |
| 80 deg |
0.174 |
5.7 |
solves |
| 85 deg |
0.087 |
11.4 |
solves |
| 86 deg |
0.070 |
14.3 |
fails |
| 88 deg |
0.035 |
28.6 |
fails |
Both cases are pinned by tests in optika/systems/_sequential_test.py: test_stops_solve_with_a_folded_beam passes, and test_stops_solve_when_the_launch_surface_is_nearly_edge_on is marked xfail(strict=True), so it will start failing when this is fixed.
Suggested fix
Anchor the chart to each ray's seed direction rather than to any fixed axis. The seed is already aimed at the target, so the solution sits near the well-conditioned point by construction and the pole is more than ninety degrees away, which the aim logic makes unreachable. A two-vector in the plane perpendicular to the seed is enough; this does not need quaternions, which parametrise rotations rather than directions and would still have to be reduced to two numbers to serve as the free variable.
Only the direction branch is affected. The position branch recovers its third component from surface_first.sag, which is a statement about the launch surface's own frame and is correctly solved there.
Blast radius
No system I have access to is near either pole, so this is latent rather than live.
- ESIS launches from an untilted surface.
- FURST's first stop is its object, with an angular aperture, so it uses the position branch and never reaches this chart.
- MaGIXS does use this branch, with a grazing-incidence paraboloid as its pupil stop, and is nonetheless safe: its grazing angle comes from the sag of a surface of revolution about the axis, not from a tilted frame, so the surface's local axis is exactly the world's and the solved rays sit at a transverse component of 0.038 against a pole at 1.
Where it would bite is a grazing optic modelled as a tilted segment in its own rotated frame, a Kirkpatrick-Baez pair or a grazing fold flat, if that surface is the first stop.
Also
When the solve fails this way, the re-raised ValueError advises marking the object surface as the field stop. That is the diagnosis for a different failure and is misleading here, since the message asserts a cause it has not established.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Lm9SxSpyNg9hx8dNL9pUXc
AbstractSequentialSystem._solve_raysnames the free direction of the stop solve by its two transverse components and recovers the third from the unit-norm constraint:That is a chart on the sphere, and no two-parameter chart of a sphere is regular everywhere. Wherever the chart is anchored, it has a pole, and near it the slope
dz/d|xy|diverges. A Newton step then overshoots past unit magnitude,zbecomes not-a-number, and since a NaN never satisfiesnp.abs(f) < max_abs_errorthe solver runs its full iteration budget and fails the whole batch rather than the one ray.Both anchorings have a pole, in different places
Measured on two minimal systems, each with the pupil stop first and carrying a physical aperture, so that the free variable is the direction.
main)xSo this is not a regression introduced by either. The pole moved.
For the second case the boundary is sharp. Tilting the launch surface, with the resulting slope in the last column:
Both cases are pinned by tests in
optika/systems/_sequential_test.py:test_stops_solve_with_a_folded_beampasses, andtest_stops_solve_when_the_launch_surface_is_nearly_edge_onis markedxfail(strict=True), so it will start failing when this is fixed.Suggested fix
Anchor the chart to each ray's seed direction rather than to any fixed axis. The seed is already aimed at the target, so the solution sits near the well-conditioned point by construction and the pole is more than ninety degrees away, which the aim logic makes unreachable. A two-vector in the plane perpendicular to the seed is enough; this does not need quaternions, which parametrise rotations rather than directions and would still have to be reduced to two numbers to serve as the free variable.
Only the direction branch is affected. The position branch recovers its third component from
surface_first.sag, which is a statement about the launch surface's own frame and is correctly solved there.Blast radius
No system I have access to is near either pole, so this is latent rather than live.
Where it would bite is a grazing optic modelled as a tilted segment in its own rotated frame, a Kirkpatrick-Baez pair or a grazing fold flat, if that surface is the first stop.
Also
When the solve fails this way, the re-raised
ValueErroradvises marking the object surface as the field stop. That is the diagnosis for a different failure and is misleading here, since the message asserts a cause it has not established.🤖 Generated with Claude Code
https://claude.ai/code/session_01Lm9SxSpyNg9hx8dNL9pUXc