Source code for gemini_instruments.gsaoi.adclass
import math
from astrodata import astro_data_tag, TagSet, astro_data_descriptor, returns_list
from ..gemini import AstroDataGemini, use_keyword_if_prepared
from .. import gmu
from ..common import build_group_id
from . import lookup
[docs]
class AstroDataGsaoi(AstroDataGemini):
__keyword_dict = dict(array_section='CCDSEC',
camera='DETECTOR',
central_wavelength='WAVELENG',
detector_name='DETECTOR',
)
@staticmethod
def _matches_data(source):
return source[0].header.get('INSTRUME', '').upper() == 'GSAOI'
@astro_data_tag
def _tag_instrument(self):
return TagSet(['GSAOI'])
@astro_data_tag
def _tag_dark(self):
if self.phu.get('OBSTYPE') == 'DARK':
return TagSet(['DARK', 'CAL'], blocks=['IMAGE'])
@astro_data_tag
def _tag_image(self):
tags = ['IMAGE']
if self.phu.get('OBSTYPE') == 'FLAT':
tags.extend(['FLAT', 'CAL'])
if 'DOMEFLAT' in self.phu.get('OBJECT', '').upper():
tags.extend(['DOMEFLAT', 'FLAT', 'CAL'])
elif 'TWILIGHT' in self.phu.get('OBJECT', '').upper():
tags.extend(['TWILIGHT', 'FLAT', 'CAL'])
return TagSet(tags)
# Kept separate from _tag_image, because some conditions defined
# at a higher level conflict with this
@astro_data_tag
def _type_gcal_lamp(self):
obj = self.phu.get('OBJECT', '').upper()
if obj == 'DOMEFLAT':
return TagSet(['LAMPON'])
elif obj == 'DOMEFLAT OFF':
return TagSet(['LAMPOFF'])
[docs]
@returns_list
@astro_data_descriptor
def array_name(self):
"""
Returns a list of the array names of each extension
Returns
-------
list/str
names of the arrays
"""
try:
return self.hdr['ARRAYID']
except KeyError:
# Data have been mosaicked, so return the detector name
# (as a single-element list if necessary)
return self.phu.get('DETECTOR')
[docs]
@astro_data_descriptor
def central_wavelength(self, asMicrometers=False, asNanometers=False,
asAngstroms=False):
"""
Returns the central wavelength in meters or the specified units
Parameters
----------
asMicrometers : bool
If True, return the wavelength in microns
asNanometers : bool
If True, return the wavelength in nanometers
asAngstroms : bool
If True, return the wavelength in Angstroms
Returns
-------
float
The central wavelength setting
"""
unit_arg_list = [asMicrometers, asNanometers, asAngstroms]
if unit_arg_list.count(True) == 1:
# Just one of the unit arguments was set to True. Return the
# central wavelength in these units
if asMicrometers:
output_units = "micrometers"
if asNanometers:
output_units = "nanometers"
if asAngstroms:
output_units = "angstroms"
else:
# Either none of the unit arguments were set to True or more than
# one of the unit arguments was set to True. In either case,
# return the central wavelength in the default units of meters.
output_units = "meters"
central_wavelength = self.phu.get('WAVELENG', -1)
if central_wavelength < 0.0:
return None
else:
return gmu.convert_units('angstroms', central_wavelength,
output_units)
@returns_list
@use_keyword_if_prepared
@astro_data_descriptor
def gain(self):
"""
Returns the gain (electrons/ADU) of the extensions
Returns
-------
list/float
gain (e/ADU)
"""
return self._look_up_arr_property('gain')
[docs]
@astro_data_descriptor
def group_id(self):
"""
Returns a string representing a group of data that are compatible
with each other. This is used when stacking, for example. Each
instrument and mode of observation will have its own rules.
Returns
-------
str
A group ID for compatible data
"""
# Additional descriptors required for each frame type
# Note: dark_id and flat_twilight are not in common use.
# Those are therefore place holder with initial guess
#
# For flat_id, "on" and "off" domeflats are not taken
# with the same obsID, so that association cannot be
# made. The only other sensible characteristic would
# be to require a timeframe check, eg. within X hours.
#
# The UT date and local date change in the middle of the
# night. Can't reliably use that. Thought for a while
# using the either the fake UT or equivalently the date
# string in the file would work, but found at least one
# case where the flat sequence is taken across the 2pm
# filename change.
#
# Because the group_id is a static string, I can't use
# if-tricks or or-tricks. The only thing that doesn't
# change is the program ID. That's a bit procedural though
# but that's the only thing left.
#
#dark_id = ["exposure_time", "coadds"]
flat_id = ["filter_name", "exposure_time", "program_id"]
#flat_twilight_id = ["filter_name"]
science_id = ["observation_id", "filter_name", "exposure_time"]
# Associate rules with data type
# Note: add darks and twilight if necessary later.
if 'FLAT' in self.tags:
id_descriptor_list = flat_id
else:
id_descriptor_list = science_id
# Add in all the common descriptors required
id_descriptor_list.extend(["read_mode", "detector_section"])
return build_group_id(self, id_descriptor_list, prettify=('filter_name'))
[docs]
@astro_data_descriptor
def is_coadds_summed(self):
"""
Tells whether or not the co-adds have been summed. If not, they
have been averaged. GSAOI averages them.
Returns
-------
bool
True if the data has been summed. False if it has been averaged.
"""
return False
[docs]
@astro_data_descriptor
def nominal_photometric_zeropoint(self):
"""
Returns the nominal zeropoints (i.e., the magnitude corresponding to
a pixel value of 1) for the extensions in an AD object.
Zeropoints in table are for electrons, so subtract 2.5*lg(gain)
if the data are in ADU
Returns
-------
float/list
zeropoint values, one per SCI extension
"""
def _zpt(array, filt, gain, in_adu):
zpt = lookup.nominal_zeropoints.get((filt, array))
try:
return zpt - (2.5 * math.log10(gain) if in_adu else 0)
except TypeError:
return None
gain = self.gain()
filter_name = self.filter_name(pretty=True)
array_name = self.array_name()
in_adu = self.is_in_adu()
if self.is_single:
return _zpt(array_name, filter_name, gain, in_adu)
else:
return [_zpt(a, filter_name, g, in_adu)
for a, g in zip(array_name, gain)]
@use_keyword_if_prepared
@astro_data_descriptor
def non_linear_level(self):
"""
Returns the level at which the data become non-linear, in the units
of the data.
Returns
-------
int/list
Value at which the data become non-linear
"""
# Column 3 gives the fraction of the saturation level at which
# the data become non-linear
fraction = self._look_up_arr_property('linlimit')
sat_level = self.saturation_level()
if self.is_single:
try:
return fraction * sat_level
except TypeError:
return None
else:
return [f * s if f and s else None
for f, s in zip(fraction, sat_level)]
@returns_list
@use_keyword_if_prepared
@astro_data_descriptor
def read_noise(self):
"""
Returns the read noise of each extension in electrons, as a float or
a list of floats
Returns
-------
float/list
read noise in electrons
"""
# Column 0 has the read noise (for 1 coadd)
raw_read_noise = self._look_up_arr_property('readnoise')
coadd_factor = math.sqrt(self.coadds())
if self.is_single:
try:
return round(raw_read_noise / coadd_factor, 2)
except TypeError:
return None
else:
return [round(r / coadd_factor, 2) if r else None
for r in raw_read_noise]
[docs]
@astro_data_descriptor
def read_speed_setting(self):
"""
Returns a string describing the read speed setting, as used in the OT
Returns
-------
str
read speed setting
"""
# The number of non-destructive reads is the key in the dict
return lookup.read_modes.get(self.phu.get('LNRS'), 'Unknown')
@use_keyword_if_prepared
@astro_data_descriptor
def saturation_level(self):
"""
Returns the saturation level in the units of the data for each
extension, as a list or a single value. Values are obtained from
a LUT which has the saturation level in ADU, so this is converted
to electrons using the original gain (from the LUT) and then
divided by the current gain (from the descriptor), which should
have been set to 1.0 if the data have been converted to electrons.
Returns
-------
int/list
saturation level
"""
welldepth = self._look_up_arr_property('welldepth')
orig_gain = self._look_up_arr_property('gain')
gain = self.gain()
if self.is_single:
try:
return welldepth * orig_gain / gain
except TypeError:
return None
return [w * o / g if w and o and g else None for w, o, g in zip(welldepth, orig_gain, gain)]
[docs]
@astro_data_descriptor
def wcs_ra(self):
"""
Returns the Right Ascension of the center of the field based on the
WCS rather than the RA keyword. This just uses the CRVAL1 keyword.
Returns
-------
float
right ascension in degrees
"""
# Try the first (only if sliced) extension, then the PHU
try:
h = self[0].hdr
crval = h['CRVAL1']
ctype = h['CTYPE1']
except KeyError:
crval = self.phu.get('CRVAL1')
ctype = self.phu.get('CTYPE1')
return crval if ctype == 'RA---TAN' else None
[docs]
@astro_data_descriptor
def wcs_dec(self):
"""
Returns the Declination of the center of the field based on the
WCS rather than the DEC keyword. This just uses the CRVAL2 keyword.
Returns
-------
float
declination in degrees
"""
# Try the first (only if sliced) extension, then the PHU
try:
h = self[0].hdr
crval = h['CRVAL2']
ctype = h['CTYPE2']
except KeyError:
crval = self.phu.get('CRVAL2')
ctype = self.phu.get('CTYPE2')
return crval if ctype == 'DEC--TAN' else None
def _look_up_arr_property(self, attr):
"""
Helper function to extract information from the array_properties dict
Will return a list or a value, depending on the object it's called on
Returns
-------
list/float
the required data
"""
read_speed = self.read_speed_setting()
array_names = self.array_name()
if isinstance(array_names, list):
return [getattr(lookup.array_properties.get((read_speed, a)),
attr, None) for a in array_names]
else:
return getattr(lookup.array_properties.get((read_speed,
array_names)), attr, None)