Note
This page is generated from the Jupyter notebook
notebooks/02_saturation_flag.ipynb in the
repository. You can download it and run it interactively.
Saturation flag demo
Demonstrates the detector saturation flag (ADU clip).
A source saturates when its brightest pixel reaches the detector ADC full scale (adc_max = 2**bit_depth - 1, in ADU). The peak-pixel value combines the source PSF peak, the per-pixel sky background, the per-pixel dark current, and the additive bias.
Key API:
sensor.bit_depth,sensor.bias_level,sensor.adc_maxsim.peak_pixel_fraction()— fraction of total source energy in the brightest pixelsim.get_peak_pixel(time, units='adu')sim.is_saturated(time)
[ ]:
import matplotlib.pyplot as plt
import numpy as np
import wcc_etc
wcc_etc.set_wcc_style()
Build a scene and simulation
A moderately bright star on the ZWO/Sony sensor (16-bit) in the r band.
[ ]:
scene = wcc_etc.get_scene(
name="G5V",
mag=15,
host=None,
background="zodi",
bandpass="johnson_r",
background_prop={"bandpass": "johnson_r", "mag": 22.5},
)
sim = wcc_etc.Simulation.from_sensor_and_scene("sony:r", scene)
Sensor ADC properties
adc_max is the clip ceiling in ADU (u.ct).
[ ]:
print(f"bit_depth : {sim.sensor.bit_depth}")
print(f"adc_max : {sim.sensor.adc_max}")
print(f"bias_level : {sim.sensor.bias_level}")
print(f"gain : {sim.sensor.gain}")
Peak-pixel fraction
The fraction of the total source energy that lands in the single brightest pixel, from the detector-grid PSF render. (Depends on wavelength, f-number, pixel size, and jitter.)
[ ]:
frac = sim.peak_pixel_fraction()
print(
f"peak_pixel_fraction = {frac:.4f} ({100 * frac:.1f}% of the source flux in the brightest pixel)"
)
Single-exposure check
[ ]:
for t in [1, 10, 60]:
peak = sim.get_peak_pixel(t, units="adu")
print(
f"t = {t:>4} s peak = {peak.value:12.1f} ADU saturated = {bool(sim.is_saturated(t))}"
)
Sweep exposure time
get_peak_pixel and is_saturated broadcast over an array of times.
[ ]:
texp = np.logspace(-1, 3, 200) # 0.1 s to 1000 s
peak_adu = sim.get_peak_pixel(texp, units="adu").value
saturated = sim.is_saturated(texp)
adc_max = sim.sensor.adc_max.value
# first exposure time that saturates
t_sat = texp[saturated][0] if saturated.any() else None
fig, ax = plt.subplots(figsize=(7, 4))
ax.loglog(texp, peak_adu, label="peak pixel")
ax.axhline(adc_max, color="crimson", ls="--", label=f"adc_max = {adc_max:.0f} ADU")
if t_sat is not None:
ax.axvline(t_sat, color="k", ls=":", label=f"saturates at ~{t_sat:.2g} s")
ax.set_xlabel("Exposure time [s]")
ax.set_ylabel("Peak pixel [ADU]")
ax.set_title("Brightest-pixel value vs exposure time (G5V, r=15, sony:r)")
ax.legend()
ax.grid(True, which="both", lw=0.3, alpha=0.4)
plt.show()
print(
"saturates at ~ {:.3g} s".format(t_sat)
if t_sat is not None
else "never saturates in this range"
)
Effect of source brightness
Brighter stars (lower magnitude) saturate sooner.
[ ]:
fig, ax = plt.subplots(figsize=(7, 4))
for mag in [12, 15, 18]:
sim.update(source__mag=mag)
peak = sim.get_peak_pixel(texp, units="adu").value
sat = sim.is_saturated(texp)
t_s = texp[sat][0] if sat.any() else None
label = f"r = {mag}" + (f" (sat ~{t_s:.2g} s)" if t_s is not None else " (no sat)")
ax.loglog(texp, peak, label=label)
ax.axhline(adc_max, color="crimson", ls="--", label="adc_max")
ax.set_xlabel("Exposure time [s]")
ax.set_ylabel("Peak pixel [ADU]")
ax.set_title("Saturation onset vs source magnitude (sony:r)")
ax.legend()
ax.grid(True, which="both", lw=0.3, alpha=0.4)
plt.show()
# restore
sim.update(source__mag=15);
Sensor comparison: 16-bit vs 12-bit
The qcmos sensor is 12-bit (adc_max = 4095), so it clips far sooner than the 16-bit ZWO/Sony for the same source — lower full-scale, and a different gain.
[ ]:
sim_q = wcc_etc.Simulation.from_sensor_and_scene("qcmos:r", scene)
for label, s in [("sony:r (16-bit)", sim), ("qcmos:r (12-bit)", sim_q)]:
peak = s.get_peak_pixel(texp, units="adu").value
sat = s.is_saturated(texp)
t_s = texp[sat][0] if sat.any() else None
t_s_str = f"{t_s:.3g} s" if t_s is not None else "no sat in range"
print(
f"{label:>18} adc_max = {s.sensor.adc_max.value:7.0f} ADU saturates at ~ {t_s_str}"
)
[ ]: