Source code for specsr_roman.data.photometry

"""Broadband photometry handling for the redshift stage.

Two operations, both of which have been got wrong at least once in this
project's history and both of which change the headline number by an order of
magnitude.
"""

from __future__ import annotations

import numpy as np
import torch

from ..grids import PHOT_BANDS, resolve_phot_tier

__all__ = ["select_bands", "apply_phot_noise", "band_names",
           "standardization_stats"]


[docs] def band_names(indices) -> list[str]: """Index list -> OU2024 column names, for logging and model cards.""" return [PHOT_BANDS[i] for i in indices]
[docs] def select_bands(phot: np.ndarray, tier: str | None) -> tuple[np.ndarray, tuple[int, ...] | None]: """Keep only the bands that ship with the grism. ``tier`` is ``"medium"`` (Roman F106/F129/F158 --- what the HLWAS grism actually comes with), an explicit ``"8,9,11"``, or ``None`` to keep everything. Using ``"all"`` on OU2024 means feeding LSST *ugrizy* plus all eight Roman bands: an effectively complete SED, from which the redshift can be read without the spectrum contributing anything. It is a valid diagnostic and an invalid model. """ keep = resolve_phot_tier(tier) if keep is None: return phot, None return phot[:, list(keep)], keep
[docs] def apply_phot_noise(phot: torch.Tensor, mag_err: float, generator: torch.Generator | None = None) -> torch.Tensor: """Multiplicative log-normal flux error of ``mag_err`` magnitudes. Catalogue photometry in a simulation is noiseless truth. Training on it teaches the head to trust colours far beyond what a real measurement supports, and --- worse --- *evaluating* on it reports an accuracy nobody will reproduce. Apply this at train and validation both; pass a seeded ``generator`` for validation so checkpoint selection is not comparing epochs across different noise draws. """ if mag_err <= 0: return phot if generator is None: dm = mag_err * torch.randn_like(phot) else: dm = mag_err * torch.randn(phot.shape, generator=generator, device=phot.device, dtype=phot.dtype) return phot * torch.pow(10.0, -0.4 * dm)
[docs] def standardization_stats(phot_train: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """``log10`` mean/std from the TRAIN split only -> ZHead buffers. Computing these over the full set leaks test-set information into the input normalisation. It is a small leak next to feeding the whole SED, but it is free to avoid. """ pm = np.log10(np.clip(phot_train, 1e-12, None)) return pm.mean(0), np.clip(pm.std(0), 1e-6, None)