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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions