import logging
import os
import numpy as np
from . import continuum_subtraction as csb
from . import photframe, spec_imager, utils
from .sgs import dmutils, frame_coordinates, psf_model, relative_flux
from .spec_crop import NoExtraction, SpecCrop
logger = logging.getLogger(__name__)
[docs]
class SpecFrame:
"""
Represents a spectral frame (observation) in the Gelsa simulation.
This class handles the parameters, loading, and coordinate transformations
for a specific spectral observation frame (e.g., a NISP slitless
spectroscopy exposure). It interfaces with various calibration models
(detector, optical, dispersion, sensitivity, PSF) to provide accurate
coordinate conversions and flux calculations. It can load existing
FITS frames or be used to define and simulate new ones.
"""
_default_params = {
'grism_name': 'RGS000',
'RA': 0,
'DEC': 0,
'PA': 0,
'tilt': 0,
'PIXSCALE': 0.3,
'exptime_sec': 550.,
'sigma2_det': 2.33,
'apply_median_filter': False,
'median_filter_size_pix': 40,
'median_filter_mask_threshold': 80,
'apply_persistence_correction': False,
'persistence_mask_threshold': 30,
'use_cache': False,
}
dispersion_angles = {
'RGS000': 0.,
'RGS180': 180.,
'BGS000': 0.
}
detectors = [11,21,31,41,
12,22,32,42,
13,23,33,43,
14,24,34,44]
def __init__(self, continuum_subtraction=None, **kwargs):
"""
Initialize a SpecFrame object.
Sets up default parameters and updates them with any provided keyword
arguments. Initializes the frame coordinate system.
Args:
**kwargs: Keyword arguments to override default parameters like
'RA', 'DEC', 'PA', 'grism_name', 'tilt', etc.
"""
self._detector_cache = {}
self.params = self._default_params.copy()
for key, value in kwargs.items():
if key in self.params:
self.params[key] = value
self.hdu_loaded = False
self._setup()
self._setup_continuum_subtraction(continuum_subtraction)
def __str__(self):
"""
Return a string representation of the SpecFrame object.
Includes the class name and the current parameter dictionary.
Returns:
str: String representation of the object.
"""
s = f"<{self.__class__}\n"
s += "params = {"
for key, value in self.params.items():
s += f" '{key}': {value},\n"
s += "} >"
return s
def copy(self):
""" """
newframe = SpecFrame(**self.params)
newframe.framecoord = self.framecoord
newframe.set_detector_model(self.detector_model)
newframe.sensitivity_params = self.sensitivity_params
newframe.optical_params = self.framecoord.optical_params
newframe.params['wavelength_range'] = self.params['wavelength_range']
newframe.relative_flux_loss_params = self.relative_flux_loss_params
newframe.psf_params = self.psf_params
try:
newframe.zero_order_mask = self.zero_order_mask
except AttributeError:
newframe.zero_order_mask = None
try:
newframe._persistence_frame = self._persistence_frame
except AttributeError:
pass
return newframe
def _setup(self):
"""
Internal setup method.
Calculates the total dispersion angle based on grism and tilt.
Initializes the FrameCoordinates object used for transformations.
"""
self.params['angle'] = self.params['tilt'] + \
self.dispersion_angles[self.params['grism_name']]
self.framecoord = frame_coordinates.FrameCoordinates(
self.params,
)
def _setup_continuum_subtraction(self, continuum_subtraction=None):
""" """
if continuum_subtraction is None:
if self.params['apply_median_filter'] and \
(self.params['median_filter_size_pix'] > 0):
continuum_subtraction = csb.MedianFilterFrame(
median_filter_size_pix=self.params['median_filter_size_pix']
)
self._cs = continuum_subtraction
[docs]
def load_frame(self, frame_path=None, loctable_path=None, persistence_path=None):
"""
Load frame data and metadata from a FITS file.
Reads the primary header and potentially updates pointing information
(RA, DEC, PA) either from specific SIR headers or by using a
location table if provided or found automatically.
Args:
frame_path (str, optional): Path to the FITS science frame file.
Can be .gz compressed. Defaults to None.
loctable_path (str, optional): Path to the corresponding location
table FITS file. If None, attempts
to find it automatically based on
the science frame path. Defaults to None.
"""
if frame_path.endswith(".gz"):
# check if decompressed file exists
path_ = frame_path[:-3]
if os.path.exists(path_):
frame_path = path_
logger.info(f"found {frame_path}")
self._hdul, self._metadata = dmutils.load_scienceframe(frame_path)
self._header = self._hdul[0].header
self.params['frame_path'] = frame_path
self.params['loctable_path'] = loctable_path
self.params['grism_name'] = self._header['GWA_POS'].strip()
self.params['tilt'] = self._header['GWA_TILT']
self.params['DATE-OBS'] = self._header['DATE-OBS']
try:
# Look for SIR adjusted pointing center
self.params['RA'] = self._header['SIR_RA']
self.params['DEC'] = self._header['SIR_DEC']
self.params['PA'] = self._header['SIR_PA']
got_adjusted_pointing = True
except KeyError:
# Fall back to commanded pointing with location table
self.params['RA'] = self._header['RA']
self.params['DEC'] = self._header['DEC']
self.params['PA'] = self._header['PA']
got_adjusted_pointing = False
self.params['PIXSCALE'] = self._header['PIXSCALE']
self.params['PTGID'] = self._header['PTGID']
self.params['OBS_ID'] = self._header['OBS_ID']
self.params['DITHOBS'] = self._header['DITHOBS']
# Read with get: these are not in every frame, and a missing keyword
# should leave the value empty rather than stop the frame loading.
self.params['CALBLKID'] = self._header.get('CALBLKID')
self.params['OBSMODE'] = self._header.get('OBSMODE')
self.params['PATCH_ID'] = self._header.get('PATCH_ID')
self._setup()
if not got_adjusted_pointing:
if loctable_path is None:
try:
loctable_path = dmutils.find_location_table(frame_path)
except ValueError:
logger.warning("could not find location table automatically")
try:
self.framecoord.update_optical_model_from_location_table(loctable_path)
except Exception:
pass
self.hdu_loaded = True
def set_pointing_center(self, ra, dec, pa):
""" """
self.framecoord.set_pointing_center(ra, dec, pa)
@property
def hdul(self):
"""
Access the FITS HDUList object for the frame.
Loads the FITS file on first access if not already loaded.
Returns:
astropy.io.fits.HDUList: The HDUList object.
"""
try:
return self._hdul
except AttributeError:
self._hdul, self._metadata = dmutils.load_scienceframe(self.params['frame_path'])
# close_frame() drops the header along with the HDU list, so restore
# it here: a reopened frame must be as closeable as a freshly
# loaded one.
self._header = self._hdul[0].header
self.hdu_loaded = True
return self._hdul
def clear_cache(self):
""" """
self._detector_cache = {}
[docs]
def close_frame(self):
"""
Close the associated FITS file handle.
Deletes internal references to the HDUList and header. Closing a frame
that is already closed does nothing.
"""
if self.hdu_loaded:
hdul = self.__dict__.pop('_hdul', None)
if hdul is not None:
hdul.close()
self.__dict__.pop('_header', None)
self.zero_order_mask = None
self.clear_cache()
[docs]
def set_detector_model(self, detector_model):
"""
Set the detector model for coordinate transformations.
Args:
detector_model (detector_model.DetectorModel): The detector model instance.
"""
self.framecoord.detector_model = detector_model
self.detector_model = detector_model
[docs]
def set_optical_model(self, optical_model):
"""
Set the optical model based on the current grism and tilt.
Args:
optical_model (optical_model.OpticalModel): The optical model instance.
"""
self.framecoord.optical_params = optical_model.get_model(
self.params['grism_name'], tilt=self.params['tilt']
)
self.optical_params = self.framecoord.optical_params
[docs]
def set_dispersion_model(self, dispersion_model):
"""
Set the dispersion models (IDS and CRV) based on grism and tilt.
Args:
dispersion_model (dict): A dictionary containing 'ids' and 'crv'
DisplacementModel instances.
"""
ids_model = dispersion_model['ids']
crv_model = dispersion_model['crv']
self.framecoord.ids_params = ids_model.get_model(
self.params['grism_name'], tilt=self.params['tilt'])
self.framecoord.crv_params = crv_model.get_model(
self.params['grism_name'], tilt=self.params['tilt'])
[docs]
def set_sensitivity(self, sensitivity_model):
"""
Set the sensitivity model and update the frame's wavelength range.
Args:
sensitivity_model (sensitivity_model.SensitivityModel): The sensitivity
model instance.
"""
self.sensitivity_params = sensitivity_model.get_model(
self.params['grism_name'], self.params['tilt'])
self.params['wavelength_range'] = self.sensitivity_params['bounds']
[docs]
def set_relative_flux_model(self, relative_flux_model):
"""
Set the relative flux loss calibration model.
Args:
relative_flux_model (relative_flux.RelativeFluxCalibration or None):
The relative flux model instance, or None if not used.
"""
self.relative_flux_loss_params = None
if relative_flux_model is not None:
self.relative_flux_loss_params = relative_flux_model.get_model(
self.params['grism_name'], self.params['tilt']
)
[docs]
def set_psf_model(self, psf_model):
"""
Set the Point Spread Function (PSF) model.
Args:
psf_model (psf_model.PSFModel or psf_model.DefaultPSFModel or None):
The PSF model instance, or None if not used.
"""
self.psf_params = None
if psf_model is not None:
self.psf_params = psf_model.get_model(
self.params['grism_name']
)
[docs]
def set_zero_order_mask(self, zero_order_mask):
"""
Set and apply the zero-order contamination mask.
Args:
zero_order_mask (zero_order_mask.ZeroOrderMask or None):
The zero-order mask instance, or None if not used.
"""
if zero_order_mask is None:
self.zero_order_mask = None
else:
self.zero_order_mask = zero_order_mask.create_zero_order_mask(self)
def set_persistence_mask(self, path):
""" """
if path is None:
self._persistence_frame = None
return
self._persistence_frame = photframe.PhotFrame(sir_layout=True)
self._persistence_frame.load_frame(path)
@property
def persistence_frame(self):
""" """
try:
return self._persistence_frame
except AttributeError:
return None
@property
def detector_shape(self):
"""
Return detector size in pixels (ny, nx).
Note the order (ny, nx) consistent with numpy array indexing.
Returns:
tuple: (height, width) in pixels.
"""
nx = self.detector_model.params['nx_pixels']
ny = self.detector_model.params['ny_pixels']
return ny, nx # Note: numpy shape order (rows, columns)
[docs]
def arcsec_to_pixel(self, x_arcsec):
"""Convert units from arsec to pixels
x_pix = x_arcsec / pixscale
with pixscale in arcsec/pixel
"""
return x_arcsec / self.params['PIXSCALE']
[docs]
def radec_to_pixel_(self, ra, dec, wavelength, dispersion_order=1, **kwargs):
"""
Compute detector pixel coordinates from RA, Dec sky position and wavelength.
Args:
ra (float or numpy.ndarray): Sky position RA in degrees.
dec (float or numpy.ndarray): Sky position Dec in degrees.
wavelength (float or numpy.ndarray): Wavelength in angstrom.
dispersion_order (int, optional): Dispersion order (0 or 1). Defaults to 1.
**kwargs: Additional arguments passed to framecoord.radec_to_pixel.
Returns:
tuple: (x, y, detector_index)
x (float or ndarray): Pixel x-coordinate(s).
y (float or ndarray): Pixel y-coordinate(s).
detector_index (int or ndarray): Detector index/indices (0-15), -1 if off detector.
"""
return self.framecoord.radec_to_pixel_(ra, dec, wavelength,dispersion_order)
[docs]
def radec_to_pixel(self, ra, dec, wavelength, dispersion_order=1, **kwargs):
"""
Compute detector pixel coordinates from RA, Dec sky position and wavelength.
Args:
ra (float or numpy.ndarray): Sky position RA in degrees.
dec (float or numpy.ndarray): Sky position Dec in degrees.
wavelength (float or numpy.ndarray): Wavelength in angstrom.
dispersion_order (int, optional): Dispersion order (0 or 1). Defaults to 1.
**kwargs: Additional arguments passed to framecoord.radec_to_pixel.
Returns:
tuple: (x, y, detector_index)
x (float or ndarray): Pixel x-coordinate(s).
y (float or ndarray): Pixel y-coordinate(s).
detector_index (int or ndarray): Detector index/indices (0-15), -1 if off detector.
"""
return self.framecoord.radec_to_pixel(ra, dec, wavelength,dispersion_order)
[docs]
def radec_to_fov(self, ra, dec, wavelength, dispersion_order=1, **kwargs):
"""
Compute detector pixel coordinates from RA, Dec sky position and wavelength.
Args:
ra (float or numpy.ndarray): Sky position RA in degrees.
dec (float or numpy.ndarray): Sky position Dec in degrees.
wavelength (float or numpy.ndarray): Wavelength in angstrom.
dispersion_order (int, optional): Dispersion order (0 or 1). Defaults to 1.
**kwargs: Additional arguments passed to framecoord.radec_to_pixel.
Returns:
tuple: (x, y, detector_index)
x (float or ndarray): Pixel x-coordinate(s).
y (float or ndarray): Pixel y-coordinate(s).
detector_index (int or ndarray): Detector index/indices (0-15), -1 if off detector.
"""
return self.framecoord.radec_to_fov(ra, dec, wavelength, dispersion_order=dispersion_order)
[docs]
def fov_to_pixel(self, xfov,yfov, **kwargs):
"""
Compute detector pixel coordinates from RA, Dec sky position and wavelength.
Args:
ra (float or numpy.ndarray): Sky position RA in degrees.
dec (float or numpy.ndarray): Sky position Dec in degrees.
wavelength (float or numpy.ndarray): Wavelength in angstrom.
dispersion_order (int, optional): Dispersion order (0 or 1). Defaults to 1.
**kwargs: Additional arguments passed to framecoord.radec_to_pixel.
Returns:
tuple: (x, y, detector_index)
x (float or ndarray): Pixel x-coordinate(s).
y (float or ndarray): Pixel y-coordinate(s).
detector_index (int or ndarray): Detector index/indices (0-15), -1 if off detector.
"""
return self.detector_model.getPixel(xfov, yfov)
[docs]
def pixel_to_radec(self, x, y, det, wavelength, dispersion_order=1):
"""
Compute sky position RA, Dec from detector pixel coordinates and wavelength.
Args:
x (float or numpy.ndarray): Detector pixel x-coordinate(s).
y (float or numpy.ndarray): Detector pixel y-coordinate(s).
det (int or numpy.ndarray): NISP detector index/indices (0-15).
wavelength (float or numpy.ndarray): Wavelength in angstrom.
dispersion_order (int, optional): Dispersion order (0 or 1). Defaults to 1.
Returns:
tuple: (RA, Dec)
RA (float or ndarray): Right Ascension coordinate(s) in degrees.
Dec (float or ndarray): Declination coordinate(s) in degrees.
"""
return self.framecoord.pixel_to_radec( x, y, det, wavelength,
dispersion_order)
[docs]
def sample_psf(self, x=None, y=None, detector=None, wavelength_ang=None,
**args):
"""
Draw samples representing the PSF offset at given coordinates/wavelength.
Args:
x (float or ndarray, optional): Detector x-coordinate(s). Not currently used
for position dependence. Defaults to None.
y (float or ndarray, optional): Detector y-coordinate(s). Not currently used
for position dependence. Defaults to None.
detector (int or ndarray, optional): Detector index/indices. Not currently used.
Defaults to None.
wavelength_ang (float or ndarray, optional): Wavelength(s) in angstroms.
Used for chromatic PSF. Defaults to None.
**args: Additional arguments passed to psf_model.sample_psf.
Returns:
tuple: (dx, dy) offsets in pixel coordinates, or (0, 0) if no PSF model is set.
"""
if self.psf_params is None:
return 0, 0
pos_fov = None
samples = psf_model.sample_psf(
self.psf_params,
pos_fov=pos_fov, wavelength_ang=wavelength_ang,
**args
)
return samples
[docs]
def lineflux_to_counts(self, flux, wavelength):
"""
Convert line flux in erg/s/cm^2 to photon counts.
Uses the sensitivity model and exposure time.
Args:
flux (float or ndarray): Line flux in erg/s/cm^2.
wavelength (float or ndarray): Wavelength(s) in angstroms.
Returns:
float or ndarray: Corresponding photon counts.
"""
return flux * self.sensitivity_params['func'](wavelength) * self.params['exptime_sec']
[docs]
def flux_to_counts(self, flux, wavelength):
"""
Convert flux density in erg/s/cm^2/A to photon counts.
Uses the sensitivity model, wavelength step, and exposure time.
Args:
flux (float or ndarray): Flux density in erg/s/cm^2/A.
wavelength (float or ndarray): Wavelength array in angstroms. Assumes
uniform spacing to calculate step.
Returns:
float or ndarray: Corresponding photon counts.
"""
step = wavelength[1] - wavelength[0]
return flux * step * self.sensitivity_params['func'](wavelength) * self.params['exptime_sec']
[docs]
def counts_to_flux(self, counts, wavelength):
"""
Convert photon counts to flux density in erg/s/cm^2/A.
Inverse of flux_to_counts. Uses sensitivity, wavelength step, and exposure time.
Args:
counts (float or ndarray): Photon counts.
wavelength (float or ndarray): Wavelength array in angstroms. Assumes
uniform spacing to calculate step.
Returns:
float or ndarray: Corresponding flux density in erg/s/cm^2/A.
"""
step = wavelength[1] - wavelength[0]
return counts / (step * self.sensitivity_params['func'](wavelength) * self.params['exptime_sec'])
[docs]
def get_sensitivity(self, wavelength):
"""
Return sensitivity (response) in units counts/(erg/cm^2/s).
Note: This is the sensitivity integrated over the pixel wavelength step.
Args:
wavelength (float or ndarray): Wavelength array in angstroms. Assumes
uniform spacing to calculate step.
Returns:
float or ndarray: Sensitivity value(s).
"""
step = wavelength[1] - wavelength[0]
return step * self.sensitivity_params['func'](wavelength) * self.params['exptime_sec']
# alias
sensitivity = get_sensitivity
[docs]
def get_relative_flux_loss_radec(self, ra, dec, wavelength_ang=None):
"""Calculates the relative flux loss factor at given detector coordinates and wavelength.
Uses the relative flux calibration model (`relative_flux_loss_params`)
if it has been set via `set_relative_flux_model`.
Args:
x (float | np.ndarray): Detector x-coordinate(s).
y (float | np.ndarray): Detector y-coordinate(s).
det_index (int | np.ndarray): NISP detector index/indices (0-15).
wavelength_ang (float | np.ndarray): Wavelength(s) in Angstroms.
Returns:
float | np.ndarray: Flux loss factor. This is a multiplicative factor
(<= 1.0) representing the fraction of flux remaining after
accounting for effects like vignetting or detector gaps modeled
by the relative flux calibration. Returns 1.0 if no model is
set or if the model doesn't apply to the given inputs. The
shape matches the input coordinate/wavelength arrays.
Raises:
AttributeError: If the detector model has not been set (needed by
the underlying `relative_flux.get_flux_loss`).
"""
try:
self.relative_flux_loss_params
except AttributeError:
return 1
if self.relative_flux_loss_params is None:
return 1
if wavelength_ang is None:
wavelength_ang = (self.params['wavelength_range'][0] + self.params['wavelength_range'][1])/2.
scalar = np.ndim(ra) == 0
ra = np.atleast_1d(ra)
dec = np.atleast_1d(dec)
wavelength_ang = np.ones(len(ra)) * wavelength_ang
xmm_undist, ymm_undist = self.framecoord.getUndistortedObjectPosition(ra, dec)
xfov, yfov = self.framecoord.getReferencePosition_jit(
xmm_undist=xmm_undist, ymm_undist=ymm_undist,
order=1
)
y = relative_flux.get_flux_loss_fov(
self.detector_model,
self.relative_flux_loss_params,
xfov, yfov, wavelength_ang=wavelength_ang
)
if scalar:
return y[0]
return y
[docs]
def get_detector(self, detector_index):
"""
Get the pixel data arrays for a specific detector.
Retrieves the science image, data quality (mask), and variance arrays.
Applies the zero-order mask if it has been set.
Args:
detector_index (int): NISP detector index (0-15).
Returns:
tuple: (image, mask, variance)
image (ndarray): Science image data.
mask (ndarray): Data quality mask (0=good, 1=bad).
variance (ndarray): Variance data.
"""
d = self.detectors[detector_index]
cache_key = detector_index
if cache_key in self._detector_cache:
return self._detector_cache[cache_key]
if self.hdu_loaded:
extname = f'DET{d}.SCI'
maskname = f'DET{d}.DQ'
varname = f'DET{d}.VAR'
fullimage = self.hdul[extname].data
fullmask = self.hdul[maskname].data
fullvar = self.hdul[varname].data
else:
# defaults if simulating
fullimage = 0
fullmask = 0
fullvar = 0
# Handle case where data is simulated in memory
if hasattr(self, '_data') and detector_index in self._data:
fullimage += fullimage + self._data[detector_index]
fullmask = (fullmask + self._mask[detector_index]) > 0
fullvar = fullvar + self._var[detector_index]
elif not self.hdu_loaded:
# Nothing loaded or simulated. In this case, image is zero
nx = self.framecoord.detector_model.params['nx_pixels']
ny = self.framecoord.detector_model.params['ny_pixels']
fullimage = np.zeros((ny, nx), dtype='d')
fullmask = np.zeros((ny, nx), dtype=bool)
fullvar = np.zeros((ny, nx), dtype='d')
try:
if self.zero_order_mask is not None:
invalid = self.zero_order_mask[detector_index] == 0
fullimage[invalid] = 0
fullmask[invalid] = 1
except AttributeError:
pass
if self._cs is not None:
fullimage, fullmask = self._cs.process_frame(self, fullimage, fullmask)
if self.persistence_frame is not None:
persistance_image = self.persistence_frame.get_detector(detector_index)
if self.params['apply_persistence_correction']:
# apply persistence correction
fullimage -= persistance_image
if self.params['persistence_mask_threshold'] > 0:
# mask persistence above threshold
invalid = persistance_image > self.params['persistence_mask_threshold']
fullimage[invalid] = 0
fullmask[invalid] = 1
logger.info(f"Masking persistence {self.params['persistence_mask_threshold']} " \
f"fraction of pixels: {np.sum(invalid)/invalid.size}")
# store in cache
if self.params['use_cache']:
self._detector_cache[cache_key] = (fullimage, fullmask, fullvar)
return fullimage, fullmask, fullvar
[docs]
def cutout(self, ra, dec, redshift=0, lam_step=20, wavelength_range=None,
select_detector=None, **crop_args):
"""
Create spectral cutouts (SpecCrop objects) around a sky position.
Calculates the trace of the object across the detectors for the given
wavelength range and creates SpecCrop instances for each detector
the trace falls on.
Args:
ra (float): Right Ascension of the target center (degrees).
dec (float): Declination of the target center (degrees).
redshift (float, optional): Redshift of the target (used for metadata). Defaults to 0.
lam_step (float, optional): Wavelength step for calculating the trace (Angstroms).
Defaults to 20.
wavelength_range (tuple, optional): (min, max) wavelength range for the trace.
Defaults to the frame's sensitivity range.
**crop_args: Additional keyword arguments passed to SpecCrop.crop_trace
(e.g., padx, pady for padding around the trace).
Returns:
dict: A dictionary where keys are detector indices (int) and values
are the corresponding SpecCrop objects.
Raises:
ValueError: If the target's trace does not fall on any detector within
the specified wavelength range.
"""
if wavelength_range is None:
wavelength_range = self.params['wavelength_range']
wave_trace = utils.intrange(*wavelength_range, lam_step)
n = len(wave_trace)
detx, dety, detid = self.radec_to_pixel(
ra*np.ones(n),
dec*np.ones(n),
wave_trace
)
valid = detid >= 0
if select_detector is not None:
logger.info(f"selecting detector {select_detector}")
valid &= detid == select_detector
if np.sum(valid) == 0:
raise ValueError(f"RA, Dec not on detector within wavelength range {wavelength_range}: {(ra, dec)}")
detx = detx[valid]
dety = dety[valid]
detid = detid[valid]
wave_trace = wave_trace[valid]
pack_list = {}
for det in np.unique(detid):
sel = detid == det
detx_ = detx[sel]
dety_ = dety[sel]
crop = SpecCrop.crop_trace(self, det, detx_, dety_, **crop_args)
crop.center = (ra, dec)
crop.wavelength_trace = wave_trace[sel]
crop.redshift = redshift
crop.apply_continuum_subtraction(self._cs)
pack_list[det] = crop
return pack_list
[docs]
def add_sources(self, galaxy_list, **kwargs):
"""
Simulate and add sources to the frame's internal data arrays.
This method is used when the SpecFrame is not loaded from an existing
FITS file but is being used for simulation. It uses SpectralImager
to generate the image of the provided galaxies.
Args:
galaxy_list (list): A list of Galaxy objects to simulate.
**kwargs: Additional keyword arguments passed to
SpectralImager.make_image.
"""
self._data = {}
self._var = {}
self._mask = {}
S = spec_imager.SpectralImager(self)
images, var_images = S.make_image(galaxy_list, return_var=True, **kwargs)
for det, image in images.items():
if det not in self._data:
self._data[det] = 0
self._var[det] = 0
self._mask[det] = np.zeros(image.shape, dtype=bool)
self._data[det] = self._data[det] + image
self._var[det] = self._var[det] + var_images[det]
[docs]
def resample_on_wavelength(self, ra, dec, **args):
"""Extracts and combines 2D spectra onto a uniform wavelength grid.
Creates cutouts around the target RA/Dec using `cutout`, then calls
`resample_on_wavelength` on each `SpecCrop` object. The results from
all detectors are summed.
Args:
ra (float): Right Ascension of the target center (degrees).
dec (float): Declination of the target center (degrees).
**args: Additional keyword arguments passed directly to
`SpecCrop.resample_on_wavelength` (e.g., `wave_range`,
`wave_step`, `extraction_window`, `ndrops`).
Returns:
tuple:
- im (np.ndarray): The combined, resampled 2D spectrum (flux).
- var (np.ndarray): The combined variance of the resampled spectrum.
- norm (np.ndarray): The combined normalization map (sum of weights).
- pix_bins (tuple): The pixel and wavelength bin edges used for
resampling, taken from the last processed crop (assumes they
are consistent).
Raises:
ValueError: If `cutout` fails to find the target on any detector.
# Add other potential exceptions from SpecCrop.resample_on_wavelength
"""
try:
crop = self.cutout(ra, dec)
except ValueError:
#print("Source not on detector")
raise
im = 0
var = 0
norm = 0
got_extraction = False
for det in crop.keys():
try:
im_, var_, norm_, pix_bins = crop[det].resample_on_wavelength(
ra, dec, **args)
except NoExtraction:
continue
got_extraction = True
im = im + im_
var = var + var_
norm = norm + norm_
if not got_extraction:
raise NoExtraction
return im, var, norm, pix_bins