Skip to content

Stop root-finding does not converge for refractive (lens) systems #177

Description

@jacobdparker

With the Glass material from #176, refractive (lens) systems can be built, but the stop root-finding problem doesn't converge for them once #169 and #170 are applied. There are two distinct root causes, and both sit in the _anchor_surface / involutory-stop logic those PRs introduce — so I wanted to align on the design before writing more code.

All repros use the Glass material from #176 and were run against fix/transmissive-stops (i.e. #169 + #170) with that material cherry-picked on top.

Root cause 1 — a refractive surface used as the stop (#170)

Modeling the stop as the optic itself (the pattern in prime_focus.ipynb, where the primary mirror is the pupil stop):

import astropy.units as u, named_arrays as na, optika
nd = 1.5168; f = 100*u.mm; R = 2*(nd-1)*f; t = 5*u.mm; r = 10*u.mm
front = optika.surfaces.Surface(name="front", sag=optika.sags.SphericalSag(R),
    material=optika.materials.Glass.n_bk7(),
    aperture=optika.apertures.CircularAperture(r), is_pupil_stop=True)
back = optika.surfaces.Surface(name="back", sag=optika.sags.SphericalSag(-R),
    material=optika.materials.Vacuum(), aperture=optika.apertures.CircularAperture(r),
    transformation=na.transformations.Cartesian3dTranslation(z=t))
sensor = optika.sensors.ImagingSensor(name="sensor", width_pixel=15*u.um,
    axis_pixel=na.Cartesian2dVectorArray("dx","dy"), num_pixel=na.Cartesian2dVectorArray(64,64),
    transformation=na.transformations.Cartesian3dTranslation(z=t+f), is_field_stop=True)
grid = optika.vectors.ObjectVectorArray(wavelength=500*u.nm,
    field=na.Cartesian2dVectorLinearSpace(-1,1,axis=na.Cartesian2dVectorArray("fx","fy"),num=1,centers=True),
    pupil=na.Cartesian2dVectorLinearSpace(-1,1,axis=na.Cartesian2dVectorArray("px","py"),num=5,centers=True))
optika.systems.SequentialSystem(surfaces=[front,back], sensor=sensor, grid_input=grid).rayfunction_stops

This converges on main but raises Could not solve ... 'front' and 'sensor' on the stack. A refractive stop is (correctly) non-involutory, so _calc_rayfunction_stops_only puts it in propagators = subsystem and the seed ray is positioned on the stop surface. Re-intercepting a curved surface from a point already on it lands on the far side of the sphere, so the residual is meaningless. This is fine for the flat transmissive stops #170 targets (FZP / transmission gratings), but breaks for a curved refractive stop.

Root cause 2 — _anchor_surface seeding for on-axis / nearby power (#169)

The realistic camera-lens layout — a flat aperture stop (iris) just ahead of a refractive singlet — fails even for the single on-axis field point:

stop  = optika.surfaces.Surface(name="stop", aperture=optika.apertures.CircularAperture(8*u.mm), is_pupil_stop=True)
front = optika.surfaces.Surface(name="front", sag=optika.sags.SphericalSag(R), material=optika.materials.Glass.n_bk7(),
    aperture=optika.apertures.CircularAperture(12*u.mm), transformation=na.transformations.Cartesian3dTranslation(z=2*u.mm))
# back + sensor as above, shifted; stop is 2 mm ahead of the lens

It diverges with invalid value encountered in sqrtMax iterations exceeded. _anchor_surface returns the curved front (2 mm downstream) and the seed aims every pupil ray at its center, so off-axis pupil points get near-grazing seed directions and the Newton step pushes |d_xy| > 1, making zfunc = sqrt(1 - |d_xy|^2) undefined. Moving the iris farther from the lens confirms the mechanism:

iris → lens distance result
2 mm diverges
50 mm converges
200 mm converges

The anchor-center seed is right for an off-axis feed/fold mirror (the case it was added for), but wrong for an on-axis powered surface close to the stop, where a near-collimated seed (≈ +z for an object at infinity) is what's needed.

Question

Since both of these are in the anchor/involutory logic you're reviewing on #169/#170, how would you like to handle refractive support? The directions I see:

  1. Seeding: when the object is at infinity (or the anchor is essentially on-axis from the stop), seed the free ray near-collimated rather than aimed at the anchor center.
  2. Curved non-involutory stop: seed the ray before the stop surface (so its intercept is well-defined) instead of on it, or otherwise apply its interaction without the spurious re-intercept.

Happy to implement either once we agree on the approach. The Glass material in #176 is independent of all this and stands on its own. A lens tutorial (Cooke triplet) is the eventual goal but is blocked on this.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions