Skip to content

Raise a clear error when Camera.gain is None - #55

Merged
roytsmart merged 1 commit into
mainfrom
raise-when-gain-is-none
Aug 19, 2026
Merged

Raise a clear error when Camera.gain is None#55
roytsmart merged 1 commit into
mainfrom
raise-when-gain-is-none

Conversation

@roytsmart

Copy link
Copy Markdown
Contributor

SensorData.electrons and TapData.electrons failed for any image opened via msfc_ccd.fits.open():

import msfc_ccd
img = msfc_ccd.fits.open(msfc_ccd.samples.path_fe55_esis1)
img.electrons   # TypeError: 'NoneType' object is not subscriptable

Cause

Camera.gain defaults to None and, unlike Camera.sensor (which __post_init__ resolves to TeledyneCCD230()), nothing ever resolves it. AbstractCamera.dn_to_electrons then computed None * a, which yields a TapData whose outputs is None rather than raising. The failure only surfaced later in SensorData.from_taps as TypeError: 'NoneType' object is not subscriptable, pointing at the wrong place entirely.

msfc_ccd.fits.open constructs a default Camera(), so every image loaded the documented way hit this. Passing an explicit gain always worked.

Worth noting the bug was worse than a misplaced traceback: TapData.electrons never raised at all, it returned a TapData whose outputs was None, so anything consuming taps directly got silent corruption.

Fix

dn_to_electrons now raises a ValueError naming the missing parameter before doing any arithmetic:

ValueError: `gain` is `None`, so this camera cannot convert DN into electrons.
Measure the gain of this camera and provide it explicitly, for example
`msfc_ccd.Camera(gain=2.5 * u.electron / u.DN)`.

The alternative was resolving gain in __post_init__ to a measured default for the Teledyne CCD230, mirroring how sensor is handled. That was rejected for now: there is no measured gain anywhere in this repository (the bias and dark-current reports contain none, and the only gain values in the codebase are synthetic test fixtures), and gain is documented as tap-dependent, so a single hardcoded scalar would be wrong for the per-tap case. Baking in a default is a natural follow-up once there is a measurement to bake in.

Docstrings updated to match: Camera.gain now states there is no measured default and that it must be set before dn_to_electrons / .electrons will work, and fits.open's camera parameter notes that the default camera has no gain.

Tests

Nothing previously exercised .electrons on an image from msfc_ccd.fits.open — the existing image tests all build a Camera with an explicit gain fixture.

  • test_fits.py gains test_open_electrons (explicit-gain camera through fits.open, asserting both .taps.electrons and .electrons come back in electron and are nonzero) and test_open_electrons_default_camera (default camera raises on both). The path list is now a shared module-level _paths.
  • AbstractTestAbstractCamera gains test_gain and test_dn_to_electrons, which branch on whether gain is None. TestCamera is now parametrized over both Camera() and a camera with a per-tap gain array, so both branches are covered.

Full suite passes (123 tests); ruff check and black --check are clean.

🤖 Generated with Claude Code

`Camera.gain` defaults to `None` and, unlike `Camera.sensor`, nothing ever
resolves it. `AbstractCamera.dn_to_electrons` then computed `None * a`, which
silently produced a `TapData` whose `outputs` was `None` instead of raising.
The failure only surfaced later in `SensorData.from_taps` as
`TypeError: NoneType object is not subscriptable`, which points at the wrong
place entirely.

Since `msfc_ccd.fits.open` constructs a default `Camera()`, every image loaded
the documented way hit this.

`dn_to_electrons` now raises a `ValueError` naming the missing parameter. A
measured default was considered instead, but there is no measured gain in this
repository yet and the gain is tap-dependent, so a single hardcoded value would
be wrong.

Added tests covering `.electrons` on an image from `msfc_ccd.fits.open`, both
with an explicit gain and with the default camera, since nothing exercised that
path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #55   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           19        19           
  Lines          840       875   +35     
=========================================
+ Hits           840       875   +35     
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 merged commit b3d6fda into main Aug 19, 2026
11 checks passed
@roytsmart
roytsmart deleted the raise-when-gain-is-none branch August 19, 2026 19:14
roytsmart added a commit that referenced this pull request Aug 19, 2026
#55 made `dn_to_electrons` raise a `ValueError` naming the missing parameter,
rather than silently producing an image whose outputs are `None`. Say so, since
a reader who reaches for `.electrons` will now meet that error, and note that
the gain differs from tap to tap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
roytsmart added a commit that referenced this pull request Aug 19, 2026
* Expand the README and the documentation landing page

Both pages described the library in one sentence and showed a single example,
loading a frame and displaying it, which left the reader with no idea that the
package splits a frame into its four taps, measures and removes the bias, or
models the sensor.

Both pages now list the public API, and add a "Key concepts" section covering
what a reader has to know to use the calibration steps: an image pairs pixel
values with the header that describes them, the sensor is read out through four
taps which each have their own bias, the blank and overscan columns are what
measures that bias, and converting to electrons needs a gain that has to be
measured.

The landing page gains three examples: measuring and removing the per-tap bias,
reading the header, and inspecting the sensor model. The README shows the same
examples with their real output. The original example keeps its position, so
the figure link already in the README still resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Describe the error raised when the gain is missing

#55 made `dn_to_electrons` raise a `ValueError` naming the missing parameter,
rather than silently producing an image whose outputs are `None`. Say so, since
a reader who reaches for `.electrons` will now meet that error, and note that
the gain differs from tap to tap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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