Up-the-Ramp Readout Simulation — Liger Detector

[ ]:
from liger_iris_sim.utr.create_ramp import create_ramp

import numpy as np
import matplotlib.pyplot as plt

Detector and readout parameters

[ ]:
np.random.seed(42)

shape = (2048, 2048)
n_reads   = 32
readtime  = 2.7      # seconds between reads
n_channels = 32      # parallel readout channels

gain       = np.ones(shape, dtype=np.float32)
flat       = np.ones(shape, dtype=np.float32)
dark       = np.zeros(shape, dtype=np.float32)   # e-/s
bias       = np.full(shape, 1000.0, dtype=np.float32)  # e-
rn_map     = np.full(shape, 9.0, dtype=np.float32)     # e- RMS
kTC_noise  = 50.0    # e- RMS

# Uniform source: 1000 e-/s everywhere
electron_rate = np.full(shape, 1000.0, dtype=np.float32)

Run the UTR simulation

[ ]:
ramp_data = create_ramp(
    electron_rate,
    readtime=readtime,
    n_reads=n_reads,
    nonlin_coeffs=[1E-5, 1, 0],
    gain=gain,
    flat=flat,
    dark=dark,
    bias=bias,
    kTC_noise=kTC_noise,
    poisson_noise=True,
    read_noise=rn_map,
    convert_to_uint16=True,
    clip_ramps=True,
    max_cores=1,
    n_channels=n_channels,
)

data = ramp_data['data']   # (n_reads, 2048, 2048), uint16
print(f"Ramp shape: {data.shape}, dtype: {data.dtype}")

Single-pixel ramp and fitted slope

The simple slope estimate below is wrong due to saturated pixels and nonlinearity.

[ ]:
cy, cx = shape[0] // 2, shape[1] // 2
t = np.arange(n_reads) * readtime
y = data[:, cy, cx].astype(np.float32)
pfit = np.polyfit(t, y, deg=1)

print(f"Injected rate : 1000.0 e-/s")
print(f"Recovered rate: {pfit[0]:.1f} e-/s  (centre pixel)")

plt.figure(figsize=(7, 4))
plt.plot(t, y, 'o', ms=4, label='reads')
plt.plot(t, np.polyval(pfit, t), '-', lw=1.5, label=f'fit: {pfit[0]:.0f} e/s')
plt.xlabel('Time (s)')
plt.ylabel('Signal (ADU)')
plt.title('Up-the-ramp — centre pixel')
plt.legend()
plt.tight_layout()
plt.show()

Slope map from linear fits

[ ]:
# Vectorised least-squares slope over the ramp axis
t_norm = t - t.mean()
slopes = np.tensordot(t_norm, data.astype(np.float32), axes=([0], [0])) / np.dot(t_norm, t_norm)

fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))

im0 = axes[0].imshow(slopes, origin='lower', cmap='viridis')
plt.colorbar(im0, ax=axes[0], label='Slope (ADU/s)')
axes[0].set_title('Slope map')

axes[1].hist(slopes.ravel(), bins=80, color='C0', edgecolor='none')
axes[1].axvline(1000.0, color='r', lw=1.5, ls='--', label='injected')
axes[1].set_xlabel('Slope (ADU/s)')
axes[1].set_ylabel('Pixels')
axes[1].set_title('Slope distribution')
axes[1].legend()

plt.tight_layout()
plt.show()

print(f"Slope  mean : {slopes.mean():.2f} ADU/s")
print(f"Slope  std  : {slopes.std():.2f} ADU/s")