Source code for specsr_roman.extraction.simulate

"""Dispersing a scene and adding Roman grism noise."""

from __future__ import annotations

import numpy as np

from ..grids import GRISM_BKG, GRISM_EXPTIME, GRIZLI_BEAM_SIZE, READ_NOISE

__all__ = ["disperse_scene", "add_grism_noise"]


[docs] def disperse_scene(flt, compact_ids, object_ids, mags, redshifts, seds, scene_indices, verbose: bool = True): """Disperse every listed source with its true SED into ``flt.model``. Returns ``{index: grizli spectrum}`` for the sources that succeeded, so a later pass can re-disperse a target on its own without re-reading the SED file. ``size=85, compute_size=False`` is not tunable in practice. The Roman trace sits up to ~66 px from the source on detector 1 (and ~162 px on detector 4), which overflows grizli's default adaptive cutout and silently truncates the beam. """ kept = {} n_fail = 0 for j, i in enumerate(scene_indices): cid, oid = int(compact_ids[i]), int(object_ids[i]) z = redshifts.get(oid) if z is None: n_fail += 1 continue try: got = seds.grizli_spectrum(oid, float(z)) except KeyError: got = None if got is None: n_fail += 1 continue spec, raw = got kept[i] = (spec, raw) flt.compute_model_orders(id=cid, mag=float(mags[i]), size=GRIZLI_BEAM_SIZE, compute_size=False, spectrum_1d=spec, is_cgs=False, store=False, in_place=True) if verbose and (j + 1) % 100 == 0: print(f" {j + 1}/{len(scene_indices)} dispersed", flush=True) if verbose and n_fail: print(f" {n_fail} sources had no catalogue/SED entry (skipped)") return kept
[docs] def add_grism_noise(scene: np.ndarray, exptime: float = GRISM_EXPTIME, background: float = GRISM_BKG, read_noise: float = READ_NOISE, seed: int = 0): """Noiseless scene (e-/s) -> ``(noisy, error)``, both in e-/s. Poisson from source plus zodiacal background, plus read noise, all expressed as a rate variance so the arrays stay in the units grizli's optimal extraction expects. The Gaussian approximation to the Poisson term is safe here: even a faint HLSS source accumulates enough electrons over 301 s that the distribution is near-normal, and the background alone contributes ~170 e-. """ rng = np.random.default_rng(seed) var_rate = ((np.clip(scene, 0, None) + background) / exptime + (read_noise / exptime) ** 2) err = np.sqrt(var_rate).astype(np.float32) noisy = (scene + rng.normal(0, err)).astype(np.float32) return noisy, err