Skip to content

Outline the beam on any surface of a sequential system - #258

Merged
roytsmart merged 5 commits into
mainfrom
feature/beam-footprint
Oct 12, 2026
Merged

roytsmart merged 5 commits into
mainfrom
feature/beam-footprint

Conversation

@roytsmart

@roytsmart roytsmart commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Adds AbstractSequentialSystem.beam_footprint(index, wavelength=None, samples_field_stop=401, samples_pupil_stop=401). It returns the outline of the beam on one surface of a system, meaning the convex hull of where the light that reaches the end of the system crosses that surface. The result is a PolygonalAperture in the surface's local coordinates. An aperture with a transformation of its own gives its vertices before that transformation, so on a stop the outline matches aperture.transformation(aperture.vertices).

The ESIS instrument paper needs it to draw each channel's footprint on the primary mirror (esis-mission/esis-instrument-paper#18). Baffles need the same outline at each baffle.

The hull comes from na.geometry.convex_hull (sun-data/named-arrays#270), released in named-arrays 2.15, so pyproject.toml now requires named-arrays~=2.15.

f1d2e83 fixes what a review found.

How

The outline is the hull of two sets of rays, each traced through the whole system, keeping only those that reach its end:

  • The rays along the edges of the stops: the rays that graze the edges of both stops, solved for as rayfunction_stops solves them, at as many samples along each edge as asked for.
  • The rays of the default grid: the rays of grid_input, at wavelength, traced as raytrace traces them.

Why both:

  • Where the stops bound the beam, every ray that reaches the end passes inside both stops, and the outermost of them pass through both edges. So the rays along the edges find the outline, much more sharply than a grid (see below).

  • Clipping: where another aperture cuts the beam, the corners it makes where its edge crosses the outline of the beam are found from the rays along the edges too, to within the spacing of the samples.

  • What the edges miss, the grid fills in, to within its spacing:

    • a corner of another aperture that lies inside the beam;
    • a beam that another aperture, smaller than the beam everywhere, bounds instead of the stops, so that it blocks every ray along their edges.

    So the outline always holds every ray of the grid that reaches the end, and its vertices are NaN only where no ray arrives.

  • Rounding at the edges: each ray along the edges lies on the edge of both stops, where rounding would block about half of them. Both stops are therefore left open for that trace, the way _surfaces_open leaves the field stop open.

    • The logic for opening a surface moves into _surfaces_with_open(surfaces, indices), which both use; _surfaces_open is unchanged.
    • With more than one surface marked as either stop, the stops left open are the pair the solve finishes on. The new _pairs_stops(surfaces) gives the pairs, and the solve in _calc_rayfunction_stops_only now iterates over them, so the two can't disagree.

Why not a grid of rays alone

A grid over the field and the pupil finds the corners of the hull only to within its spacing. That spacing shrinks as the fourth root of the number of rays; along the edges of the stops it shrinks as the square root. For the ESIS footprints on the primary mirror:

rays how short of a 1-billion-ray grid's hull time
1.2 M 11² field × 41² pupil grid 2.0 mm, 5% of the area 2.5 s
66 M 41² × 81² grid 0.5 mm, 1% 2 min
1.0 G 81² × 161² grid (reference) 28 min
1.0 M 401 × 401 along the stop edges contains it to 3 µm, and is 0.8–1.0% larger 4 s

The grid's hull falls short of the true one. The edge outline reaches 0.3–0.5 mm beyond the billion-ray hull, which is about what that hull's own convergence implies it is short by. Between 401 × 401 and 401 × 1001 edge samples, the outline moves by less than 0.08 mm.

Limitations

  • Where the stops don't bound the beam, the outline is only as close to it as the default grid is fine. This covers an aperture smaller than the beam everywhere, or a corner of another aperture inside the beam. One of the systems in TestSequentialSystem is like this: its object aperture, 10 mm, is inside the ±20 mm field of its marked field stop.
  • Curved cuts: where an aperture other than the stops cuts the outside of the beam along a curve, the outline cuts straight across from one corner to the next. Straight-edged apertures, like all of ESIS's, are outlined exactly.
  • Uncertain systems: na.geometry.convex_hull doesn't support uncertain components yet, so neither does this. That belongs in named-arrays.
  • Cost: each call solves the stops and traces the whole system again, so outlining every surface repeats that work for each. Baffles can add a way to outline every surface at once when they need it.

Tests

  • test_beam_footprint (abstract, every test system), on every surface:
    • checks the type and the axes;
    • checks that the outline is a number, since light reaches every surface of these systems;
    • checks that the outline holds every ray of the default grid that reaches the end.
  • test_beam_footprint_holds_rays_between_the_samples: this runs on the four systems in which nothing but the stops blocks a ray. On every surface, the outline holds every ray of a 6² field × 30² pupil grid that reaches the end. The tolerance is the sagitta of the chords that 21 samples cut across a round stop. This grid's rays on and near the edges fall between the samples, where the default grid's land on them.
  • test_beam_footprint_on_a_stop_is_the_stop: this uses a system with rectangular stops and no other apertures, one stop off its center. On both stops, the outline equals the stop, compared by support function over a fan of directions. The system is moved rigidly first, so an outline left in the wrong frame fails.
  • test_beam_footprint_is_cut_by_an_aperture_with_straight_edges: this uses the Newtonian test telescope, with a straight-edged obscuration that cuts off one side of the beam.
    • On every surface, the outline holds every ray of a 9² × 33² grid that reaches the end, to the sagitta of 401 samples, and reaches no more than 10% past them.
    • On the obscuration, the corners of the cut lie within 5 µm of its edge.
  • test_beam_footprint_holds_a_corner_inside_the_beam: this moves the Newtonian telescope's sensor so that its corner lies at the center of the image. The outline on the sensor then holds every ray of the default grid, including those at the corner.
  • test_beam_footprint_opens_the_stops_the_solve_finishes_on: this marks the Newtonian telescope's primary as a pupil stop too, and shrinks the last-marked pupil stop to 5 mm. The outline on that stop stays inside it.
  • test_beam_footprint_at_other_wavelengths: this uses the grazing spectrograph. Outlines at three wavelengths along an axis of their own each match the outline of the same system with its default grid at that wavelength, and the grating moves them across the sensor.
  • Mutation checks: each of these breaks at least one test:
    • leaving out the grid;
    • tracing the grid at the default wavelength;
    • opening the last-marked stops;
    • leaving the stops closed;
    • opening only the field stop;
    • reporting in the global frame;
    • an off-by-one surface index;
    • dropping the vignetting mask.
  • Locally: ruff and black pass, and the optika suite passes against named-arrays 2.15.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP

roytsmart and others added 2 commits October 11, 2026 21:11
`AbstractSequentialSystem.beam_footprint(index, wavelength, samples_field_stop, samples_pupil_stop)`
gives the convex hull of where the light which reaches the end of the
system crosses one of its surfaces, as a `PolygonalAperture` in the
local coordinates of that surface.

The hull is that of the rays which graze the edges of both stops,
solved for as they are for `rayfunction_stops` and traced through the
whole system with both stops left open, since rounding would block
about half of the rays on their edges. A grid of rays finds the corners
of the hull only to within its spacing, which shrinks as the fourth
root of the number of rays, against the square root along the edges.

The logic which opens a surface moves from `_surfaces_open` into
`_surfaces_opened`, which both use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
`beam_footprint` needs `na.geometry.convex_hull`, from
sun-data/named-arrays#270, which is not released yet. Go back to
`named-arrays~=2.15` once it is.

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

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4ec2592) to head (f1d2e83).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main      #258    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          129       129            
  Lines        10031     10181   +150     
==========================================
+ Hits         10031     10181   +150     
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.

So that the transformation of a stop's own aperture is carried into the
outline on it, and the branch of the test which applies it runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
roytsmart and others added 2 commits October 12, 2026 13:18
It was released with sun-data/named-arrays#270, so `pyproject.toml` no
longer needs to take named-arrays from that PR's branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
- The outline is the hull of the rays of the default grid as well as of
  the rays along the edges of the stops. The rays along the edges missed
  a corner of another aperture which lies inside the beam, and an
  aperture smaller than the beam everywhere blocked all of them, which
  left an outline of NaN although light reached the end. The grid fills
  both in, to within its spacing, so the outline now holds every ray of
  the grid which reaches the end, and is NaN only where none does.
- With more than one surface marked as either stop, the stops left open
  are the pair the solve finishes on, which `_pairs_stops` now gives to
  both. They used to be the last surface marked as each, which can be a
  different pair, so the stop the rays were solved on stayed closed and
  another was opened.
- The docstring says the outline is in the local coordinates of the
  surface, which differ from those an aperture with a transformation of
  its own gives its vertices in.
- `_surfaces_opened` is renamed `_surfaces_with_open`, and takes the
  surfaces its caller has already built.
- The tests allow for the chords between the samples along the edge of a
  round stop by their sagitta, rather than by a tolerance smaller than it
  which passed only because the default grid's rays on the edges land on
  samples. A new test traces a grid whose rays fall between the samples.
- The abstract test requires every outline to be a number, rather than
  allowing NaN for every system.
- New tests cover a corner of the sensor inside the beam, a second pupil
  stop, and outlines at other wavelengths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
@roytsmart
roytsmart marked this pull request as ready for review October 12, 2026 21:11
@roytsmart
roytsmart merged commit bb8b649 into main Oct 12, 2026
12 checks passed
@roytsmart
roytsmart deleted the feature/beam-footprint branch October 12, 2026 21:12
roytsmart added a commit to esis-mission/esis-instrument-paper that referenced this pull request Oct 12, 2026
The footprints on the primary mirror were the hull of a grid of 1.2
million rays, which fell short of their true outline by up to 2 mm and
rounded off its corners. `beam_footprint` (sun-data/optika#258, released
in optika 3.2) outlines them from the rays along the edges of the field
stop and the grating, which converge to within 0.1 mm for about as many
rays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYjDL98znSud1yFh9chnkP
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