Skip to content

Add SimpleDistortionModel, an analytic linear distortion#157

Merged
roytsmart merged 5 commits into
mainfrom
feature/simple-distortion
May 31, 2026
Merged

Add SimpleDistortionModel, an analytic linear distortion#157
roytsmart merged 5 commits into
mainfrom
feature/simple-distortion

Conversation

@roytsmart
Copy link
Copy Markdown
Collaborator

Summary

Adds an analytic (non-fit) distortion model to optika.distortion, alongside the existing polynomial-fit model.

  • AbstractLinearDistortionModel — an affine distortion, distort(c) = matrix @ (c - center) + intercept, with abstract matrix/center/intercept. Because the map is linear, undistort is its exact inverse via matrix.inverse (unlike the approximate inverse fit of PolynomialDistortionModel).
  • SimpleDistortionModel — a concrete linear model parameterized by plate_scale, dispersion, angle, wavelength_ref, and position_ref. It builds the rotation + dispersion + plate-scale na.SpectralPositionalMatrixArray, capturing the distortion of an idealized spectrograph.

Motivation

ctis.instruments.IdealInstrument implements exactly this analytic rotation+dispersion distortion inline. Hosting it in optika (as a concrete AbstractDistortionModel) lets ctis reuse it, complementing the data-fit PolynomialDistortionModel for the raytrace-based interpolated system.

Testing

  • Verified numerically that SimpleDistortionModel.distort reproduces the IdealInstrument formula (~1e-13 pix), carries wavelength through exactly, and round-trips exactly (~1e-13 arcsec).
  • pytest optika/distortion/_distortion_test.py — 42 passed; 100% coverage; black/ruff clean.

🤖 Generated with Claude Code

roytsmart and others added 2 commits May 31, 2026 12:48
Introduce `AbstractLinearDistortionModel`, an affine distortion
(`distort = matrix @ (c - center) + intercept`) whose `undistort` is the
exact inverse via `matrix.inverse`, and a concrete `SimpleDistortionModel`
parameterized by `plate_scale`, `dispersion`, `angle`, `wavelength_ref`, and
`position_ref`. It builds the rotation + dispersion + plate-scale
`SpectralPositionalMatrixArray`, capturing the distortion of an idealized
spectrograph (cf. the ctis `IdealInstrument` distortion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`SimpleDistortionModel` now takes one `reference: SpectralPositionalVectorArray`
(the reference wavelength + the sensor position the field center maps to)
instead of separate `wavelength_ref`/`position_ref` fields; `intercept` is then
just `reference`. The class example now distorts a grid and scatter-plots the
result on the sensor, colored by wavelength.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.31%. Comparing base (4b67d7f) to head (c200ac2).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #157   +/-   ##
=======================================
  Coverage   99.31%   99.31%           
=======================================
  Files         114      114           
  Lines        5671     5722   +51     
=======================================
+ Hits         5632     5683   +51     
  Misses         39       39           
Flag Coverage Δ
unittests 99.31% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 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 and others added 3 commits May 31, 2026 13:43
`na.plt.scatter(c=...)` lets matplotlib autoscale a colormap `Normalize`,
which calls `float()` on the color values; a `Quantity` wavelength raises
"only dimensionless scalar quantities can be converted to Python scalars"
when the figure is rendered (as it is by jupyter-execute, breaking the Read
the Docs build). Pass `sensor.wavelength.to_value(u.nm)` so `c` is unitless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increase the example dispersion to 2 nm/pix so the wavelength channels are
less spread out (the previous 0.1 nm/pix dispersed them ~±500 px versus the
~±10 px field), and label the detector x/y axes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scatter one series per wavelength (selected with a `where` mask) so each gets
its own color and a legend entry, instead of an unlabeled color mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@roytsmart roytsmart merged commit c3d1e62 into main May 31, 2026
12 checks passed
@roytsmart roytsmart deleted the feature/simple-distortion branch May 31, 2026 22:00
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