"""Module for parsing APCORR and ABVEGAOFFSET reference file data."""
import logging
import numpy as np
from astropy.utils import lazyproperty
from stdatamodels.jwst import datamodels
from stdatamodels.jwst.datamodels import ABVegaOffsetModel, ImageModel
log = logging.getLogger(__name__)
__all__ = ["ReferenceData"]
[docs]
class ReferenceData:
"""
Manipulate APCORR and ABVEGAOFFSET reference file data.
These data are needed by `~jwst.source_catalog.source_catalog_step.SourceCatalogStep`.
Parameters
----------
model : `~stdatamodels.jwst.datamodels.ImageModel`
An `~stdatamodels.jwst.datamodels.ImageModel` of drizzled image.
reffile_paths : list of 2 str
The full path filename of the APCORR and ABVEGAOFFSET reference
files.
aperture_ee : tuple of 3 int
The aperture encircled energies to be used for aperture
photometry. The values must be 3 strictly-increasing integers.
Valid values are defined in the APCORR reference files (20, 30,
40, 50, 60, 70, or 80).
"""
def __init__(self, model, reffile_paths, aperture_ee):
if not isinstance(model, ImageModel):
raise TypeError("The input model must be a ImageModel.")
self.model = model
self.aperture_ee = self._validate_aperture_ee(aperture_ee)
self.apcorr_filename = reffile_paths[0]
self.abvegaoffset_filename = reffile_paths[1]
self.instrument = self.model.meta.instrument.name
self.detector = self.model.meta.instrument.detector
self.filtername = self.model.meta.instrument.filter
self.pupil = model.meta.instrument.pupil
self.subarray = self.model.meta.subarray.name
log.info(f"Instrument: {self.instrument}")
if self.detector is not None:
log.info(f"Detector: {self.detector}")
if self.filtername is not None:
log.info(f"Filter: {self.filtername}")
if self.pupil is not None:
log.info(f"Pupil: {self.pupil}")
if self.subarray is not None:
log.info(f"Subarray: {self.subarray}")
@staticmethod
def _validate_aperture_ee(aperture_ee):
aperture_ee = np.array(aperture_ee).astype(int)
if not np.all(aperture_ee[1:] > aperture_ee[:-1]):
raise ValueError("aperture_ee values must be strictly increasing")
if len(aperture_ee) != 3:
raise ValueError("aperture_ee must contain only 3 values")
if np.any(np.logical_or(aperture_ee <= 0, aperture_ee >= 100)):
raise ValueError("aperture_ee values must be between 0 and 100")
return aperture_ee
@lazyproperty
def _aperture_ee_table(self):
"""
Get the encircled energy table for the given instrument configuration.
Returns
-------
ee_table : `astropy.table.Table`
The encircled energy table.
"""
if self.instrument in ("NIRCAM", "NIRISS"):
selector = {"filter": self.filtername, "pupil": self.pupil}
elif self.instrument == "MIRI":
selector = {"filter": self.filtername, "subarray": self.subarray}
elif self.instrument == "FGS":
selector = None
else:
raise RuntimeError(f"{self.instrument} is not a valid instrument")
apcorr_model = datamodels.open(self.apcorr_filename)
apcorr = apcorr_model.apcorr_table
if selector is None: # FGS
ee_table = apcorr
else:
mask_idx = [apcorr[key] == value for key, value in selector.items()]
ee_table = apcorr[np.logical_and.reduce(mask_idx)]
if len(ee_table) == 0:
raise RuntimeError(f"APCORR reference file data is missing for {selector}.")
return ee_table
def _get_ee_table_row(self, aperture_ee):
"""
Get the encircled energy row for the input ``aperture_ee``.
Parameters
----------
aperture_ee : int
The aperture encircled energy value. Must exactly match the value
in a row of the APCORR reference file.
Returns
-------
ee_row : `astropy.io.fits.FITS_rec`
The row of the APCORR reference file that matches the input
"""
ee_percent = np.round(self._aperture_ee_table["eefraction"] * 100)
row_mask = ee_percent == aperture_ee
ee_row = self._aperture_ee_table[row_mask]
if len(ee_row) == 0:
raise RuntimeError(
"Aperture encircled energy value of "
f"{aperture_ee} appears to be invalid. No "
"matching row was found in the APCORR "
"reference file {self.apcorr_filename}"
)
if len(ee_row) > 1:
raise RuntimeError(
"More than one matching row was found in "
"the APCORR reference file "
f"{self.apcorr_filename}"
)
return ee_row
@lazyproperty
def aperture_params(self):
"""
Build the aperture parameters dictionary.
Returns
-------
params : dict
A dictionary containing the aperture parameters (radii, aperture
corrections, and background annulus inner and outer radii).
"""
if self.apcorr_filename is None:
log.warning(
"APCorrModel reference file was not input. Using "
"fallback aperture sizes without any aperture "
"corrections."
)
params = {
"aperture_radii": np.array((1.0, 2.0, 3.0)),
"aperture_corrections": np.array((1.0, 1.0, 1.0)),
"aperture_ee": np.array((1, 2, 3)),
"bkg_aperture_inner_radius": 5.0,
"bkg_aperture_outer_radius": 10.0,
}
return params
params = {}
radii = []
apcorrs = []
skyins = []
skyouts = []
for aper_ee in self.aperture_ee:
row = self._get_ee_table_row(aper_ee)
radii.append(row["radius"][0])
apcorrs.append(row["apcorr"][0])
skyins.append(row["skyin"][0])
skyouts.append(row["skyout"][0])
if self.model.meta.resample.pixel_scale_ratio is not None:
# pixel_scale_ratio is the ratio of the resampled to the native
# pixel scale (values < 1 have smaller resampled pixels)
pixel_scale_ratio = self.model.meta.resample.pixel_scale_ratio
else:
log.warning(
"model.meta.resample.pixel_scale_ratio was not "
"found. Assuming the native detector pixel scale "
"(i.e., pixel_scale_ratio = 1)"
)
pixel_scale_ratio = 1.0
params["aperture_ee"] = self.aperture_ee
params["aperture_radii"] = np.array(radii) / pixel_scale_ratio
params["aperture_corrections"] = np.array(apcorrs)
skyins = np.unique(skyins)
skyouts = np.unique(skyouts)
if len(skyins) != 1 or len(skyouts) != 1:
raise RuntimeError(
"Expected to find only one value for skyin "
"and skyout in the APCORR reference file for "
"a given selector."
)
params["bkg_aperture_inner_radius"] = skyins[0] / pixel_scale_ratio
params["bkg_aperture_outer_radius"] = skyouts[0] / pixel_scale_ratio
return params
@lazyproperty
def abvega_offset(self):
"""
Offset to convert from AB to Vega magnitudes.
Returns
-------
abvega_offset : float
The value ``m_AB - m_Vega``.
"""
if self.abvegaoffset_filename is None:
log.warning(
"ABVEGAOFFSET reference file was not input. "
"Catalog Vega magnitudes are not correct."
)
return 0.0
if self.instrument in ("NIRCAM", "NIRISS"):
selector = {"filter": self.filtername, "pupil": self.pupil}
elif self.instrument == "MIRI":
selector = {"filter": self.filtername}
elif self.instrument == "FGS":
selector = {"detector": self.detector}
else:
raise RuntimeError(f"{self.instrument} is not a valid instrument")
abvegaoffset_model = ABVegaOffsetModel(self.abvegaoffset_filename)
offsets_table = abvegaoffset_model.abvega_offset
try:
mask_idx = [offsets_table[key] == value for key, value in selector.items()]
except KeyError as badkey:
raise KeyError(
f"{badkey} not found in ABVEGAOFFSET reference file {self.abvegaoffset_filename}"
) from None
row = offsets_table[np.logical_and.reduce(mask_idx)]
if len(row) == 0:
raise RuntimeError(
"Did not find matching row in ABVEGAOFFSET "
f"reference file {self.abvegaoffset_filename}"
)
if len(row) > 1:
raise RuntimeError(
"Found more than one matching row in "
"ABVEGAOFFSET reference file "
f"{self.abvegaoffset_filename}"
)
abvega_offset = row["abvega_offset"][0]
log.info(f"AB to Vega magnitude offset {abvega_offset:.5f}")
abvegaoffset_model.close()
return abvega_offset