Skip to content

Detector physics ttf crossval - #1

Merged
jacotay7 merged 4 commits into
mainfrom
detector-physics-ttf-crossval
Jul 30, 2026
Merged

Detector physics ttf crossval#1
jacotay7 merged 4 commits into
mainfrom
detector-physics-ttf-crossval

Conversation

@jacotay7

Copy link
Copy Markdown
Owner

No description provided.

jacotay7 and others added 4 commits July 29, 2026 12:18
…darks

Cross-validated the KURO 1200B, Prime 95B and Marana 4.2B-11 presets against
real dark stacks (test_ttf/) and fixed what did not hold up.

The bug: the per-pixel read-noise RMS map implied by read_noise_nonuniformity
was drawn from the *per-frame* generator, so it was re-randomised every frame.
Single-frame spatial statistics looked right, which is why it went unnoticed,
but every pixel then had the same expected noise *through time* -- and an sCMOS
pixel's read noise is a fixed property of its own source-follower and column
ADC. Build the map once from fixed_pattern_seed and cache it in
FixedPatternMaps, alongside PRNU and DSNU.

The discriminating measurement: split a dark stack in half and correlate the
two per-pixel temporal-variance maps. Real detectors give r = 0.89-0.94; the
old model gave r = 0.004; it is now r ~ 0.96. This changes generated pixel
values wherever read_noise_nonuniformity > 0.

Also added, both defaulting to off:

- read_noise_rts_fraction / read_noise_rts_factor, a second noisier read-noise
  population for random-telegraph-signal pixels. ~0.5% of pixels on all three
  real sensors sit above 3x the median read noise where a single log-normal
  predicts ~0.01%, and those are the pixels that limit faint-source detection.
- detector_glow_edge_scale_px, making detector glow edge-concentrated with an
  exponential falloff rather than uniform, since amplifier glow is emitted at
  the array periphery. The Marana shows this clearly (measured 37 px scale).

The three presets now carry measured gain, read noise, dark current, bias and
non-uniformity terms instead of datasheet values. Biggest corrections:
conversion gain 1.25-1.3 -> 0.77-0.87 e-/ADU (the low-signal leg of these
dual-gain modes, which is the regime darks probe), and
dark_current_nonuniformity 0.03 -> 0.11-0.33, which was about an order of
magnitude too low. Other sCMOS presets likely share that error.

With no overrides, the updated presets now reproduce the measured variance-vs-
exposure curve to 2.9% (Kuro), 6.1% (Prime 95B), 6.1% (Marana), and the
pixel-to-pixel spread to within 10% on the two Teledynes. The Marana's spread
is still short at long exposure: it has non-Poisson excess noise in its glow
regions that a single exponential edge term does not capture.

Investigated and deliberately not added: a frame-to-frame bias pedestal drift
term. The apparent 0.59 ADU wander was an artifact of taking the median of
quantised integer-ADU data; measured from the per-frame spatial mean it is
0.01-0.04 ADU, and where it is larger it varies ~50x between runs, so it is
acquisition thermal instability rather than a detector property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every sCMOS preset carried dark_current_nonuniformity of 0.02-0.03, or omitted
it entirely. Measured against real dark stacks, three back-illuminated sCMOS
cameras came out at 0.11 (Marana 4.2B-11), 0.23 (KURO 1200B) and 0.33
(Prime 95B) -- roughly an order of magnitude higher, consistently.

Set the five uncharacterised sCMOS presets to 0.23, the median of those three.
Each preset carries a banner saying the value is carried over from
characterised hardware rather than taken from that camera's datasheet, so
nobody mistakes it for a specification.

Note this is an inference across sensors, not a measurement of these cameras;
the three measured devices all use the same 11 um back-illuminated family, so
smaller-pixel parts (ORCA-Quest 2 at 4.6 um, ORCA-Fusion at 6.5 um) may differ.
It is still a much better default than a value known to be ~10x low.

Also guard the class of error with a test asserting every sCMOS preset has a
DSNU of at least 0.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-on to the DSNU work, applying the same reasoning to the terms I had
flagged but left alone.

Presets:

- The four conventional sCMOS presets (generic_scmos, hamamatsu_orca_fusion,
  tucsen_aries_6504_pro, andor_cb1_0_5mp) gain the measured RTS population,
  read_noise_rts_fraction = 0.016 / factor 2.65. All three characterised
  sensors put 0.4-0.5% of pixels above 3x the median read noise where a bare
  log-normal predicts ~0.01%, and those pixels set the faint-source detection
  floor, so omitting them makes a simulated sensor optimistic at threshold.
- andor_cb1_0_5mp and hamamatsu_orca_quest_2 had no read_noise_nonuniformity at
  all, i.e. a perfectly uniform read noise, which is the one thing an sCMOS is
  not. Both now carry 0.2, marked as a generic default.
- hamamatsu_orca_quest_2 deliberately gets NO RTS population, and says why in
  the file: photon-number resolution depends on a tightly screened read-noise
  distribution, so importing a tail measured on conventional 11 um
  back-illuminated sCMOS would misrepresent it.
- andor_marana_4_2b_11 gains its measured hot-pixel population, 1.0e-4 of
  pixels above 10x the median dark rate.

Docs and tests:

- docs/guides/validation.md gains a section on validating a preset against your
  own detector: how to measure conversion gain from darks alone (no flats
  needed -- dark charge is Poisson, so it works as the PTC charge source), and
  the split-half test for repeatable per-pixel read noise.
- test_dark_ptc_recovers_gain_without_any_illumination pins that estimator
  against a camera whose gain is known, so the method used to characterise the
  three presets from real hardware is itself covered in CI. It also checks the
  recovered gain leaves the electron statistics Poisson, which is the
  assumption the whole technique rests on.

Re-validated against the real dark stacks: unchanged at 2.9% / 6.1% / 6.1%
median variance error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`analysis/ptc.py` characterises a *simulated* camera by driving it. The new
`analysis/characterize.py` works the other way round: give it stacks of frames
that already exist -- raw data off a real detector, or output from Camera --
and it measures the detector parameters back out.

  frames  ->  stack_statistics    per-pixel temporal mean and variance
          ->  characterize_dark   gain, read noise, dark current, bias, DSNU
          ->  to_config           a CameraConfig
          ->  Camera              synthetic frames matching your detector

`stack_statistics` reduces any iterable of frames -- arrays, Frames, a 3-D cube,
a dark_series generator, your own file reader -- through a Welford accumulator,
so stacks far larger than memory stream fine. `characterize_dark` measures gain,
read noise (with its per-pixel map, log-normal width and RTS tail), dark
current, bias and DSNU from darks alone: no flat field is needed, because dark
charge is Poisson and so serves as the PTC charge source. `characterize_flat`
adds full well, PRNU and linearity. `DarkCharacterization.to_config()` returns a
CameraConfig, which closes the loop the library was built for -- measure a real
camera, then simulate it.

Two estimator choices worth recording, both settled by measurement rather than
by which is more standard:

- Read noise comes from the shortest stack with its dark term subtracted, not
  from the variance regression extrapolated to zero exposure. Both are unbiased
  in the median, but the extrapolation carries every pixel's fit error into the
  read-noise map: on a known camera it returned a log-normal width of 0.34
  against a true 0.25, where this form returns 0.26.
- Flat read noise likewise comes from the faintest stack with shot noise removed
  rather than the PTC intercept: 5.07 e- against a true 5.0, where the intercept
  gave 7.4.

`temporal_repeatability` (the split-half test) clips the most extreme 1% of
pixels. Running the backend against the real TTF frames is what forced this: a
cosmic ray lands in one half only and inflates that pixel's variance by orders
of magnitude, so a handful of them dominate the covariance. Real 60 s Marana
darks score 0.006 unclipped against 0.96 clipped -- a user would have concluded
the detector has no fixed read-noise structure, which is false.

Validated both ways. Against simulated cameras with known parameters, every
parameter recovers within 3-15% (tests/test_characterize.py). Against the real
KURO/Prime 95B/Marana dark stacks, it reproduces the bespoke per-pixel analysis
those presets were built from to 0.0% on gain, dark current, bias and DSNU.

Ships with docs/guides/characterization.md and
examples/15_detector_characterization.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jacotay7
jacotay7 merged commit 01cde85 into main Jul 30, 2026
14 checks passed
@jacotay7
jacotay7 deleted the detector-physics-ttf-crossval branch July 30, 2026 00:01
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