Signal-to-noise and integration time

The signal-to-noise calculation lives in AnalyticCoronagraphNoiseModel, the default noise_model of a Simulation. It is an analytic photometric model that works on the per-second electron rates returned by count_rate_components(), all evaluated in the same photometric aperture of \(N_\mathrm{pix}\) detector pixels (Sensor.num_psf_pixels, see Concepts and architecture).

The noise budget

For an on-target integration \(t\) read out in frames of duration \(t_\mathrm{frame}\), the signal is the planet rate times the integration time and the noise variance, in electrons squared, is

\[\sigma^2 = \left(R_\mathrm{source} + R_\mathrm{zodi} + R_\mathrm{exozodi} + R_\mathrm{speckle}\right) t + R_\mathrm{dark}\, N_\mathrm{pix}\, t + \sigma_\mathrm{read}^2\, N_\mathrm{pix}\, N_\mathrm{reads} + \kappa^2\, \mathrm{var}_\mathrm{ref} + (M\, t)^2,\]
\[\mathrm{SNR} = \frac{R_\mathrm{source}\, t}{\sigma}.\]

The terms, in order:

  • Photon noise of the planet, the zodiacal and exozodiacal backgrounds, and the residual starlight (speckles). Each is a rate times the integration time.

  • Dark current, \(R_\mathrm{dark}\) per pixel, over the aperture.

  • Read noise, \(\sigma_\mathrm{read}\) per pixel per read, over the aperture and over \(N_\mathrm{reads} = \lceil t / t_\mathrm{frame} \rceil\) reads: whole frames plus one shorter final frame, and never fewer than one. An integration shorter than a frame is still one read.

  • Reference subtraction, present only with reference differential imaging (RDI): the scaled random variance of the subtracted reference image, \(\kappa = 1 / (\beta\,\tau)\), where \(\beta\) is the reference-to-host brightness ratio and \(\tau = t_\mathrm{ref} / t\) the reference-to-science time ratio. \(\mathrm{var}_\mathrm{ref}\) holds the reference’s speckle (scaled by \(\beta\)), zodi, exozodi, dark and read terms over \(t_\mathrm{ref}\). See the next section.

  • The post-processed speckle floor, a systematic term with \(M = R_\mathrm{speckle} / \mathrm{ppgain}\) (the coronagraph’s noise floor factor). ppgain is an assumed suppression factor for the residual speckle pattern; the ETC does not predict post-processing performance.

The floor term grows linearly with time, exactly as the signal does, so at long integrations the SNR stops improving and tends to

\[\mathrm{SNR}_\mathrm{max} = \frac{R_\mathrm{source}}{M} = \mathrm{ppgain} \times \frac{R_\mathrm{source}}{R_\mathrm{speckle}}.\]

At the default configuration the planet and speckle rates are equal, so \(\mathrm{SNR}_\mathrm{max}\) is the ppgain of 10. The ceiling moves with a lower raw_dh_contrast (fewer speckle photons) or a higher ppgain; see Coronagraph and contrast.

Observing strategy

coronagraph.subtraction states what is assumed:

  • "none" (default): one science image; the budget above without the reference term.

  • "rdi": reference differential imaging. A reference star ref_brightness_ratio times brighter than the host is observed for ref_time_ratio times the science time, scaled to the target and subtracted. Roll subtraction and angular differential imaging are not modelled.

total_time is always the on-target science time. The result also reports science_time, charged_time (the total telescope time, science plus reference for RDI) and reference_noise_e, the reference’s contribution in electrons:

sim.update(coronagraph__subtraction="rdi",
           coronagraph__ref_brightness_ratio=5.0,   # reference 5x brighter
           coronagraph__ref_time_ratio=0.5)         # observed for half the time
r = sim.get_snr(total_time=3600, frame_time=100)
r["reference_noise_e"], r["charged_time"]          # -> ..., 5400 s

Computing SNR

result = sim.get_snr(total_time=3600, frame_time=100)
result["snr"]        # signal-to-noise ratio
result["signal_e"]   # planet electrons in total_time
result["noise_e"]    # total noise (electrons)
result["M"]          # post-processed speckle floor rate
result["reference_noise_e"], result["science_time"], result["charged_time"]

Inverting for integration time

get_int_time_for_snr() solves the same expression for the integration time needed to reach a target SNR at a given frame_time:

inv = sim.get_int_time_for_snr(snr=10, frame_time=100)
inv["int_time"]             # whole frames: the shortest realizable schedule
inv["int_time_continuous"]  # closed-form inversion with fractional reads
inv["noise_terms"]          # [dark, read, zodi, exozodi, speckle, reference] rates

int_time rounds the closed-form solution up to a whole number of frames, so it is always achievable and meets the target SNR; int_time_continuous is the unrounded value. Non-positive times, target SNRs or ppgain, and a negative contrast, raise ValueError; a frame_time above saturation_time() emits a warning.

Important

A target SNR above \(\mathrm{SNR}_\mathrm{max}\) cannot be reached by integrating longer; those entries come back as nan. Lower raw_dh_contrast or raise ppgain to lift the ceiling.

Saturation

frame_time is bounded from above by detector saturation and from below by read noise, which grows with the number of reads. Saturation explains how saturation_time() and is_saturated() bracket that choice:

sim.saturation_time()         # seconds
sim.is_saturated(100)         # True / False

Swapping the noise model

The noise model is a constructor argument, so you can pass your own object with snr and int_time_for_snr methods (for instance an image-based model) through Simulation.from_components.