Skip to content

Add a point-spread function to IdealInstrument, and a tutorial with a different PSF in each channel - #36

Merged
roytsmart merged 4 commits into
mainfrom
feature/psf
Oct 3, 2026
Merged

roytsmart merged 4 commits into
mainfrom
feature/psf

Conversation

@roytsmart

@roytsmart roytsmart commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Adds a point-spread function to IdealInstrument, and a tutorial which inverts a scene blurred by a different PSF in each channel.

psf

IdealInstrument gains two fields:

  • psf: None | na.AbstractScalar = None: the fraction of the light landing in a pixel which reaches each of the pixels around it.
  • axis_psf_xy = ("psf_x", "psf_y"): the axes of psf which run along axis_sensor_xy.

Any other axis of the PSF is broadcast by name:

  • axis_channel or axis_wavelength give a PSF per channel or per wavelength;
  • axis_sensor_xy gives one that varies across the sensor.

IdealInstrument.weights folds the PSF into the weights with na.regridding.convolve_weights() (named-arrays 2.13.0). weights_transpose is built from weights, so image() blurs by the PSF and backproject(), the transpose, accounts for it, with no other changes. MART uses only those two methods, so it models the PSF automatically.

OptikaInstrument doesn't take a PSF: it will compute its own from its optika system.

Requires named-arrays>=2.13.0.

Tutorial: "Invert a Scene Blurred by a Different PSF in Each Channel"

docs/tutorials/psf-mart.ipynb uses the Gaussian scene and the instrument from the MART tutorial, and gives each of the four channels its own elongated elliptical Gaussian PSF, with a different length, width and orientation. It then:

  1. images the scene, comparing each channel without a PSF to what it records;
  2. inverts with MART once with the PSFs modeled and once ignoring them;
  3. plots ⟨χ²⟩ per iteration: modeled it falls below one, and ignored it stalls well above one;
  4. stops the PSF-aware inversion where ⟨χ²⟩ first reaches one (the discrepancy principle), since past that MART fits the noise;
  5. compares the reconstructions and the moment histograms.

Across noise seeds, modeling the PSFs recovers all three moments better. A typical run:

radiance r Doppler shift r Doppler width r
modeling the PSFs 0.96 0.88 0.90
ignoring the PSFs 0.91 0.81 0.86

Tests

  • IdealInstrument with a PSF: every abstract instrument test now also runs on an IdealInstrument with a psf, including flux conservation through the image → backproject → image round trip.
  • test_psf: checks that the per-wavelength image with an asymmetric PSF that differs by channel equals the image without one, convolved channel by channel with scipy.ndimage.convolve. It fails if the PSF is not applied.
  • Locally: the full ctis suite passes (59 tests), and the tutorial executes cleanly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU

… different PSF in each channel.

`IdealInstrument` and `OptikaInstrument` gain `psf`, the fraction of the
light landing in a pixel which reaches each pixel around it, and
`axis_psf_xy`, its axes along `axis_sensor_xy`.  Other axes of the PSF are
broadcast by name, so it may differ per channel or wavelength or vary across
the sensor.  `AbstractLinearInstrument._convolve_psf()` folds it into
`weights` with `na.regridding.convolve_weights()`, so `image()` blurs by it,
and `backproject()`, the transpose built from the same weights, accounts for
it; MART models it with no other change.

The tutorial images the Gaussian scene with a different elliptical PSF in
each channel and compares MART with the PSFs modeled, stopped where the mean
chi squared first reaches one, against MART ignoring them.

Requires named-arrays 2.13.0.

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

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (67c4d92) to head (65d0e1b).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #36   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           24        24           
  Lines          892       918   +26     
=========================================
+ Hits           892       918   +26     
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 and others added 3 commits October 2, 2026 15:56
… the observed images only by the PSF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
…ribe the PSF without reference to charge diffusion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
…mpute its own PSF from its optika system.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqceVSp8s2msxj1Dr9MrGU
@roytsmart roytsmart changed the title Add a point-spread function to the instruments, and a tutorial with a different PSF in each channel Add a point-spread function to IdealInstrument, and a tutorial with a different PSF in each channel Oct 3, 2026
@roytsmart
roytsmart merged commit b69e037 into main Oct 3, 2026
12 checks passed
@roytsmart
roytsmart deleted the feature/psf branch October 3, 2026 00:31
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