Skip to content

Add grid() interior aperture sampling (toward robust field/pupil sampling)#180

Open
jacobdparker wants to merge 2 commits into
mainfrom
fix/field-grid-sampling
Open

Add grid() interior aperture sampling (toward robust field/pupil sampling)#180
jacobdparker wants to merge 2 commits into
mainfrom
fix/field-grid-sampling

Conversation

@jacobdparker

Copy link
Copy Markdown
Contributor

Summary

Adds a grid() method to RectangularAperture and CircularSectorAperture — the interior counterpart to wire() (which samples the boundary). It maps a grid of normalized coordinates in [-1, 1] onto points that fill the aperture, with transformation respected. This is the building block for sampling a stop's interior with a grid of rays instead of inferring its extent from the boundary.

This is intentionally a small, self-contained primitive PR. The motivation — and the follow-up that consumes it — is below.

Motivation: the field/pupil sampling is fragile

SequentialSystem._denormalize_grid maps the normalized [-1, 1] field/pupil grid onto each stop's field of view by tracing the stop perimeters (wire()) out to object space, taking the min/max to form a bounding box, and placing the element "center" at (min+max)/2:

min_field = field.min(axis=(axis_field, axis_pupil))
ptp_field = field.ptp(axis=(axis_field, axis_pupil))
result.field = ptp_field * (result.field + 1) / 2 + min_field

(min+max)/2 is a 2-point estimator of the center: it hangs off two extreme rays, which are aberration-smeared marginal rays at the aperture corners. The corner that achieves the extreme hops as the element scans the field, so the assigned center jumps discontinuously.

In a multi-slit spectrograph (MUSE), this puts a spurious ~0.3 mÅ kink into the slit-to-slit separation near specific slits — about ⅓ of the science requirement — that is not physical (it's wavelength- and grating-independent and doesn't shrink with denser [-1, 1] sampling).

The fix this enables

Replace the 2-point estimator with an N-point average: sample the stop interiors with a grid (this PR's grid()), use the same secant/Newton solver already in _calc_rayfunction_stops_only to find the ray through each (field-point, pupil-point) pair, and let the center fall out as the centroid of the grid (= the chief ray through the element center, smooth by construction). Averaging over the whole aperture is insensitive to which corner is extreme.

A prototype that swaps wire → grid on the existing solver and takes the centroid drops the per-slit center error from 4.01 maas → 0.00 maas (the kink vanishes entirely, not just shrinks). The machinery is already there — _calc_rayfunction_stops already back-propagates to object space — so the consumer is a focused change; I've kept it out of this PR because routing the default rayfunction through it is a behavior change worth its own review (and touches value-asserting system tests).

Credit to Roy Smart for the grid-sampling framing.

Tests

test_rectangular_aperture_grid and test_circular_sector_aperture_grid (grid points fill the interior). Full optika/apertures/_apertures_test.py passes (20486 tests), ruff clean.

🤖 Generated with Claude Code

…SectorAperture`

`wire()` samples an aperture's boundary; `grid()` is its interior counterpart,
mapping a grid of normalized coordinates in `[-1, 1]` onto points that fill the
aperture.  This is the primitive needed to sample a stop's interior with a grid
of rays -- rather than inferring the field of view from the boundary extremes --
e.g. to robustly center a `SequentialSystem`'s field/pupil sampling.

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

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.31%. Comparing base (a6e6f7f) to head (9b0c605).

Files with missing lines Patch % Lines
optika/apertures/_apertures.py 86.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #180      +/-   ##
==========================================
- Coverage   99.34%   99.31%   -0.03%     
==========================================
  Files         116      116              
  Lines        5983     6019      +36     
==========================================
+ Hits         5944     5978      +34     
- Misses         39       41       +2     
Flag Coverage Δ
unittests 99.31% <94.44%> (-0.03%) ⬇️

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.

Adds test_grid_transformation exercising grid() on a transformed
RectangularAperture and CircularSectorAperture, covering the two lines
codecov flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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