Source code for specsr_roman.extraction.seds
"""Ground-truth SEDs from the OU2024 (Diffsky) catalogue.
These are the super-resolution *targets*, so how they are read matters as much
as how the spectra are extracted. Two properties of the format decide
everything downstream:
* the wavelength grid is **adaptive** --- sub-Angstrom bins at emission lines,
very coarse elsewhere. Point interpolation onto a uniform grid loses line
flux, so every rebin here is flux-conserving;
* the absolute flux scale is internal to the simulation and meaningless.
Only the *shape* is used: grizli renormalises to the direct-image counts,
and the training dataset normalises per spectrum.
"""
from __future__ import annotations
import numpy as np
from ..data.transforms import fluxconserve_resample
__all__ = ["SEDLibrary", "H158_NORM_LO", "H158_NORM_HI"]
# Band used to normalise an SED before handing it to grizli. `is_cgs=False`
# treats spectrum_1d as a multiplier against a flat f_lambda == 1 spectrum, so
# the SED must have unity mean over the direct-image band for the image counts
# to set the amplitude. Skip this and raw Diffsky values (~1e-24) disperse an
# effectively empty scene -- a silent, total failure.
H158_NORM_LO, H158_NORM_HI = 13800.0, 17700.0
[docs]
class SEDLibrary:
"""Lazily opened handle on a ``galaxy_sed_<healpix>.hdf5`` file.
The file is ~14 GB per healpix and is read thousands of times per SCA, so
it is opened once and the rest-frame wavelength grid cached. Layout is
skyCatalogs': ``galaxy/<gid // 100000>/<gid>`` holding ``(3 components,
n_wave)``, with ``meta/wave_list`` the rest-frame wavelengths in Angstrom.
"""
def __init__(self, path: str):
self.path = path
self._file = None
self._wave = None
def _ensure_open(self):
if self._file is None:
import h5py
self._file = h5py.File(self.path, "r")
self._wave = np.asarray(self._file["meta/wave_list"], dtype=np.float64)
return self._file, self._wave
def close(self) -> None:
if self._file is not None:
self._file.close()
self._file = None
[docs]
def observed(self, gid: int, z: float, dlam: float = 5.0):
"""``(observed wavelength [A], f_lambda)`` on a uniform ``dlam`` grid.
The three Diffsky components are summed, redshifted, and rebinned
flux-conservingly so the line spikes survive every later
interpolation. Raises ``KeyError`` for a galaxy with no SED entry.
"""
f, wave = self._ensure_open()
sed = np.asarray(f[f"galaxy/{gid // 100000}/{gid}"],
dtype=np.float64).sum(axis=0)
wave_obs = wave * (1.0 + z)
uniform = np.arange(2500.0, min(wave_obs[-1], 25000.0), dlam)
return uniform, fluxconserve_resample(wave_obs, sed, uniform)
[docs]
def grizli_spectrum(self, gid: int, z: float):
"""``([wave, flux_normalised], (wave, flux_raw))`` for a grizli dispersal.
Returns ``None`` if the galaxy has no usable flux in the normalising
band. The second element is the un-normalised SED, which is what
becomes the training target.
"""
wave_obs, flam = self.observed(gid, z)
m = (wave_obs > H158_NORM_LO) & (wave_obs < H158_NORM_HI)
norm = float(np.mean(flam[m])) if m.any() else 0.0
if norm <= 0:
return None
spec = [wave_obs.astype(np.float64), (flam / norm).astype(np.float64)]
return spec, (wave_obs, flam)