Skip to content

Trace once for the distortion, vignetting, and sensor angle - #212

Merged
roytsmart merged 1 commit into
mainfrom
perf/share-fit-trace
Aug 28, 2026
Merged

Trace once for the distortion, vignetting, and sensor angle#212
roytsmart merged 1 commit into
mainfrom
perf/share-fit-trace

Conversation

@roytsmart

Copy link
Copy Markdown
Collaborator

Depends on #211 and must merge after it. Opened against that branch so the diff shows only this change; retargeted to main immediately, so deleting #211's branch on merge cannot close this PR.

Why

linearize fits the distortion and the vignetting from the same rays, traced with identical arguments, and then traced a third time through rayfunction_default for the cosine of the angle at which light strikes the sensor. None of the three reads the intensity.

What changes

distortion and vignetting keep their behaviour; their fitting bodies move into _fit_distortion and _fit_vignetting, so a caller needing both can trace once and fit twice. direction comes from those same rays.

before: 4 raytraces, 4 stop solves, 3.46 s
after:  2 raytraces, 1 stop solve,  2.64 s

with `num_interpolation` set:      1.66 s -> 1.06 s

What moves, and what does not

distortion      20  IDENTICAL
vignetting      10  IDENTICAL
scene            3  IDENTICAL
sensor           2  IDENTICAL
illumination     1  IDENTICAL
direction        1  CHANGED by 2.31%

direction was evaluated on grid_input.wavelength — a single value, 629.77 Å for ESIS — no matter which band was asked for. It is now averaged over the band being linearized. The old value was worse the further the requested band sat from that wavelength:

today (on grid_input.wavelength):  0.880400+0.044822j

if computed on the grid actually being linearized:
   580-640 A: differs by  5.36%
   300-350 A: differs by 12.27%
   900-1000 A: differs by 22.73%

It feeds sensor.expose, which uses it for the path length through the depletion region, so this is a radiometric correction rather than a refactor artifact.

The trap this had to avoid

The existing averaging line reads:

axis_grid = self.axis_wavelength_ + self.axis_field_ + self.axis_pupil_
direction = direction.mean(axis=tuple(ax for ax in axis_grid if ax in na.shape(direction)))

axis_wavelength_ is derived from grid_input, which holds a scalar wavelength for ESIS, so it is (). That was harmless while direction came from rayfunction_default, which has no wavelength axis either. Fed the shared trace, the axis survives:

shared trace, existing line  shape {'wavelength': 9}      <- the landmine
shared trace, wavelength too shape {}                     0.895272+0.030887j

An array direction is not merely different, it is broken. expose indexes it by the cell centers of the scene's wavelength grid, which has nothing to do with the grid being linearized:

   scene 10 edges ( 9 centres) -> NO ERROR              <- silently pairs unrelated wavelengths
   scene  5 edges ( 4 centres) -> ValueError: shapes ({'wavelength': 4}, {'wavelength': 9}) are not compatible
   scene 20 edges (19 centres) -> ValueError: shapes ({'wavelength': 19}, {'wavelength': 9}) are not compatible

So the average now names the wavelength axis explicitly, and test_linearize asserts direction is a scalar.

Worth knowing while reviewing: _linear_test's sensor is an ideal material that ignores direction entirely (9000 photons in, 9000 electrons out, and the mismatched-shape case does not even raise), so none of LinearSystem's own tests exercise it.

Tests

Full suite: 17768 passed, 304 skipped, 17 xfailed. _sequential.py at 100% line coverage. Six ndfilters tests fail with a numba ReferenceError: underlying object has vanished, identically on main — a pre-existing local flake, not from this branch.

🤖 Generated with Claude Code

@roytsmart
roytsmart changed the base branch from refactor/share-stop-solve to main August 27, 2026 23:02
@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 (772540a) to head (8a7083a).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #212   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          121       121           
  Lines         7402      7408    +6     
=========================================
+ Hits          7402      7408    +6     
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.

`linearize` fits the distortion and the vignetting from the same rays,
traced with identical arguments, and then traced a third time through
`rayfunction_default` for the cosine of the angle at which light strikes
the sensor.  None of the three reads the intensity.

Factor the fitting out of `distortion` and `vignetting` into
`_fit_distortion` and `_fit_vignetting`, so a caller needing both can
trace once and fit twice, and take `direction` from those same rays.
The public methods keep their behaviour, they just trace and then call
the fitter.

  before: 4 raytraces, 4 stop solves, 3.46 s
  after:  2 raytraces, 1 stop solve,  2.64 s

  with `num_interpolation` set:        1.66 s -> 1.06 s

The distortion and vignetting models are unchanged bit for bit, along
with the scene, sensor, and illumination arrays they are fit to.

`direction` moves by 2.31%.  It was evaluated on `grid_input.wavelength`,
a single value, 629.77 A for ESIS, no matter which band was asked for; it
is now averaged over the band being linearized.  A caller linearizing a
line far from that wavelength was getting a worse answer than this: 12%
at 300 A, 23% at 950 A.

The average now includes the wavelength axis explicitly rather than
relying on `axis_wavelength_`, which is empty whenever `grid_input` holds
a scalar wavelength and would have let that axis survive into the result.
`direction` has to stay a scalar, since
`AbstractImagingSensor.expose` indexes it by the cell centers of the
*scene's* wavelength grid, which is unrelated to the grid being
linearized: an array either fails to broadcast or pairs up wavelengths
which are not the same.  `test_linearize` now asserts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@roytsmart
roytsmart force-pushed the perf/share-fit-trace branch from fd0e4c4 to 8a7083a Compare August 28, 2026 15:30
@roytsmart
roytsmart merged commit 7d42639 into main Aug 28, 2026
12 checks passed
@roytsmart
roytsmart deleted the perf/share-fit-trace branch August 28, 2026 15:46
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