Average the effective area over the field of view, not past it - #207
Merged
Conversation
`area_effective` ends with a bare `area_eff.mean(axis_field)`, which includes field positions lying entirely outside the field stop and contributing nothing. Three lines up, `vignetting` does the opposite: it normalizes its illumination with `where=unvignetted.any(axis_pupil)`, counting only the positions inside. `LinearSystem` multiplies the two together, and that only reconstructs the effective area at a field position if both are taken over the same set of positions. The per-field effective area is proportional to the unvignetted fraction to within 0.25% here, so any choice of average works, but the two have to make the same one. They did not, so the product came out low by the illuminated fraction of whatever field grid happened to be used. Use the same set in both. The correction is not a constant: the illuminated fraction varies with wavelength, so the shape of the effective-area curve was wrong as well as its scale. Across the ESIS band the ratio runs 580 AA 1.862 610 AA 1.247 640 AA 1.424 and the result no longer depends on how much empty field was sampled, giving 0.6845, 0.6785, 0.6787 mm^2 at 5x5, 11x11 and 21x21 field grids where it previously gave 0.29, 0.49, 0.67 and 0.77. `AbstractVignettingModel` still described its illumination as normalized to one at the center of the field of view, which is how #159 described it before #184 implemented the average instead. Say what the code does. Averaging was reconsidered here against normalizing at the center and kept: the center is a single field position, so it inherits the whole of the pupil jitter, 6.4% standard deviation against 0.7% for the average. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #207 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 121 121
Lines 7272 7303 +31
=========================================
+ Hits 7272 7303 +31
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
area_effectiveends with a barearea_eff.mean(axis_field), which averages over every field position in the grid, including the ones entirely outside the field stop that contribute zero. Three lines up,vignettingdoes the opposite, normalizing its illumination withwhere=unvignetted.any(axis_pupil).LinearSystemmultiplies the two (_linear.py:251), and the product only reconstructs the effective area at a field position if both factors are taken over the same set of positions.Why any average works, as long as it is the same one
Writing
afor the per-field effective area andvfor the unvignetted fraction,a = k(λ)·vto within a quarter of a percent:So for any field functional
L, the productL[a]·v/L[v]equalsaexactly. Mean, masked mean, center, max: all correct, provided both factors use the same one. They didn't, and the product came out low by the illuminated fraction of whichever field grid was used.Effect
The illuminated fraction varies with wavelength, so this was not a constant scale error. The spectral shape was wrong too:
No constant calibration factor could have absorbed that, and the band edges were worst.
The result also stops depending on how much empty field was sampled:
Averaging vs normalizing at the center
Reconsidered here, and the average is kept. The center is a single field position, so it inherits the entire pupil-jitter noise from the
random=Truecell centers, with no other samples to average against. Eight draws on an 11×11 field:Nine times noisier. The jitter and the average belong together. (An illumination-weighted average with no hard mask was also tried: same noise, slightly worse field convergence, so not worth the complexity.)
AbstractVignettingModelstill described its illumination as "normalized to one at the center of the field of view", which is how #159 described it before #184 implemented the average. Updated to say what the code does.Tests
test_area_effective_ignores_field_outside_the_field_of_viewextends a field grid with samples far outside the field stop and asserts the effective area is unchanged. Verified to fail onmain, where the extra dark samples halve the result (ratio 0.51 against 1.0000 with the fix). The tolerance isrtol=0.05becausearea_effectivetraces at randomly placed cell centers, so repeated calls differ by about a percent.Full suite passes: 17740 passed, 304 skipped, 17 xfailed.
Not in this PR
The remaining problems are in the pupil, not the field, and are independent of this one:
linearizehands the two models different pupil grids, so theirvdisagrees by up to 31% even at 90-100% of peak illumination. It is coarse-grid quantization: ~30 unvignetted samples out of ~100 pinsvto only about ±10% per field position.🤖 Generated with Claude Code