import logging
import numpy as np
from astropy import units as u
from astropy.modeling import tabular
from astropy.modeling.mappings import Identity
from gwcs import coordinate_frames as cf
from gwcs import wcstools
from stdatamodels.jwst import datamodels
log = logging.getLogger(__name__)
__all__ = [
"do_correction",
"apply_zero_point_correction",
"calculate_wavelength_correction_transform",
"compute_dispersion",
"compute_wavelength",
]
[docs]
def do_correction(input_model, wavecorr_file):
"""
Perform wavelength correction for NIRSpec MOS and FS point sources.
Parameters
----------
input_model : `~stdatamodels.jwst.datamodels.ImageModel` or \
`~stdatamodels.jwst.datamodels.CubeModel`
Input data model. It is updated in place.
wavecorr_file : str
Wavecorr reference file name.
Returns
-------
output_model : `~stdatamodels.jwst.datamodels.ImageModel` or \
`~stdatamodels.jwst.datamodels.CubeModel`
Corrected data model.
"""
wavecorr_supported_modes = ["NRS_FIXEDSLIT", "NRS_MSASPEC", "NRS_BRIGHTOBJ", "NRS_AUTOFLAT"]
# Check for valid exposure type
exp_type = input_model.meta.exposure.type.upper()
if exp_type not in wavecorr_supported_modes:
log.info(f"Skipping wavecorr correction for EXP_TYPE {exp_type}")
input_model.meta.cal_step.wavecorr = "SKIPPED"
return input_model
# For BRIGHTOBJ, operate on the single SlitModel
corrected = False
if isinstance(input_model, datamodels.SlitModel):
if _is_point_source(input_model):
corrected = apply_zero_point_correction(input_model, wavecorr_file)
else:
for slit in input_model.slits:
if _is_point_source(slit):
completed = apply_zero_point_correction(slit, wavecorr_file)
if completed:
corrected = True
slit.wavelength_corrected = True
else: # pragma: no cover
log.warning(f"Corrections are not invertible for slit {slit.name}")
log.warning("Skipping wavecorr correction")
slit.wavelength_corrected = False
else:
slit.wavelength_corrected = False
if corrected:
input_model.meta.cal_step.wavecorr = "COMPLETE"
else:
input_model.meta.cal_step.wavecorr = "SKIPPED"
return input_model
[docs]
def apply_zero_point_correction(slit, reffile):
"""
Apply the NIRSpec wavelength zero-point correction.
Parameters
----------
slit : `~stdatamodels.jwst.datamodels.SlitModel` or \
`~stdatamodels.jwst.datamodels.CubeModel`
Slit data to be corrected.
reffile : str
The ``wavecorr`` reference file.
Returns
-------
completed : bool
A flag to report whether the zero-point correction was added or skipped.
"""
slit_wcs = slit.meta.wcs
# Retrieve the source position and aperture name from metadata
source_xpos = slit.source_xpos
if slit.meta.exposure.type in ["NRS_FIXEDSLIT", "NRS_BRIGHTOBJ"]:
aperture_name = slit.name
else:
# For the MSA the aperture name is "MOS"
aperture_name = "MOS"
lam = slit.wavelength.copy() * 1e-6
dispersion = compute_dispersion(slit.meta.wcs)
wave2wavecorr = calculate_wavelength_correction_transform(
lam, dispersion, reffile, source_xpos, aperture_name
)
# wave2wavecorr should not be None for real data
if wave2wavecorr is None:
completed = False
return completed
else:
# Make a new frame to insert into the slit wcs object
slit_spatial = cf.Frame2D(
name="slit_spatial", axes_order=(0, 1), unit=("", ""), axes_names=("x_slit", "y_slit")
)
spec = cf.SpectralFrame(
name="spectral", axes_order=(2,), unit=(u.micron,), axes_names=("wavelength",)
)
wcorr_frame = cf.CompositeFrame([slit_spatial, spec], name="wavecorr_frame")
# Insert the new transform into the slit wcs object
wave2wavecorr = Identity(2) & wave2wavecorr
slit_wcs.insert_frame("slit_frame", wave2wavecorr, wcorr_frame)
# Update the stored wavelengths for the slit
slit.wavelength = compute_wavelength(slit_wcs)
completed = True
return completed
[docs]
def compute_dispersion(wcs, xpix=None, ypix=None):
"""
Compute the pixel dispersion.
If ``xpix`` or ``ypix`` is not provided, the dispersion is computed on a grid
based on ``wcs.bounding_box``.
Parameters
----------
wcs : `~gwcs.wcs.WCS`
The WCS object for this slit.
xpix : ndarray, float, optional
X pixel coordinates for dispersion elements.
ypix : ndarray, float, optional
Y pixel coordinates for dispersion elements.
Returns
-------
dispersion : ndarray
The pixel dispersion in meters.
"""
if xpix is None or ypix is None:
xpix, ypix = wcstools.grid_from_bounding_box(wcs.bounding_box, step=(1, 1))
xleft = xpix - 0.5
xright = xpix + 0.5
_, _, lamright = wcs(xright, ypix)
_, _, lamleft = wcs(xleft, ypix)
return (lamright - lamleft) * 10**-6
[docs]
def compute_wavelength(wcs, xpix=None, ypix=None):
"""
Compute the pixel wavelength.
If ``xpix`` or ``ypix`` is not provided, the dispersion is computed on a grid
based on ``wcs.bounding_box``.
Parameters
----------
wcs : `~gwcs.wcs.WCS`
The WCS object for this slit.
xpix : ndarray, float, optional
X pixel coordinates for dispersion elements.
ypix : ndarray, float, optional
Y pixel coordinates for dispersion elements.
Returns
-------
wavelength : ndarray
The wavelength in microns.
"""
if xpix is None or ypix is None:
xpix, ypix = wcstools.grid_from_bounding_box(wcs.bounding_box, step=(1, 1))
_, _, lam = wcs(xpix, ypix)
return lam
def _is_point_source(slit):
"""
Determine if a source is a point source.
Parameters
----------
slit : `~stdatamodels.jwst.transforms.models.Slit`
A slit object.
Returns
-------
bool
`True` if point source; `False` otherwise.
"""
result = False
# Get the source type value set by the source_type step (if any)
if slit.source_type is not None:
src_type = slit.source_type
elif slit.meta.target.source_type is not None:
src_type = slit.meta.target.source_type
else:
src_type = None
if src_type is not None and src_type.upper() in ["POINT", "EXTENDED"]:
# Use the supplied value
log.info(f"Detected a {src_type} source type in slit {slit.name}")
if src_type.strip().upper() == "POINT":
result = True
else:
result = False
else:
log.info("Unknown source type")
return result