Field of Stars — Liger Imager

[ ]:
from liger_iris_sim.sources import make_point_source_image
from liger_iris_sim.expose import compute_throughput, expose_imager
from liger_iris_sim.utils import LIGER_PROPS, compute_filter_photon_flux, get_psf
from liger_iris_sim.sky import get_maunakea_sky_background
from liger_iris_drp_resources.filters import load_filters_summary

import numpy as np
import matplotlib.pyplot as plt

Instrument and exposure parameters

[ ]:
instrument_name = 'Liger'
instrument_mode = 'img'          # imager mode
filt = 'J'
size = (2048, 2048)     # detector size in pixels
read_noise = 9        # e- RMS
resolution = 4000
dark_current = 0.025  # e- / sec / pixel
scale = 0.01          # arcsec / pixel
itime = 300           # integration time, sec
n_frames = 1          # number of coadded frames
collarea = LIGER_PROPS['collarea']  # m^2

Load filter data and compute throughput

load_filters_summary returns filter metadata including the central wavelength, zero-point flux, and background magnitude.

compute_throughput folds together telescope, AO, filter, and instrument throughput.

[ ]:
filter_info = load_filters_summary(filter_name=filt)

tput = compute_throughput(
    instrument_name=instrument_name,
    instrument_mode=instrument_mode,
    wave=filter_info['wavecenter'],
    tel=0.8, ao=0.65, filt=filt
)
print(f"Total throughput for {instrument_mode} mode at {filter_info['wavecenter']:.3f} µm: {tput:.3f}")

Get the PSF

get_psf returns the PSF at a given wavelength and field position binned on the detector or lenslet/slicer grid.

[ ]:
psf, psf_info = get_psf(
    instrument_name=instrument_name,
    instrument_mode=instrument_mode,
    wave=filter_info['wavecenter'],
    xs=0, ys=0,
    output_plate_scale=scale
)
print(f"PSF shape after rebinning: {psf.shape}")

Create a field of stars

compute_filter_photon_flux converts a magnitude to photons / sec / m² using the filter zero-point. make_point_source_image places each star into the source rate image, convolved with the PSF.

[ ]:
np.random.seed(1)
n_stars = 10
mag_range = (18, 19)

source_rate = np.zeros(size, dtype=np.float32)

for i in range(n_stars):
    mag = np.random.uniform(mag_range[0], mag_range[1])
    xpix = int(np.random.uniform(5, size[1] - 5))
    ypix = int(np.random.uniform(5, size[0] - 5))
    photon_flux = compute_filter_photon_flux(mag, zp=filter_info['zpphot'])
    print(f"Star {i+1:2d}: mag={mag:.2f}, x={xpix}, y={ypix}, flux={photon_flux:.2e} phot/s/m²")
    make_point_source_image(
        xpix, ypix, photon_flux,
        psf=psf,
        image_out=source_rate,
    )

Sky background

The per-pixel sky emission rate is derived from the filter background magnitude and integrated over the pixel solid angle.

[ ]:
sky_data = get_maunakea_sky_background(
    filter_info=filter_info,
    resolution=resolution,
    T_tel=275, T_atm=258, T_aos=243,
    Em_tel=0.09, Em_atm=0.2, Em_aos=0.01,
    plate_scale=scale,
)

sky_em_rate = sky_data['sky_em_rate_bandpass_tot']
sky_trans = sky_data['sky_trans_bandpass_mean']

Run the exposure simulation

expose_imager adds Poisson noise, dark current, and read noise, and returns a dict of simulated images and noise components.

[ ]:
sim = expose_imager(
    source_rate,
    itime=itime, n_frames=n_frames, collarea=collarea,
    sky_emission_rate=sky_em_rate,
    tput=tput, read_noise=read_noise, dark_current=dark_current,
)
print(f"Peak SNR: {np.nanmax(sim['snr']):.1f}")

Visualize results

[ ]:
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
rate_map = sim['observed_rate'].copy()
rate_map[rate_map <= 0] = np.nan  # Mask non-positive values for
im0 = axes[0].imshow(
    np.log10(rate_map),
    origin='lower', cmap='inferno',
)
axes[0].set_title('log10(Observed rate (e-/s))')
plt.colorbar(im0, ax=axes[0])

im1 = axes[1].imshow(sim['snr'], origin='lower', cmap='viridis', vmin=0)
axes[1].set_title('SNR per pixel')
plt.colorbar(im1, ax=axes[1])

plt.tight_layout()
plt.show()