Detector physics ttf crossval - #1
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.