From 02beb650ad8a9d313c12d014c507e98918a65dd7 Mon Sep 17 00:00:00 2001 From: Christopher Montalban Date: Wed, 6 Mar 2024 15:59:56 -0300 Subject: [PATCH 1/4] Correct Header keywords --- soar_instruments/goodman/adclass.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/soar_instruments/goodman/adclass.py b/soar_instruments/goodman/adclass.py index 0e34a1b..6003e74 100644 --- a/soar_instruments/goodman/adclass.py +++ b/soar_instruments/goodman/adclass.py @@ -15,7 +15,7 @@ def _tag_instrument(self): @astro_data_tag def _tag_arc(self): - if self.phu.get('OBSTYPE') == 'COMP': + if self.phu.get('OBSTYPE') == 'ARC': return TagSet(['ARC', 'CAL']) @astro_data_tag @@ -25,7 +25,7 @@ def _tag_bias(self): @astro_data_tag def _tag_flat(self): - if self.phu.get('OBSTYPE') == 'FLAT': + if self.phu.get('OBSTYPE') == 'LAMPFLAT': return TagSet(['FLAT', 'CAL']) @astro_data_tag @@ -40,12 +40,12 @@ def _tag_blue(self): @astro_data_tag def _tag_image(self): - if self.phu.get('CAM_ANG') == 0 and self.phu.get('WAVMOD') == 'Imaging': + if self.phu.get('CAM_ANG') == 0 and self.phu.get('WAVMOD') == 'IMAGING': return TagSet(['IMAGE']) @astro_data_tag def _tag_spect(self): - if self.phu.get('CAM_ANG') != 0 and self.phu.get('WAVMOD') != 'Imaging': + if self.phu.get('CAM_ANG') != 0 and self.phu.get('WAVMOD') != 'IMAGING': return TagSet(['SPECT']) From bd6aab0323a11deb5776bc4022ac1f6935a2fa45 Mon Sep 17 00:00:00 2001 From: Christopher Montalban Date: Wed, 6 Mar 2024 16:16:01 -0300 Subject: [PATCH 2/4] Change more headers keywords --- soar_instruments/goodman/adclass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soar_instruments/goodman/adclass.py b/soar_instruments/goodman/adclass.py index 6003e74..122c3cf 100644 --- a/soar_instruments/goodman/adclass.py +++ b/soar_instruments/goodman/adclass.py @@ -40,12 +40,12 @@ def _tag_blue(self): @astro_data_tag def _tag_image(self): - if self.phu.get('CAM_ANG') == 0 and self.phu.get('WAVMOD') == 'IMAGING': + if self.phu.get('CAM_TARG') == 0 and self.phu.get('WAVMODE') == 'IMAGING': return TagSet(['IMAGE']) @astro_data_tag def _tag_spect(self): - if self.phu.get('CAM_ANG') != 0 and self.phu.get('WAVMOD') != 'IMAGING': + if self.phu.get('CAM_TARG') != 0 and self.phu.get('WAVMODE') != 'IMAGING': return TagSet(['SPECT']) From 41b7861b7b69d9131b232268126495e09217c75d Mon Sep 17 00:00:00 2001 From: Christopher Montalban Date: Wed, 6 Mar 2024 16:45:03 -0300 Subject: [PATCH 3/4] Add posible keywords in INSTRUME header keyword --- soar_instruments/goodman/adclass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soar_instruments/goodman/adclass.py b/soar_instruments/goodman/adclass.py index 122c3cf..937c5e4 100644 --- a/soar_instruments/goodman/adclass.py +++ b/soar_instruments/goodman/adclass.py @@ -6,8 +6,8 @@ class AstroDataGOODMAN(AstroDataSOAR): @staticmethod - def _matches_data(data_provider): - return data_provider.phu.get('INSTRUME', '') == 'Goodman Spectro' + def _matches_data(source): + return source[0].header.get('INSTRUME', '').upper() in ('Goodman Spectro', 'ghts_red', 'ghts_blue') @astro_data_tag def _tag_instrument(self): From 512a8de805129f3149c3998ab1d39a0ce998ddc3 Mon Sep 17 00:00:00 2001 From: Christopher Montalban Date: Fri, 8 Mar 2024 15:09:07 -0300 Subject: [PATCH 4/4] Added all the basic tags of SOAR and Goodman --- soar_instruments/__init__.py | 10 +- soar_instruments/goodman/__init__.py | 6 +- soar_instruments/goodman/adclass.py | 167 ++++++- soar_instruments/soar/__init__.py | 2 +- soar_instruments/soar/adclass.py | 652 ++++++++++++++++++++++++++- 5 files changed, 801 insertions(+), 36 deletions(-) diff --git a/soar_instruments/__init__.py b/soar_instruments/__init__.py index 3fd7ad8..780781f 100644 --- a/soar_instruments/__init__.py +++ b/soar_instruments/__init__.py @@ -1,4 +1,12 @@ +# Import the modules under this package, to trigger any class +# registering that may be neededfrom astrodata import version + +from astrodata import version +__version__ = version() + +import astrodata + from . import soar from . import sami -from . import goodman +from . import goodman \ No newline at end of file diff --git a/soar_instruments/goodman/__init__.py b/soar_instruments/goodman/__init__.py index 0dc4d6a..5cc917f 100644 --- a/soar_instruments/goodman/__init__.py +++ b/soar_instruments/goodman/__init__.py @@ -1,6 +1,6 @@ -__all__ = ['AstroDataGOODMAN'] +__all__ = ['AstroDataGoodman'] from astrodata import factory -from .adclass import AstroDataGOODMAN +from .adclass import AstroDataGoodman -factory.addClass(AstroDataGOODMAN) +factory.addClass(AstroDataGoodman) \ No newline at end of file diff --git a/soar_instruments/goodman/adclass.py b/soar_instruments/goodman/adclass.py index 937c5e4..5ac91d6 100644 --- a/soar_instruments/goodman/adclass.py +++ b/soar_instruments/goodman/adclass.py @@ -1,13 +1,13 @@ -import re - -from astrodata import astro_data_tag, TagSet, astro_data_descriptor, returns_list +from astrodata import (astro_data_tag, astro_data_descriptor, returns_list, + TagSet, Section) from ..soar import AstroDataSOAR +from . import lookup -class AstroDataGOODMAN(AstroDataSOAR): +class AstroDataGoodman(AstroDataSOAR): @staticmethod def _matches_data(source): - return source[0].header.get('INSTRUME', '').upper() in ('Goodman Spectro', 'ghts_red', 'ghts_blue') + return source[0].header.get('INSTRUME', '') in ('ghts_red', 'ghts_blue', 'ghts_red_imager', 'ghts_blue_imager') @astro_data_tag def _tag_instrument(self): @@ -21,37 +21,162 @@ def _tag_arc(self): @astro_data_tag def _tag_bias(self): if self.phu.get('OBSTYPE') == 'BIAS': - return TagSet(['BIAS', 'CAL']) + return TagSet(['BIAS', 'CAL'], blocks=['IMAGE','SPECT']) @astro_data_tag def _tag_flat(self): if self.phu.get('OBSTYPE') == 'LAMPFLAT': return TagSet(['FLAT', 'CAL']) + + @astro_data_tag + def _tag_domeflat(self): + if self.phu.get('OBJECT', '').upper() == 'DFLAT': + return TagSet(['DOMEFLAT', 'CAL', 'FLAT']) @astro_data_tag - def _tag_red(self): + def _tag_camera(self): if self.phu.get('INSTCONF') == 'Red': return TagSet(['RED']) + elif self.phu.get('INSTCONF') == 'Blue': + return TagSet(['BLUE']) + else: #No se si esta parte es necesaria, podria usar un raise + try: + return TagSet(['NEITHER_BLUE_OR_RED']) + except TypeError: # either is None + return None @astro_data_tag - def _tag_blue(self): - if self.phu.get('INSTCONF') == 'Blue': - return TagSet(['BLUE']) + def _tag_grating(self): + grating_map = { + '400_SYZY': 'GRATING_400', + '600_SYZY': 'GRATING_600', + '930_SYZY': 'GRATING_930', + '1200_SYZY': 'GRATING_1200', + '2100_SYZY': 'GRATING_2100' + } + + grating_value = self.phu.get('GRATING') + if grating_value in grating_map: + return TagSet([grating_map[grating_value]]) + else: + # Si 'GRATING' no coincide con ninguno de los valores conocidos, devuelve 'GRATING_CUSTOM' + return TagSet(['GRATING_CUSTOM']) + + # @astro_data_tag + # def _tag_blue(self): + # if self.phu.get('INSTCONF') == 'Blue': + # return TagSet(['BLUE']) @astro_data_tag - def _tag_image(self): - if self.phu.get('CAM_TARG') == 0 and self.phu.get('WAVMODE') == 'IMAGING': + def _tag_mode(self): + if self.phu.get('WAVMODE') == 'IMAGING': return TagSet(['IMAGE']) - - @astro_data_tag - def _tag_spect(self): - if self.phu.get('CAM_TARG') != 0 and self.phu.get('WAVMODE') != 'IMAGING': - return TagSet(['SPECT']) + elif self.phu.get('WAVMODE') != 'IMAGING': + return TagSet(['SPECT']) + else: #No se si esta parte es necesaria, podria usar un raise + return TagSet(['Error_WAVMODE']) + + # @astro_data_tag + # def _tag_spect(self): + # if self.phu.get('CAM_ANG') != 0 and self.phu.get('WAVMOD') != 'Imaging': + # return TagSet(['SPECT']) + + # @astro_data_descriptor + # def dec(self): + # """ + # Returns the Declination of the center of field in degrees. Since a + # fiber is used it coincides with the position of the target. For code + # re-used, use target_dec() if you really want the position of the target + # rather than the center of the field. + + # Returns + # ------- + # float + # declination in degrees + # """ + # return self.target_dec() + + @astro_data_descriptor + def filter_1(self): + """ + Returns the name of the primary filter in the wheel. + + Returns + ------- + str + primary filter + + """ + return self.phu.get('FILTER') + @astro_data_descriptor + def filter_2(self): + """ + Returns the name of the secondary filter in the wheel. + + Returns + ------- + str + secondary filter + + """ + return self.phu.get('FILTER2') + + @astro_data_descriptor + def grating(self): + """ + Returns the name of the VPH grating + manufacturer name. + + Returns + ------- + str + grating + + """ + return self.phu.get('GRATING') @astro_data_descriptor def instrument(self, generic=False): - if 'goodman' in self.phu.get('INSTRUME', '').lower(): - return 'goodman' - else: - return None + """ + Returns the name of the instrument making the observation + + Parameters + ---------- + generic: boolean + If set, don't specify the specific instrument if there are clones + (e.g., return "ghst" rather than "ghst_red" or "ghst_blue") + + Returns + ------- + str + instrument name + """ + return 'ghst' if generic else self.phu.get('INSTRUME') + + @astro_data_descriptor + def wavelength_config(self): + """ + Returns the name of the VPH grating + configuration mode. + + Returns + ------- + str + grating mode + + """ + return self.phu.get('WAVMODE') + + # @astro_data_descriptor + # def ra(self): + # """ + # Returns the Right Ascension of the center of field in degrees. Since a + # fiber is used it coincides with the position of the target. For code + # re-used, use target_ra() if you really want the position of the target + # rather than the center of the field. + + # Returns + # ------- + # float + # right ascension in degrees + # """ + # return self.target_ra() \ No newline at end of file diff --git a/soar_instruments/soar/__init__.py b/soar_instruments/soar/__init__.py index 97910d8..69c6096 100644 --- a/soar_instruments/soar/__init__.py +++ b/soar_instruments/soar/__init__.py @@ -3,4 +3,4 @@ from astrodata import factory from .adclass import AstroDataSOAR -factory.addClass(AstroDataSOAR) +factory.addClass(AstroDataSOAR) \ No newline at end of file diff --git a/soar_instruments/soar/adclass.py b/soar_instruments/soar/adclass.py index fa40f18..e55f410 100644 --- a/soar_instruments/soar/adclass.py +++ b/soar_instruments/soar/adclass.py @@ -1,26 +1,244 @@ -from astrodata import AstroDataFits, astro_data_tag, astro_data_descriptor, TagSet +# +# SOAR Observatory +# +# Dragons +# soar_instruments +# soar.adclass.py +# ------------------------------------------------------------------------------ +import re +import math +import datetime +import dateutil.parser + +import numpy as np + +from astropy import units as u +from astropy.coordinates import SkyCoord, Angle + +from astrodata import AstroData +from astrodata import astro_data_tag +from astrodata import astro_data_descriptor +from astrodata import TagSet, Section from ..utilities import section_to_tuple -soar_keyword_names = dict( - exposure_time = 'EXPTIME' +#I copy these from gemini.py + +soar_keyword_names = dict( #Those with the '#' aren't implemented yet + adc_status = 'ADCSTAT',# + adc_position = 'ADCPOS',# + airmass = 'AIRMASS', + azimuth = 'MOUNT_AZ', + binning = 'CCDSUM', + camera = 'CAMERA', + camera_angle_in_deg = 'CAM_ANG',# + camera_target_in_deg = 'CAM_TARG',# + camera_temp_in_C = 'CAM_TEMP', + camera_focus = 'CAM_FOC', + collimator_focus = 'COLL_FOC',# + collimator_temp_in_C = 'COL_TEMP',# + dec = 'DEC', + detector_size = 'DETSIZE', # + dispersion_axis = 'DISPAXIS', # + dome_shutter_azimuth = 'DOME_AZ',# + elevation = 'MOUNT_EL', + epoch = 'EQUINOX', + exposure_time = 'EXPTIME', + filter_1 = 'FILTER1', ## + filter_2 = 'FILTER2', ## + focus = 'FOCUS', + gain = 'GAIN', + grating = 'GRATING', # + grating_angle = 'GRT_ANG', # + grating_target = 'GRT_TARG', # + hour_angle = 'HA', + instrument = 'INSTRUME', + local_sidereal_time = 'LST', + environment_wind_at_start = 'ENVWIN', + environment_atm_pressure_at_start = 'ENVPRE', + environment_wind_direction_at_start = 'ENVDIR', + environment_outside_temperature_in_C_at_start = 'ENVTEM', + environment_relative_humidity_at_start = 'ENVHUM', + obs_ra = 'OBSRA', + obs_dec = 'OBSDEC', + observation_id = 'OBSID', + observation_type = 'OBSTYPE', + pos_angle = 'POSANGLE', + ra = 'RA', + read_noise = 'RDNOISE', + rotator = 'ROTATOR', + seeing = 'SEEING', + slit = 'SLIT', + trim_section = 'TRIMSEC', + ut_time = 'UT', + wavelength_config = 'WAVMODE' ) -class AstroDataSOAR(AstroDataFits): +def use_keyword_if_prepared(fn): + """ + A decorator for descriptors. If decorated, the descriptor will bypass its + main code on "PREPARED" data in favour of simply returning the value of + the associated header keyword (as defined by the "_keyword_for" method) + if this exists in all the headers (if the keyword is missing, it will + execute the code in the descriptor method). + """ + def gn(self): + if "PREPARED" in self.tags: + try: + return self.hdr[self._keyword_for(fn.__name__)] + except (KeyError, AttributeError): + pass + return fn(self) + return gn + + +# ------------------------------------------------------------------------------ +class AstroDataSOAR(AstroData): __keyword_dict = soar_keyword_names @staticmethod - def _matches_data(data_provider): - if data_provider.phu.get('OBSERVAT', '').upper() == 'SOAR' or \ - data_provider.phu.get('TELESCOP', '') == 'SOAR 4.1m': - return True - else: - return False + def _matches_data(source): + obs = source[0].header.get('OBSERVAT', '').upper() + tel = source[0].header.get('TELESCOP', '').upper() + + isSOAR = (obs == 'SOAR' or tel == 'SOAR 4.1M') + return isSOAR @astro_data_tag def _type_observatory(self): return TagSet(['SOAR']) + # @astro_data_tag #Pendiente, Ver como clasificar entre imagen y espectro + # def _type_mode(self): + # mode = self.phu.get(self._keyword_for('observation_mode'), '').upper() + + # if mode: + # tags = [mode] + # if mode != 'IMAGE': + # # assume SPECT + # tags.append('SPECT') + # return TagSet(tags) + + @astro_data_tag + def _status_prepared(self): + if any(('PREPAR' in kw) for kw in self.phu): + return TagSet(['PREPARED']) + else: + return TagSet(['UNPREPARED']) + + @astro_data_tag #No se que es GEM-TLM, pendiente + def _status_raw(self): + if 'GEM-TLM' not in self.phu: + return TagSet(['RAW']) + + @astro_data_tag #Pendiente ver como se aƱaden estos tags + def _status_overscan(self): + found = [] + for pattern, tag in (('TRIMOVER', 'OVERSCAN_TRIMMED'), ('SUBOVER', 'OVERSCAN_SUBTRACTED')): + if any((pattern in kw) for kw in self.phu): + found.append(tag) + if found: + return TagSet(found) + + @astro_data_tag #Entiedno que aqui se definene los procesos intermedios tras los recipes + def _status_processed_cals(self): + kwords = {'PROCARC', 'GBIAS', 'PROCBIAS', 'PROCDARK', + 'GIFLAT', 'PROCFLAT', 'GIFRINGE', 'PROCFRNG', 'PROCSTND', 'PROCILLM'} + + if set(self.phu.keys()) & kwords: + return TagSet(['PROCESSED']) + + @astro_data_tag #Pendiente + def _status_processed_science(self): + kwords = {'GMOSAIC', 'PROCSCI'} + + if self.phu['OBSTYPE'] == 'OBJECT' and set(self.phu.keys()) & kwords: + return TagSet(['PROCESSED_SCIENCE', 'PROCESSED'], blocks=['RAW']) + + @astro_data_tag #Pendiente + def _type_extracted(self): + if 'EXTRACT' in self.phu: + return TagSet(['EXTRACTED']) + + # # GCALFLAT and the LAMPON/LAMPOFF are kept separated because the #Entiendo que esta parte tiene que ver con las lamparas + # # PROCESSED status will cancel the tags for lamp status, but the + # # GCALFLAT is still needed + # @astro_data_tag + # def _type_gcalflat(self): + # gcallamp = self.phu.get('GCALLAMP') + # if gcallamp == 'IRhigh' or (gcallamp is not None and gcallamp.startswith('QH')): + # return TagSet(['GCALFLAT', 'FLAT', 'CAL']) + + # @astro_data_tag + # def _type_gcal_lamp(self): + # if self.phu['INSTRUME'].startswith('GMOS'): + # return + + # gcallamp = self.phu.get('GCALLAMP') + # if gcallamp == 'IRhigh' or (gcallamp is not None and gcallamp.startswith('QH')): + # shut = self.phu.get('GCALSHUT') + # if shut == 'OPEN': + # return TagSet(['GCAL_IR_ON', 'LAMPON'], blocked_by=['PROCESSED']) + # elif shut == 'CLOSED': + # return TagSet(['GCAL_IR_OFF', 'LAMPOFF'], blocked_by=['PROCESSED']) + # elif self.phu.get('GCALLAMP') == 'No Value' and \ + # self.phu.get('GCALSHUT') == 'CLOSED': + # return TagSet(['GCAL_IR_OFF', 'LAMPOFF'], blocked_by=['PROCESSED']) + + def _ra(self): + """ + Parse RA from header. + + Utility method to pull the right ascension from the header, parsing text if appropriate. + + Returns + ------- + float : right ascension in degrees, or None + """ + ra = self.phu.get(self._keyword_for('ra'), None) + if type(ra) == str: + # maybe it's just a float + try: + return float(ra) + except: + try: + if not ra.endswith('hours') and not ra.endswith('degrees'): + rastr = f'{ra} hours' + else: + rastr = ra + return Angle(rastr).degree + except: + self._logger.warning(f"Unable to parse RA from {ra}") + return None + return ra + + def _dec(self): + """ + Parse DEC from header. + + Utility method to pull the declination from the header, parsing text if appropriate. + + Returns + ------- + float : declination in degrees, or None + """ + dec = self.phu.get(self._keyword_for('dec'), None) + if type(dec) == str: + # maybe it's just a float + try: + return float(dec) + except: + try: + if not dec.endswith('degrees'): + decstr = f'{dec} degrees' + else: + decstr = dec + return Angle(decstr).degree + except: + self._logger.warning(f"Unable to parse dec from {dec}") + return None + return dec + # Utilities def _parse_section(self, keyword, pretty): try: @@ -41,3 +259,417 @@ def _parse_section(self, keyword, pretty): return [process_fn(raw) for raw in sections] except (KeyError, TypeError): return None + + @astro_data_descriptor + def airmass(self): + """ + Returns the airmass of the observation. + + Returns + ------- + float + Airmass value. + """ + return self.phu.get(self._keyword_for('airmass')) #hdr devuelve una lista, [] y phu el valor? + + @astro_data_descriptor + def azimuth(self): + """ + Returns the azimuth of the telescope, in degrees + + Returns + ------- + float + azimuth + + """ + return self.phu.get(self._keyword_for('azimuth')) + + @astro_data_descriptor + def binning(self): + """ + Returns the binning of the observation. + + Returns + ------- + float + Binning value. + """ + return self.hdr.get(self._keyword_for('binning')) + + @astro_data_descriptor + def camera(self, stripID=False, pretty=False): #Esto esta mal + """ + Returns the name of the camera. 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 camera. + pretty : bool + Same as for stripID. Pretty here does not do anything more. + + Returns + ------- + str + The name of the camera with or without the component ID. + + """ + return self._may_remove_component(self._keyword_for('camera'), + stripID, pretty) + + @astro_data_descriptor + def camera_focus(self, stripID=False, pretty=False): + """ + Returns the value of the camera focus. + + Returns + ------- + float + camera focus + + """ + return self.phu.get(self._keyword_for('camera_focus')) + + @astro_data_descriptor + def dec(self): + """ + Returns the Declination of the center of the field, in degrees. + + Returns + ------- + float + declination in degrees + """ + dec = self.wcs_dec() + if dec is None: + dec = self._dec() + return dec + + @astro_data_descriptor + def elevation(self): + """ + Returns the elevation of the telescope, in degrees + + Returns + ------- + float + elevation + """ + return self.phu.get(self._keyword_for('elevation')) + + @astro_data_descriptor + def environment_wind_at_start(self): + """ + Returns the environment wind at the start of the exposure, in km/h + + Returns + ------- + float + environmental wind + """ + return self.phu.get(self._keyword_for('environment_wind_at_start')) + + @astro_data_descriptor + def environment_wind_direction_at_start(self): + """ + Returns the environment wind direction at the start of the exposure, + + Returns + ------- + float + environmental wind direction + """ + return self.phu.get(self._keyword_for('environment_wind_direction_at_start')) + + @astro_data_descriptor + def environment_atm_pressure_at_start(self): + """ + Returns the environment atmospheric pressure at the start of the exposure, pending units + + Returns + ------- + float + environmental atmospheric pressure + """ + return self.phu.get(self._keyword_for('environment_atm_pressure_at_start')) + + @astro_data_descriptor + def environment_outside_temperature_in_C_at_start(self): + """ + Returns the temperature outside the observaratory at the start of the exposure, in C + + Returns + ------- + float + outside temperature + """ + return self.phu.get(self._keyword_for('environment_outside_temperature_in_C_at_start')) + + @astro_data_descriptor + def environment_relative_humidity_at_start(self): + """ + Returns the relative humidity outside the observaratory at the start of the exposure, pending units + + Returns + ------- + float + outside humidity + """ + return self.phu.get(self._keyword_for('environment_relative_humidity_at_start')) + + @astro_data_descriptor + def epoch(self): + """ + Returns the current epoch for celestial coordinates + + Returns + ------- + float + epoch + """ + return self.phu.get(self._keyword_for('epoch')) + + @astro_data_descriptor + def focus(self): + """ + Returns the focus of the telescope + + Returns + ------- + float + focus used for the observation + """ + return self.phu.get(self._keyword_for('focus')) + + @astro_data_descriptor + def gain(self): + """ + Returns the gain (electrons/ADU) for each extension + + Returns + ------- + list of floats/float + Gains used for the observation + """ + return self.phu.get(self._keyword_for('gain')) + + @astro_data_descriptor + def hour_angle(self): + """ + Returns the hour angle of the target + + Returns + ------- + + hour angle + """ + return self.phu.get(self._keyword_for('hour_angle')) + + @astro_data_descriptor + def instrument(self): + """ + Returns the current instrument used in SOAR + + Returns + ------- + + str + """ + return self.phu.get(self._keyword_for('instrument')) + + @astro_data_descriptor + def local_sidereal_time(self): + """ + Returns the local time stored at the time of the observation. + + Returns + ------- + datetime.datetime.time() + Local time of the observation. + """ + try: + local_time = self.phu[self._keyword_for('local_sidereal_time')] + return dateutil.parser.parse(local_time).time() + except (ValueError, TypeError, KeyError): + return None + + @astro_data_descriptor + def observation_type(self): + """ + Returns the type of an observation, e.g., 'OBJECT', 'FLAT', 'ARC'. + + Returns + ------- + str + the observation type + """ + return self.phu.get('OBSTYPE') + + @astro_data_descriptor + def ra(self): + """ + Returns the Right Ascension of the center of the field, in degrees. + + Returns + ------- + float + right ascension in degrees + """ + ra = self.wcs_ra() + if ra is None: + ra = self._ra() + return ra + + @astro_data_descriptor + def read_noise(self): + """ + Returns the read noise in electrons for each extension. A list is + returned unless called on a single-extension slice, when a float + + Returns + ------- + float/list of floats + the read noise + """ + return self.phu.get(self._keyword_for('read_noise')) + + @astro_data_descriptor + def rotator(self): + """ + Returns the rotator. Pending docstring + + Returns + ------- + float/list of floats + the read noise + """ + return self.phu.get(self._keyword_for('rotator')) + + @astro_data_descriptor + def seeing(self): + """ + Returns the value of the seeing, in arcsec + + Returns + ------- + float + seeing + """ + return self.phu.get(self._keyword_for('seeing')) + + @astro_data_descriptor + def slit(self): + """ + Returns the name of the entrance slit used for the observation + + Returns + ------- + str + the slit name + """ + return self.phu.get(self._keyword_for('slit')) + + @astro_data_descriptor + def trim_section(self): + """ + Returns the list of values of the image trim section + + Returns + ------- + list, int + trim section + """ + return self.phu.get(self._keyword_for('trim_section')) + + @astro_data_descriptor + def ut_time(self): + """ + Returns the date of observation in UT + + Returns. Pending docstring + ------- + list, float + trim section + """ + return self.phu.get(self._keyword_for('ut_time')) + + @astro_data_descriptor + def wavelength_config(self): + """ + Returns the mode of observation in SOAR. For goodman spectropraph it + will return the current configurations modes for the grating. Typical + modes includes 400_M1, 400_M2. + + Returns string? + ------- + str + wavelength mode + """ + return self.phu.get(self._keyword_for('wavelength')) + + def _get_wcs_coords(self): + """ + Returns the RA and dec of the location around which the celestial + sphere is being projected (CRVALi in a FITS representation) + + Returns + ------- + dict + {'lon': right ascension, 'lat': declination} plus other coords + """ + wcs = self.wcs if self.is_single else self[0].wcs + if wcs is None: + return None + + ra = dec = None + coords = {name: None for name in wcs.output_frame.axes_names} + for m in wcs.forward_transform: + try: + ra = m.lon.value + dec = m.lat.value + except AttributeError: + pass + + if 'NON_SIDEREAL' in self.tags and ra is not None and dec is not None: + ra, dec = gmu.toicrs('APPT', ra, dec, ut_datetime=self.ut_datetime()) + + coords["lon"] = ra + coords["lat"] = dec + return coords + + @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 header keyword. + + Returns + ------- + float + right ascension in degrees + """ + # Return None if the WCS isn't sky coordinates + try: + return self._get_wcs_coords()['lon'] + except (KeyError, TypeError): + return None + + @astro_data_descriptor + def wcs_dec(self): + """ + Returns the Declination of the center of the field based on the + WCS rather than the DEC header keyword. + + Returns + ------- + float + declination in degrees + """ + # Return None if the WCS isn't sky coordinates + try: + return self._get_wcs_coords()['lat'] + except (KeyError, TypeError): + return None \ No newline at end of file