Source code for specsr_roman.extraction.extract

"""Contamination-subtracted optimal extraction of a single target."""

from __future__ import annotations

import numpy as np

from ..data.transforms import fluxconserve_resample, interp_ascending
from ..grids import GRIZLI_BEAM_SIZE, WAVE_HR, WAVE_LR

__all__ = ["extract_target", "ExtractionFailure"]


[docs] class ExtractionFailure(Exception): """A target could not be extracted usefully. Skip it and move on."""
[docs] def extract_target(flt, scene, noisy, err2d, compact_id: int, mag: float, spectrum_1d, min_finite: int = 100): """Extract one target, subtracting everything else in the scene. Returns ``(wave, flam, flam_err)`` on the native grism sampling. Contamination is handled exactly, not modelled approximately: the target is re-dispersed alone with its own true SED, and that 2D model is subtracted from the full scene. Whatever remains inside the beam is genuinely other galaxies' light --- which is the whole point of simulating a slitless survey rather than isolated sources. Flux calibration comes from a second, flat-f_lambda pass through the same beam: dividing the extracted counts by the extracted flat model converts to f_lambda while cancelling the trace, the sensitivity curve and the optimal-extraction profile in one step. Pixels where the flat model falls below 5% of its peak are set to NaN --- the band edges, where that division is unstable. """ out = flt.compute_model_orders(id=compact_id, mag=mag, size=GRIZLI_BEAM_SIZE, compute_size=False, spectrum_1d=spectrum_1d, is_cgs=False, store=False, in_place=False) # grizli may return either the model array or a (status, array) tuple # depending on version -- unpack defensively. if isinstance(out, (list, tuple)): out = out[1] own2d = np.asarray(out, dtype=np.float32).reshape(scene.shape) beams = flt.compute_model_orders(id=compact_id, mag=mag, size=GRIZLI_BEAM_SIZE, compute_size=False, store=False, in_place=False, get_beams=["A"]) beam = beams["A"] beam.compute_model() # flat f_lambda -> calibration beam slx, sly = beam.slx_parent, beam.sly_parent sci_c = noisy[sly, slx] - (scene - own2d)[sly, slx] ivar = 1.0 / np.clip(err2d[sly, slx], 1e-10, None) ** 2 beam.init_optimal_profile() wave, fnum, ferr = beam.optimal_extract(sci_c, ivar=ivar) _, flat_c, _ = beam.optimal_extract(beam.model, ivar=ivar) calib = np.where(flat_c > 0.05 * np.nanmax(flat_c), flat_c, np.nan) flam, flam_err = fnum / calib, ferr / calib ok = np.isfinite(flam) & np.isfinite(flam_err) & (flam_err > 0) if ok.sum() < min_finite: raise ExtractionFailure(f"only {ok.sum()} usable pixels") return wave[ok], flam[ok], flam_err[ok]
def to_fixed_grids(wave, flam, flam_err, sed_wave, sed_flux, wave_lr=None, wave_hr=None, min_finite: int = 100): """Native extraction + true SED -> the shared LR and HR grids. ``interp_ascending`` rather than ``np.interp``: Roman's ``DLDP_A_1`` is negative so ``wave`` comes out descending, and plain ``np.interp`` would return a constant. See :func:`specsr_roman.data.transforms.interp_ascending`. """ wave_lr = WAVE_LR if wave_lr is None else wave_lr wave_hr = WAVE_HR if wave_hr is None else wave_hr lr = interp_ascending(wave_lr, wave, flam) lr_err = interp_ascending(wave_lr, wave, flam_err) lr_ok = np.isfinite(lr) if lr_ok.sum() < min_finite or np.nanstd(lr[lr_ok]) == 0: raise ExtractionFailure("resampled spectrum is empty or constant") hr = fluxconserve_resample(sed_wave, sed_flux, wave_hr) return lr, lr_err, hr