Source code for gemini_instruments.niri.adclass

from astrodata import astro_data_tag, TagSet, astro_data_descriptor, returns_list
from ..gemini import AstroDataGemini, use_keyword_if_prepared
import math
import re

from . import lookup
from .. import gmu
from ..common import build_ir_section, build_group_id

[docs] class AstroDataNiri(AstroDataGemini): # NIRI has no specific keyword overrides @staticmethod def _matches_data(source): return source[0].header.get('INSTRUME', '').upper() == 'NIRI' @astro_data_tag def _tag_instrument(self): return TagSet(['NIRI']) @astro_data_tag def _tag_dark(self): if self.phu.get('OBSTYPE') == 'DARK': return TagSet(['DARK', 'CAL'], blocks=['SPECT', 'IMAGE']) @astro_data_tag def _tag_arc(self): if self.phu.get('OBSTYPE') == 'ARC': return TagSet(['ARC']) @astro_data_tag def _tag_flat(self): if self.phu.get('OBSTYPE') == 'FLAT': return TagSet(['CAL', 'FLAT']) @astro_data_tag def _tag_image(self): if 'grism' not in self.phu.get('FILTER3', ''): tags = ['IMAGE'] if self.phu.get('OBJECT', '').upper() == 'TWILIGHT': tags.extend(['CAL', 'FLAT', 'TWILIGHT']) return TagSet(tags) @astro_data_tag def _tag_spect(self): if 'grism' in self.phu.get('FILTER3', ''): return TagSet(['SPECT', 'LS'])
[docs] @astro_data_descriptor def array_section(self, pretty=False): """ Returns the section covered by the array(s) relative to the detector frame. For example, this can be the position of multiple amps read within a CCD. If pretty is False, a tuple of 0-based coordinates is returned with format (x1, x2, y1, y2). If pretty is True, a keyword value is returned without parsing as a string. In this format, the coordinates are generally 1-based. One tuple or string is return per extension/array, in a list. If the method is called on a single slice, the section is returned as a tuple or a string. Parameters ---------- pretty : bool If True, return the formatted string found in the header. Returns ------- tuple of integers or list of tuples Position of extension(s) using Python slice values str/list of str Position of extension(s) using an IRAF section format (1-based) """ return build_ir_section(self, pretty)
[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" # Use the lookup dict, keyed on camera, focal_plane_mask and grism camera = self.camera() try: disperser = self.disperser(stripID=True)[0:6] except TypeError: disperser = None fpmask = self.focal_plane_mask(stripID=True) try: wave_in_angstroms = lookup.spec_wavelengths[camera, fpmask, disperser][0] except KeyError: return None return gmu.convert_units('angstroms', wave_in_angstroms, output_units)
[docs] @astro_data_descriptor def data_section(self, pretty=False): """ Returns the rectangular section that includes the pixels that would be exposed to light. If pretty is False, a tuple of 0-based coordinates is returned with format (x1, x2, y1, y2). If pretty is True, a keyword value is returned without parsing as a string. In this format, the coordinates are generally 1-based. One tuple or string is return per extension/array, in a list. If the method is called on a single slice, the section is returned as a tuple or a string. Parameters ---------- pretty : bool If True, return the formatted string found in the header. Returns ------- tuple of integers or list of tuples Location of the pixels exposed to light using Python slice values. string or list of strings Location of the pixels exposed to light using an IRAF section format (1-based). """ # All NIRI pixels are data return self._parse_section('FULLFRAME', pretty)
[docs] @astro_data_descriptor def detector_roi_setting(self): """ Returns the ROI setting. Most instruments don't allow this to be changed, so at the Gemini level it just returns 'Fixed' Returns ------- str Name of the ROI setting used, ie, "Fixed" """ data_section = self.data_section() # If we have a list, check they're all the same and take one element if isinstance(data_section, list): assert data_section == data_section[::-1], \ "Multiple extensions with different data_sections" data_section = data_section[0] elif data_section is None: return None x1, x2, y1, y2 = data_section # Check for a sensibly-sized square if x1==0 and y1==0 and x2==y2 and x2 % 256==0: roi_setting = 'Full Frame' if x2==1024 else 'Central{}'.format(x2) else: roi_setting = 'Custom' return roi_setting
[docs] @astro_data_descriptor def detector_section(self, pretty=False): """ Returns the section covered by the detector relative to the whole mosaic of detectors. If pretty is False, a tuple of 0-based coordinates is returned with format (x1, x2, y1, y2). If pretty is True, a keyword value is returned without parsing as a string. In this format, the coordinates are generally 1-based. One tuple or string is return per extension/array, in a list. If the method is called on a single slice, the section is returned as a tuple or a string. Parameters ---------- pretty : bool If True, return the formatted string found in the header. Returns ------- tuple of integers or list of tuples Position of the detector using Python slice values. string or list of strings Position of the detector using an IRAF section format (1-based). """ return self.array_section(pretty=pretty)
[docs] @astro_data_descriptor def detector_x_offset(self): """ Returns the offset from the reference position in pixels along the positive x-direction of the detector Returns ------- float The offset in pixels """ try: offset = self.phu.get('POFFSET') / self.pixel_scale() except TypeError: # either is None return None # Flipped if on bottom port unless AO is operating return -offset if (self.phu.get('INPORT')==1 and not self.is_ao()) else offset
[docs] @astro_data_descriptor def detector_y_offset(self): """ Returns the offset from the reference position in pixels along the positive y-direction of the detector Returns ------- float The offset in pixels """ try: return -self.phu.get('QOFFSET') / self.pixel_scale() except TypeError: # either is None return None
[docs] @astro_data_descriptor def disperser(self, stripID=False, pretty=False): """ Returns the name of the disperser. The component ID can be removed with either 'stripID' or 'pretty' set to True. Parameters ---------- stripID : bool If True, removes the component ID and returns only the name of the disperser. pretty : bool Does nothing. Exists for compatibility. Returns ------- str The name of the disperser with or without the component ID. """ stripID |= pretty try: filter3 = self.phu['FILTER3'] except KeyError: return None if 'grism' in filter3: return gmu.removeComponentID(filter3) if stripID else filter3 else: return 'MIRROR'
[docs] @astro_data_descriptor def dispersion(self, asMicrometers=False, asNanometers=False, asAngstroms=False): """ Returns the dispersion in meters per pixel as a list (one value per extension) or a float if used on a single-extension slice. It is possible to control the units of wavelength using the input arguments. 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 ------- list/float The dispersion(s) """ camera = self.camera() try: disperser = self.disperser(stripID=True)[0:6] except TypeError: disperser = None try: dispersion = lookup.dispersion_by_config[camera, disperser] except KeyError: dispersion = None unit_arg_list = [asMicrometers, asNanometers, asAngstroms] output_units = "meters" # By default 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" if dispersion is not None: dispersion = gmu.convert_units('angstroms', dispersion, output_units) if not self.is_single: dispersion = [dispersion] * len(self) return dispersion
[docs] @returns_list @astro_data_descriptor def dispersion_axis(self): return 1
[docs] @astro_data_descriptor def filter_name(self, stripID=False, pretty=False): #TODO: Complete rewrite here so serious testing required """ Returns the name of the filter(s) used. If a combination of filters is used, the filter names will be join into a unique string with '&' as separator. The component IDs can be removed from the filter names. Alternatively, a single descriptive filter name can be returned, based on a lookup table. Parameters ---------- stripID : bool If True, removes the component ID(s) and returns only the name(s) of the filter(s). pretty : bool Returns a single filter name Returns ------- str The name of the filter combination with or without the component ID. """ raw_filters = [self._may_remove_component('FILTER{}'.format(i), stripID, pretty) for i in [1,2,3]] # eliminate any open/grism/pupil from the list filters = [f for f in raw_filters if f is not None and not any(x in f.lower() for x in ['open', 'grism', 'pupil'])] filters.sort() if 'blank' in filters: return 'blank' if not filters: return 'open' if pretty: try: return lookup.filter_name_mapping[tuple(filters) if len(filters)>1 else filters[0]] except KeyError: pass return '&'.join(filters)
@returns_list @use_keyword_if_prepared @astro_data_descriptor def gain(self): """ Returns the gain (electrons/ADU) for each extension Returns ------- float/list gain """ return lookup.array_properties.get('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, mode of observation, and data type will have its own rules. Returns ------- str A group ID for compatible data. """ tags = self.tags if 'DARK' in tags: desc_list = ['exposure_time', 'coadds'] else: desc_list = ['observation_id', 'filter_name', 'camera'] desc_list.extend(['read_mode', 'well_depth_setting', 'detector_section']) if 'SPECT' in tags: desc_list.extend(['disperser', 'focal_plane_mask']) if 'IMAGE' in tags and 'FLAT' in tags: additional_item = 'NIRI_IMAGE_TWILIGHT' if 'TWILIGHT' in tags \ else 'NIRI_IMAGE_FLAT' else: additional_item = None return build_group_id(self, desc_list, prettify=['filter_name', 'disperser', 'focal_plane_mask'], additional=additional_item)
[docs] @astro_data_descriptor def nominal_photometric_zeropoint(self): """ Returns the nominal photometric zeropoint (i.e., magnitude corresponding to 1 pixel count) for each extension Returns ------- float/list of floats Photometric zeropoint """ gain = self.gain() filter_name = self.filter_name(pretty=True) camera = self.camera() in_adu = self.is_in_adu() zpt = lookup.nominal_zeropoints.get((filter_name, camera)) # Zeropoints in table are for electrons, so subtract 2.5*log10(gain) # if the data are in ADU if self.is_single: try: return zpt - (2.5 * math.log10(gain) if in_adu else 0) except TypeError: return None else: return [zpt - (2.5 * math.log10(g) if in_adu else 0) if zpt and g else None for g in gain]
@use_keyword_if_prepared @astro_data_descriptor def non_linear_level(self): """ Returns the level at which the data become non-linear, in units of the data. Returns ------- int/list non-linearity level """ sat_level = self.saturation_level() linear_limit = lookup.array_properties['linearlimit'] if isinstance(sat_level, list): return [(int(linear_limit * s) if s else None) for s in sat_level] else: return int(linear_limit * sat_level) if sat_level else None
[docs] @astro_data_descriptor def pupil_mask(self, stripID=False, pretty=False): """ Returns the name of the pupil mask used for the observation Returns ------- str the pupil mask """ try: filter3 = self.phu['FILTER3'] except KeyError: return None if filter3.startswith('pup'): return gmu.removeComponentID(filter3) if pretty or stripID \ else filter3 else: return 'MIRROR'
[docs] @astro_data_descriptor def read_mode(self): """ Returns the readout mode used for the observation. This has one of 3 settings, depending on the number of reads and averages. If these numbers do not conform to a standard setting, 'Invalid' is returned Returns ------- str the read mode used """ setting = (self.phu.get('LNRS'), self.phu.get('NDAVGS')) if setting == (16,16): return 'Low Background' elif setting == (1,16): return 'Medium Background' elif setting == (1,1): return 'High Background' else: return 'Unknown'
@returns_list @use_keyword_if_prepared @astro_data_descriptor def read_noise(self): """ Returns the read noise in electrons, as a list unless called on a single-extension slice. Returns ------- float/list read noise """ read_mode = self.read_mode() if read_mode == 'Low Background': key = 'lowreadnoise' elif read_mode == 'High Background': key = 'readnoise' else: key = 'medreadnoise' try: read_noise = lookup.array_properties[key] except KeyError: return None # Because coadds are summed, read noise increases by sqrt(COADDS) return read_noise * math.sqrt(self.coadds()) @use_keyword_if_prepared @astro_data_descriptor def saturation_level(self): """ Returns the saturation level of the data, in the units of the data Returns ------- int/list saturation level """ coadds = self.coadds() gain = self.gain() well = lookup.array_properties.get(self.well_depth_setting().lower()+'well') if self.is_single: try: return int(well * coadds / gain) except TypeError: return None else: return [int(well * coadds / g) if g and well else None for g in gain]
[docs] @astro_data_descriptor def slit_width(self): """ Returns the width of the slit in arcseconds Returns ------- float/None the slit width in arcseconds """ fpmask = self.focal_plane_mask(pretty=True) if 'pix' in fpmask: m = re.match('f(.*)-(.*)pix', fpmask) return int(m.group(2)) * 0.7 / int(m.group(1)) return None
[docs] @astro_data_descriptor def well_depth_setting(self): """ Returns a string describing the well-depth setting of the instrument. NIRI has 'Shallow' and 'Deep' options. 'Invalid' is returned if the bias voltage doesn't match either setting. Returns ------- str the well-depth setting """ try: biasvolt = self.phu['A_VDDUC'] - self.phu['A_VDET'] if abs(biasvolt - lookup.array_properties['shallowbias']) < 0.05: return 'Shallow' elif abs(biasvolt - lookup.array_properties['deepbias']) < 0.05: return 'Deep' except KeyError: pass return 'Unknown'