Noise and saturation
add_noise_and_digitize() applies the wcc_etc noise
model to the rendered source image, following the ETC’s semantics exactly so
that measured SNRs match ETC predictions.
The model
Starting from the source-only image in electrons:
Sky + dark. Uniform per-pixel rates (zodiacal background and dark current from the ETC) times
exptimeare added, giving the clean expectation imageimage_clean.Saturation cap. The per-frame expectation
image_clean / n_readsis compared against the full-well depth and the ADC ceiling (adc_max × gain); the expectation is capped at the smaller of the two (timesn_reads), and the per-pixel booleansatmaskrecords which pixels hit the limit.Poisson noise on the (capped) expectation.
Read noise, Gaussian with variance scaling as
n_reads(each coadded frame contributes one read).Digitization. Electrons are divided by the gain, the bias level is added, and the result is clipped to
[0, n_reads × adc_max]ADU.
With add_noise=False, steps 3 and 4 are skipped and image_e equals
the capped expectation of step 2 (image_clean where nothing saturates),
which is what a photometric closure test wants.
Three products come out of this: the truth image_clean (noiseless
expectation, e-), the pre-digitization realization image_e (e-, may be
negative) and the delivered frame
image_adu (what the camera would hand you). Photometry only ever sees the
delivered frame, converted back to electrons as (ADU - bias) * gain, so
a SimulatedField and the FITS file written from it give identical
measurements. Note that both WCC sensors carry bias = 0 in the ETC, so
the zero clip removes negative read-noise excursions from the delivered
frame; a low-background sky therefore measures slightly above its
expectation, on every path alike.
n_reads semantics
n_reads follows the ETC convention of coadded frames: the total
exposure time is split into n_reads equal frames that are summed.
Consequences:
saturation is evaluated per frame, so splitting a long exposure into more reads is the way to keep bright stars unsaturated;
read-noise variance grows linearly with
n_reads;the ADU ceiling of the summed image is
n_reads × adc_max.
Per-star saturation flags
The FITS/output catalog carries a per-star saturated column computed by
star_saturated(). It flags a star if any saturated
pixel lies within 32 px of its center. A window is needed, rather than the
central pixel alone, because the defocused PSFs are centrally depressed: a bright star can
saturate its ring while its central pixel stays below full well (the 2-wave
ring extends to ~29 px).
Quick example
from wcc_sim import simulate_field
field = simulate_field(291.0, 44.5, sensorfilter="zwo:r", focus=2,
exptime=90, n_reads=1, seed=42, shape=(1024, 1024))
n_sat_px = field.saturation_mask.sum()
sat_stars = field.catalog[field.catalog["saturated"]]
# Same field, 9 reads: per-frame exposure drops from 90 s to 10 s
field9 = simulate_field(291.0, 44.5, sensorfilter="zwo:r", focus=2,
exptime=90, n_reads=9, seed=42, shape=(1024, 1024))
Every noise-model quantity used is recorded in field.params (and the FITS
header): gain, read_noise, dark_e_s, sky_e_s, well_depth.