Skip to content

Added support for using the object surface as the field stop of a SequentialSystem#169

Open
jacobdparker wants to merge 1 commit into
fix/solver-robustnessfrom
fix/object-field-stop
Open

Added support for using the object surface as the field stop of a SequentialSystem#169
jacobdparker wants to merge 1 commit into
fix/solver-robustnessfrom
fix/object-field-stop

Conversation

@jacobdparker

Copy link
Copy Markdown
Contributor

Second PR in the stop-solver stack — stacked on #168 (the diff shown here is relative to that branch).

Problem

Marking the object surface (with an angular, dimensionless aperture) as the field stop is the natural configuration for dispersive systems — at a single wavelength of a dispersed spectrum, most of the sensor outline is unreachable, so targeting the sensor wire is ill-posed. But this configuration was broken in three independent ways:

  1. The position-variable branch of _calc_rayfunction_stops_only called sag() with a 2-component vector → TypeError before the solve started.
  2. The solved outputs were never broadcast over both stop axes → axis error in the field_min/max / pupil_min/max reductions.
  3. The hardcoded initial guess (rays at the first stop's origin, direction +z) produces NaN first residuals whenever the next surface is far off that axis (e.g. the feed mirror of a Rowland-circle spectrograph, 174 mm off-axis in FURST) or on steep grazing flanks — and Newton cannot recover from NaN.

Changes

  • zfunc builds a proper 3-D vector before calling sag().
  • _calc_rayfunction_stops broadcasts position and direction against each other before the stop-axis reductions.
  • New _anchor_surface helper: the seed now aims each ray at its own target point on the last stop surface when no powered surface lies between the stops (nearly exact — the grazing solve converges in ~5 iterations), and otherwise at the center of the first powered surface.

Tests

Adds TestSequentialSystemGrazingSpectrograph: a grazing-incidence paraboloid + transmission-grating spectrograph whose source is the field stop, asserting the recovered field equals the source's angular radius (0.25°) to 1e-6 deg. Also verified against the FURST Spectrograph model from Kankelborg-Group/furst-optics#25, which recovers field_max = ±0.26656° (the sunpy solar angular radius).

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.50%. Comparing base (901c539) to head (74c747b).

Additional details and impacted files
@@                    Coverage Diff                    @@
##           fix/solver-robustness     #169      +/-   ##
=========================================================
+ Coverage                  99.34%   99.50%   +0.15%     
=========================================================
  Files                        116      116              
  Lines                       5974     6042      +68     
=========================================================
+ Hits                        5935     6012      +77     
+ Misses                        39       30       -9     
Flag Coverage Δ
unittests 99.50% <100.00%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@jacobdparker jacobdparker force-pushed the fix/solver-robustness branch from 9905960 to 901c539 Compare June 11, 2026 23:04
@jacobdparker jacobdparker force-pushed the fix/object-field-stop branch from 40c7ea5 to d5a51da Compare June 11, 2026 23:14
…uentialSystem

Marking the object surface (with an angular, dimensionless aperture) as
the field stop is the natural configuration for dispersive systems,
where the sensor outline is not reachable at a single wavelength, but
the stop root-finding problem was broken in three ways for this case:

- The position-variable branch of `_calc_rayfunction_stops_only` called
  `sag()` with a 2-component vector, raising a TypeError before the
  solve even started.
- The solved outputs were never broadcast over both stop axes, so the
  reductions in `field_min`/`field_max`/`pupil_min`/`pupil_max` raised
  an axis error.
- The initial guess started every ray at the origin of the first stop
  with direction +z, which produces NaN residuals on the first
  iteration for surfaces far from that axis (e.g. the off-axis feed
  mirror of a Rowland-circle spectrograph) or on steep grazing-incidence
  flanks, and Newton's method cannot recover from NaN.

The seed now aims each ray at its own target point on the last stop
surface when no surface with optical power lies between the two stops
(nearly exact), and otherwise at the center of the first powered
surface.

Adds a grazing-incidence spectrograph regression test whose source is
the field stop, asserting that the recovered field equals the source's
angular radius.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jacobdparker jacobdparker force-pushed the fix/object-field-stop branch from d5a51da to 74c747b Compare June 11, 2026 23:50
if material is not None and material.is_mirror:
return surface
sag = surface.sag
if sag is not None and not isinstance(sag, optika.sags.NoSag):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not enough for there to be a sag, the material must also have a different index of refraction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants