Skip to content

Average the effective area over the field of view, not past it - #207

Merged
roytsmart merged 1 commit into
mainfrom
fix/area-effective-field-mean
Aug 27, 2026
Merged

Average the effective area over the field of view, not past it#207
roytsmart merged 1 commit into
mainfrom
fix/area-effective-field-mean

Conversation

@roytsmart

Copy link
Copy Markdown
Collaborator

area_effective ends with a bare area_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, vignetting does the opposite, normalizing its illumination with where=unvignetted.any(axis_pupil).

LinearSystem multiplies 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 a for the per-field effective area and v for the unvignetted fraction, a = k(λ)·v to within a quarter of a percent:

  wavelength 0:  a/v over 77 illuminated field points   spread=1.01x   std/mean=0.21%
  wavelength 1:  a/v over 77 illuminated field points   spread=1.01x   std/mean=0.25%
  wavelength 2:  a/v over 61 illuminated field points   spread=1.00x   std/mean=0.10%

So for any field functional L, the product L[a]·v/L[v] equals a exactly. 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:

wavelength (AA): [580. 588. 595. 602. 610. 618. 625. 632. 640.]
before         : [0.3696 0.655  0.781  0.8851 1.0149 1.1603 1.2811 1.4027 1.3452]
after          : [0.688  0.8615 0.9742 1.1041 1.266  1.4473 1.5981 1.7498 1.9149]
after / before : [1.862  1.315  1.247  1.247  1.247  1.247  1.247  1.247  1.424]

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:

             before                      after
  5x5    0.29 ->                    0.6845
 11x11   0.49 ->                    0.6785
 21x21   0.67 / 0.77 ->             0.6787

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=True cell centers, with no other samples to average against. Eight draws on an 11×11 field:

  centre         mean=1.0359  std/mean=6.42%  ptp/mean=21.45%
  masked_mean    mean=1.0334  std/mean=0.69%  ptp/mean= 2.44%

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.)

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. Updated to say what the code does.

Tests

test_area_effective_ignores_field_outside_the_field_of_view extends a field grid with samples far outside the field stop and asserts the effective area is unchanged. Verified to fail on main, where the extra dark samples halve the result (ratio 0.51 against 1.0000 with the fix). The tolerance is rtol=0.05 because area_effective traces 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:

  • linearize hands the two models different pupil grids, so their v disagrees by up to 31% even at 90-100% of peak illumination. It is coarse-grid quantization: ~30 unvignetted samples out of ~100 pins v to only about ±10% per field position.
  • The pupil quadrature does not settle. Effective area at field center by pupil cells: 1.294 at 4×4, 1.445 at 10×10, 1.497 at 20×20, 1.497 at 40×40, 1.449 at 80×80. A square grid over a round stop with cells counted whole or not at all, so O(1/N) with oscillating sign.

🤖 Generated with Claude Code

`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

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

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

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     
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 roytsmart closed this Aug 27, 2026
@roytsmart roytsmart reopened this Aug 27, 2026
@roytsmart
roytsmart merged commit c32ab6d into main Aug 27, 2026
21 checks passed
@roytsmart
roytsmart deleted the fix/area-effective-field-mean branch August 27, 2026 03:36
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