optiland icon indicating copy to clipboard operation
optiland copied to clipboard

Paraxial focal length: fix wrong sign for negative lenses

Open rjmoerland opened this issue 2 months ago • 3 comments

Why

The paraxial back focal length f2 of a negative lens is returned as a positive number by Optiland:

lens = Optic()

lens.add_surface(index=0, radius=np.inf, thickness=10_000)
lens.add_surface(
    index=1, surface_type="paraxial", f=-50.0, thickness=50.0, is_stop=True
)
lens.add_surface(index=2)
lens.set_aperture(aperture_type="float_by_stop_size", value=5)
lens.add_wavelength(value=0.55, is_primary=True)
print(lens.paraxial.f2())

Output: 50.0. The code contained a be.abs([...]) statement that, when removed, would return the correct value of -50 for this negative lens. However, it would also return a negative value for compound lenses that, overall, have a positive focal length, but contain an internal focus, as the sign of the ray's y-location is not taken into account.

Implementation

I've updated the logic, where I hopefully make the correct choice depending on the ray location, slope, and whether or not there are internal foci:

  • If the ray is exactly at the center of the lens surface, that counts as an internal (positive) focus
  • If the ray has a positive starting point, but negative slope, it's a positive lens (system)
  • Likewise, a negative starting point but positive slope also means it's a positive lens
  • If the ray and slope are both positive or negative:
    • There is no internal focus: the lens system is negative
    • There is an internal focus: the lens system is positive

I've checked that a lens system is considered positive, even if the focal plane is inside the system, with Winlens3D (Basic) and OpTaliX. All (py) tests I could think of now check out, but it's worth having a critical review. FWIW, all new pytests that I thought of fail on master, which I think is a good sign.

I've learned more about paraxial ray tracing than I ever wanted to (also, not as trivial as it sounds 😅 )

rjmoerland avatar Oct 26 '25 12:10 rjmoerland

Codecov Report

:white_check_mark: All modified and coverable lines are covered by tests.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #382      +/-   ##
==========================================
+ Coverage   92.81%   92.89%   +0.07%     
==========================================
  Files         245      253       +8     
  Lines       13839    14228     +389     
==========================================
+ Hits        12845    13217     +372     
- Misses        994     1011      +17     
Files with missing lines Coverage Δ
optiland/paraxial.py 93.01% <100.00%> (+0.27%) :arrow_up:

... and 19 files with indirect coverage changes

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

codecov[bot] avatar Oct 26 '25 12:10 codecov[bot]

Hi @rjmoerland,

Sorry for the delay. It's been a long week.

I had a chance to go through this in more detail. Thanks for submitting this! I think your reasoning checks out in most cases, though I wonder if there might be a simpler, more physically direct way to determine the sign. From a paraxial optics standpoint, the sign of the effective focal length should follow naturally from how a parallel input ray exits the system. That's essentially the same information contained in the ABCD matrix, but obtainable from a single paraxial ray trace. In principle, (i think) tracing one ray parallel to the optical axis and taking the ratio -y_out / u_out (with proper sign conventions) should yield both the magnitude and sign of f2 without needing to reason about internal foci. I did not check, but maybe there are some peculiarities with the Paraxial surface type here though..

I’ll try to verify that this gives consistent results with your current implementation, but if it does, it might simplify the logic quite a bit.

Thanks, Kramer

HarrisonKramer avatar Nov 01 '25 08:11 HarrisonKramer

No worries, all good. I realized in the mean time that this does not fix the sign for mirror optics. That'll need another fix.

I'm not sure you can get away from considering internal foci, but if you can that's great, and for sure I'd appreciate it if you critically look at my reasoning.

I was also thinking about a more general approach, since the code (seems to) assume that all optical elements are on a straight line, even though the real rays can be traced in 3D space. It would be great if the paraxial tracing would equally "not care" about 45° mirrors and such. I am thinking along the lines of tracings a (paraxial) ray along the optical axis with normal (x, y, z) = (0,0,1), and tracing a ray at (say) +1.0 Y with normal (0,0,1). Where these two rays end up on the last surface defines the orientation of the projection of the coordinate system. The +y axis' origin on the last surface is wherever the paraxial ray along the optical axis ended, and the positive direction is wherever the ray starting at y=+1 ended up.

I think that the same information that currently is used in the code (plus or minus y, plus or minus u) is present in those vectors, and my expectation is that the sign of the cross product of the "local" y and the ray leaving the last surface should tell us something about the sign of the focus. Would need to write it out to be sure though.

rjmoerland avatar Nov 01 '25 23:11 rjmoerland