Source code for prose.telescope

import inspect
from dataclasses import asdict, dataclass
from datetime import datetime

import astropy.units as u
import numpy as np
import yaml
from dateutil import parser as dparser

from prose import CONFIG
from prose.console_utils import info


def str_to_astropy_unit(unit_string):
    return u.__dict__[unit_string]


# TODO: add exposure time unit
[docs] @dataclass class Telescope: """Save and store FITS header keywords definition for a given telescope This is a Python Data Class, so that all attributes described below can be used as keyword-arguments when instantiating a Telescope """ name: str = "Unknown" """Name taken by the telescope if saved""" names: tuple = () """Alternative names that the telescope may take in the fits header values of `keyword_telescope`""" # Keywords # -------- keyword_telescope: str = "TELESCOP" """FITS header keyword for telescope name, default is :code:`"TELESCOP"`""" keyword_object: str = "OBJECT" """FITS header keyword for observed object name, default is :code:`"OBJECT"`""" keyword_image_type: str = "IMAGETYP" """ FITS header keyword for image type (e.g. dark, bias, science), default is :code:`"IMAGETYP"`""" keyword_light_images: str = "light" """value of `keyword_image_type` associated to science (aka light) images, default is :code:`"light"`""" keyword_dark_images: str = "dark" """value of `keyword_image_type` associated to dark calibration images, Default is :code:`"dark"`""" keyword_flat_images: str = "flat" """value of `keyword_image_type` associated to flat calibration images, default is :code:`"flat"`""" keyword_bias_images: str = "bias" """value of `keyword_image_type` associated to flat calibration images, default is :code:`"bias"`""" keyword_observation_date: str = "DATE-OBS" """FITS header keyword for observation date, default is "DATE:code:`-OBS"`""" keyword_exposure_time: str = "EXPTIME" """ FITS header keyword for exposure time, default is :code:`"EXPTIME"`""" keyword_filter: str = "FILTER" """FITS header keyword for filter, default is :code:`"FILTER"`""" keyword_airmass: str = "AIRMASS" """FITS header keyword for airmass, default is :code:`"AIRMASS"`""" keyword_fwhm: str = "FWHM" """FITS header keyword for image full-width-half-maximum (fwhm), default is :code:`"FWHM"`""" keyword_seeing: str = "SEEING" """FITS header keyword for image seeing, default is :code:`"SEEING"`""" keyword_ra: str = "RA" """FITS header keyword for right ascension, default is :code:`"RA"`""" keyword_dec: str = "DEC" """FITS header keyword for declination, default is :code:`"DEC"`""" keyword_jd: str = "JD" """ FITS header keyword for julian day, default is :code:`"JD"`""" keyword_bjd: str = "BJD" """FITS header keyword for barycentric julian day, default is :code:`"BJD"`""" keyword_flip: str = "PIERSIDE" """FITS header keyword for meridian flip configuration, default is :code:`"PIERSIDE"`""" # Units, formats and scales # ------------------------- ra_unit: str = "deg" """unit of the value of `keyword_ra`, default is :code:`"deg"`""" dec_unit: str = "deg" """unit of the value of `keyword_dec`, default is :code:`"deg"`""" jd_scale: str = "utc" """unit of the value of `JD`, default is :code:`"utc"`""" bjd_scale: str = "utc" """ unit of the value of `BJD`, default is :code:`"utc"`""" mjd: float = 0.0 """value to subtract from the value of `keyword_jd`""" # Specs # ----- trimming: tuple = (0, 0) """horizontal and vertical overscan of an image in pixels, default is :code:`(0, 0)`""" read_noise: float = 9 """detector read noise in ADU, default is :code:`9`""" gain: float = 1 """detector gain in electrons/ADU, default is :code:`1`""" altitude: float = 2000 """altitude of the telescope in meters, default is :code:`2000`,""" diameter: float = 100 """diameter of the telescope in centimeters, default is :code:`100`""" pixel_scale: float = None """pixel scale (or plate scale) of the detector in arcsec/pixel, default is :code:`None`""" latlong: tuple = (None, None) """latitude and longitude of the telescope, default is :code:`(None, None)`""" saturation: float = 55000 """detector's pixels full depth (saturation) in ADU, default is :code:`55000`""" hdu: int = 0 """index of the FITS HDU where to find image data, default is :code:`0`""" camera_name: str = None """name of the telescope camera, default is :code:`None`""" date_string_format: str = None """date string format, default is :code:`None`""" _default: bool = True save: bool = False def __post_init__(self): if self.save: telescope_dict = asdict(self) del telescope_dict["_default"] del telescope_dict["save"] CONFIG.save_telescope_file(telescope_dict)
[docs] @classmethod def load(cls, filename): return cls.from_dict(**yaml.full_load(open(filename, "r")))
[docs] @classmethod def from_dict(cls, env): """Load from a dict ensuring that only class attributes are used""" return cls( **{k: v for k, v in env.items() if k in inspect.signature(cls).parameters} )
@property def earth_location(self): from astropy.coordinates import EarthLocation if self.latlong[0] is None or self.latlong[1] is None: return None else: return EarthLocation(self.latlong[1], self.latlong[0], self.altitude) # TODO keep?
[docs] def error(self, signal, area, sky, exposure, airmass=None, scinfac=0.09): _signal = signal.copy() _squarred_error = _signal + area * ( self.read_noise**2 + (self.gain / 2) ** 2 + sky ) if airmass is not None: scintillation = ( scinfac * np.power(self.diameter, -0.6666) * np.power(airmass, 1.75) * np.exp(-self.altitude / 8000.0) ) / np.sqrt(2 * exposure) _squarred_error += np.power(signal * scintillation, 2) return np.sqrt(_squarred_error)
[docs] @classmethod def from_name(cls, name, verbose=True, strict=False): telescope_dict = CONFIG.match_telescope_name(name) if telescope_dict is not None: telescope = cls.from_dict(telescope_dict) else: if strict: return None telescope = cls() telescope.name = name if verbose: info(f"telescope {name} not found - using default") return telescope
[docs] @staticmethod def from_names(instrument_name, telescope_name, verbose=True, strict=True): # we first check by instrument name telescope = Telescope.from_name(instrument_name, verbose=False, strict=True) # if not found we check telescope name if telescope is None: telescope = Telescope.from_name(telescope_name, verbose=verbose) if telescope is None: if not strict: telescope = Telescope() telescope.name = f"default_{telescope_name}" return telescope
[docs] def date(self, header): header_date_str = header.get(self.keyword_observation_date, None) if header_date_str is not None: if self.date_string_format is not None: return datetime.strptime(header_date_str, self.date_string_format) else: return dparser.parse(header_date_str) else: return datetime(1800, 1, 2)
[docs] def image_type(self, header): return header.get(self.keyword_image_type, "").lower()