Skip to content

Add FigureErrorSag, a base sag profile plus a map of its figure error - #269

Merged
roytsmart merged 7 commits into
mainfrom
feature/figure-error
Oct 31, 2026
Merged

roytsmart merged 7 commits into
mainfrom
feature/figure-error

Conversation

@roytsmart

@roytsmart roytsmart commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

optika.sags.FigureErrorSag adds a map of the figure error of a surface, such as one measured with an interferometer, to the height of a base sag profile. The map is a function array, evaluated at the positions with the multilinear interpolation of named-arrays.

Design

  • Height only: the map raises the surface, which a physical-optics calculation that samples the surface sees, but the normal vectors and the intercepts of rays are those of the base profile, so the map doesn't deflect rays. The slope of a measured map between its pixels is mostly noise, and where the map shifts the phase of the wavefront by less than about a radian, it scatters a little of the light instead of deflecting all of it. Low-order terms which should deflect rays belong in the base profile, for example as a ZernikeSag, with the rest of the map in FigureErrorSag.
  • Axes: axis names the two axes along which the map is sampled, and is required whenever there is a map. The map's other axes, such as one along which there is a separate map for each channel, are axes of the sag profile. The map may be sampled on a separate grid for each channel. A missing, misnamed or repeated axis, a str, or a third axis fails as soon as the shape of the sag profile is needed, as a misnamed axis of ZernikeSag does.
  • Uncertainty: the map and the positions may be uncertain, which needs named-arrays 2.18, so the requirement goes from named-arrays~=2.15 to ~=2.18.
  • Missing pixels: a pixel which wasn't measured should be filled, since the map is NaN next to a pixel which is NaN. Outside the map, the heights are extrapolated linearly from the cells at its edge.

Things to note

  • The intercept test is replaced. The abstract test_intercept of the sag profiles requires the intercepts to lie on the surface, sag(position). Those of FigureErrorSag lie on the base profile instead, by design, and miss the surface by up to the height of the map, which is nanometers for a measured map. TestFigureErrorSag.test_intercept replaces the abstract test with one which requires the positions and directions of the intercepted rays to be exactly those of the base profile.
  • A raytrace changes slightly when the map is on a stop surface. A system launches the rays of its stop solve from the height of the stop surface, which includes the map, while the rays intercept the base profile. In a test system with a uniform 10 nm map on the pupil stop, the rays moved by up to 0.7 nm and turned by up to 3.5 nrad. With the map on a surface which isn't a stop, the raytrace was exactly the same as through the base profile. The docstring says so, rather than the stop solve working around it.
  • What uses the map isn't merged yet. The map is for Jake's wave optics, optika.wavefields on feature/wavefields, which samples each surface at position.z = sag(position). Its PR, Add optika.wavefields: physical-optics propagation through a SequentialSystem #173, was closed unmerged two seconds after its base, Add optika.zernikes and ZernikeSag #172, was merged, and Add beam-footprint utilities and PSF metrics for the wavefield path #174 is still open on top of it. Until the wave optics are merged, the map only shows up where the height of the sag profile is evaluated directly, which includes the stop solve.

Tests

  • TestFigureErrorSag runs the abstract tests of the sag profiles on a map of a flat surface, a parabolic one whose axes are given as a list, a separate map for each channel on a transformed spherical one, a separate grid for each channel, an uncertain map on top of a ZernikeSag, and no map. Its test_normal also requires the normal vectors to be exactly those of the base profile.
  • test__call__figure_error checks the height above the base profile against an affine map, which multilinear interpolation reproduces exactly, in the coordinate system of the transformed sag profile, for each of those cases.
  • test_axis_ and test_shape_figure_error check the axes of each case, and that the shape leaves out the axes along which the map is sampled. test_axis_invalid checks that a missing, misnamed, single or repeated axis, a str, and a third axis each fail, for the shape and for the height.
  • Each of 11 mutations of FigureErrorSag at 8979388 made at least one test fail, and so does each of 5 mutations of the checks of axis.

Checks

  • black and ruff pass.
  • Locally, with named-arrays 2.18.0 from PyPI, the full suite passes: 23,685 tests under xdist, and the 482 test_to_dxf cases run on their own. Those all write test_dwg.dxf in the working directory, so they race between xdist workers. CI runs the suite serially.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP

roytsmart and others added 5 commits October 29, 2026 11:59
`optika.sags.FigureErrorSag` adds a map of the figure error of a
surface, such as one measured with an interferometer, to the height of a
base sag profile. The map is a function array, evaluated at the
positions with multilinear interpolation.

The map raises the surface, which a physical-optics calculation that
samples the surface sees, but the normal vectors and the intercepts of
rays are those of the base profile, so the map does not deflect rays.
The slope of a measured map between its pixels is mostly noise, and
where the map shifts the phase of the wavefront by less than about a
radian, it scatters a little of the light instead of deflecting all of
it. Low-order terms which should deflect rays belong in the base
profile, for example as a `ZernikeSag`.

`axis` names the axes along which the map is sampled, and its other
axes, such as one along which there is a separate map for each channel,
are axes of the sag profile. The map and the positions may be
uncertain, which needs named-arrays 2.18.

The tests run the abstract tests of sag profiles, with the test of the
intercepts replaced by one which requires them to be those of the base
profile, and check:
- the height above the base profile against an affine map, in the
  coordinate system of a transformed sag profile, with a map for each
  channel, with an uncertain map, and with none;
- that the normal vectors are those of the base profile;
- the shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
The map has two components, so it is always sampled along two axes, and a single axis could never be resampled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
…annel

With a grid for each channel, the default axes would include the channels, so the test fails if the explicit axes are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
The slice at y = 10 mm ran near a node of the map, where its height is only about 1.4 nm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (771a4f2) to head (e09659c).

Additional details and impacted files
@@            Coverage Diff             @@
##              main      #269    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          129       131     +2     
  Lines        10540     10681   +141     
==========================================
+ Hits         10540     10681   +141     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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.

roytsmart and others added 2 commits October 30, 2026 15:05
A system launches the rays of its stop solve from the height of the stop
surface, which includes the figure error, so on a stop surface the map
moves the rays of a raytrace slightly. Say so. Also say that the map is
extrapolated linearly outside of it, and that leaving the map out of the
intercepts moves them along each ray by its height divided by the cosine
of the angle of incidence, rather than by no more than its height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
A misnamed axis, or a str, which was split into its characters, made the
axes of the map look like batch axes of the sag profile, and the error only
came when the height was evaluated, which a raytrace does only on a stop
surface. By default, every axis of the inputs was an axis of the map, so a
separate grid for each channel made the channel an axis of the map too.

Now `axis` must name two distinct axes along which the inputs vary whenever
there is a map, and `shape` fails fast otherwise, as it does for a misnamed
axis of `ZernikeSag`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
@roytsmart
roytsmart merged commit 1430258 into main Oct 31, 2026
12 checks passed
@roytsmart
roytsmart deleted the feature/figure-error branch October 31, 2026 22:05
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.

1 participant