Source code for msumastro.header_processing.feder

from itertools import chain
import logging
import datetime

from astropy.coordinates import EarthLocation
import astropy.units as u
import numpy as np
from ccdproc.utils.slices import slice_from_string

from .fitskeyword import FITSKeyword

logger = logging.getLogger(__name__)

__all__ = ['FederSite', 'ImageSoftware', 'Instrument', 'ApogeeAltaU9',
           'ApogeeAspenCG16', 'MaximDL4', 'MaximDL5']


[docs]class FederSite(EarthLocation): """ The Feder Observatory site. An astropy location with the observatory location pre-set to: + `lat` = 46.86678 degrees North + `long` = -96.453278 degrees East + `height` = 311.8 meters and a few additional properties/methods that are convenient: + `name` = Feder Observatory """ def __new__(cls): return EarthLocation.__new__(FederSite, lat=46.86678*u.degree, lon=-96.453278*u.degree, height=311.8*u.m) def __init__(self): self._name = 'Feder Observatory' @property def name(self): return self._name
[docs]class Instrument(object): """ Telescope instrument with simple properties. Parameters ---------- name : str Name of the instrument. fits_names : list of str List of names by which the instrument is known in FITS headers rows : int Number of rows in an image produced by this instrument, including overscan. columns : int Number of columns in an image produced by this instrument, including overscan. image_unit : astropy.units.Unit Unit of the image; default value is ``None`` trim_region : string Region of the CCD that should be preserved after overscan subtraction. Should use *FITS* conventions for specifying slices (i.e. slice starts at 1, includes endpoint, and uses FITS NAXIS1, NAXIS2 for order of indices). useful_overscan_region : string Complete specification of the region of the CCD actually useful for overscan calibration. This may (or may not) be smaller than the entire portion of the chip the manufacturer labels as overscan. Should use *FITS* conventions for specifying slices (i.e. slice starts at 1, includes endpoint, and uses FITS NAXIS1, NAXIS2 for order of indices). Examples -------- Consider an image whose dimensions as given in its FITS header are ``NAXIS1 = 3085`` and ``NAXIS2 = 2048`` with an overscan region that begins at position 3073 along axis 1. The useful part of that overscan is from FITS column 3076 up to and including, 3079, and the full range of rows (``NAXIS2``). The correct overscan settings for this instrument are:: # Note not all of the overscan region is actually useful. useful_overscan_region = '[3076:3079, :]' # But the whole overscan region should be trimmed away in reduction. trim_region = '[1:3073, :]' """ def __init__(self, name, fits_names=None, rows=0, columns=0, image_unit=None, trim_region=None, useful_overscan_region=None): self.name = name self.fits_names = fits_names self.rows = rows self.columns = columns self.image_unit = image_unit self.trim_region = trim_region self.useful_overscan = useful_overscan_region
[docs] def has_overscan(self, image_dimensions): """ Determine whether an image taken by this instrument has overscan Parameters ---------- image_dimensions : list-like with two elements Shape of the image; can be any type as long as it has two elements. The order should be the FITS order, ``NAXIS1`` then ``NAXIS2``. Returns ------- bool Indicates whether or not image has overscan present. """ if self.trim_region is None: return False # Grab the trim region as a slice to make it easier to access end # points. Do *not* convert from FITS convention because input # dimensions follow FITS conventions. trim_dim = slice_from_string(self.trim_region) dim_end = lambda ax: (trim_dim[ax].stop + 1 if trim_dim[ax].stop is not None else image_dimensions[ax]) # print(trim_dim1, trim_dim2) # if trim_dim1.stop is not None: # dim1_end = trim_dim1.stop + 1 # else: # # None means use the whole thing.... # dim1_end = image_dimensions[0] # if trim_dim2.stop is not None: # dim2_end = trim_dim2.stop + 1 # else: # # None means use the whole thing.... # dim2_end = image_dimensions[1] if (dim_end(0) < image_dimensions[0] or dim_end(1) < image_dimensions[1]): return True else: return False
[docs]class ApogeeAltaU9(Instrument): """ The Apogee Alta U9 """ def __init__(self): Instrument.__init__(self, "Apogee Alta U9", fits_names=["Apogee Alta", "Apogee USB/Net"], rows=2048, columns=3085, useful_overscan_region='[3076:3079, :]', trim_region='[1:3073, :]', image_unit=u.adu)
class SBIGSpectrometer(Instrument): """ SBIG ST-7 spectromter. """ def __init__(self): super(SBIGSpectrometer, self).__init__("SBIG ST-7 Spectrometer", fits_names=["SBIG ST-7"]) class CelestronNightscape10100(Instrument): """ The Celestron Nightscape 10100, an RGB color CCD. """ def __init__(self): Instrument.__init__(self, "Celestron Nightscape 10100", fits_names=["Celestron Nightscape 10100"], image_unit=u.adu)
[docs]class ApogeeAspenCG16(Instrument): """ Apogee Aspen CG16 (manufactured by Andor).by """ def __init__(self): super(ApogeeAspenCG16, self).__init__( 'Apogee Aspen CG16', fits_names=["Apogee Aspen CG16M", "AspenCG16"], rows=4096, columns=4109, useful_overscan_region='[4096:4109]', trim_region='[1:4096, :]', image_unit=u.adu )
[docs]class ImageSoftware(object): """ Represents software that takes images at telescope. Parameters ---------- name : str Name of the software. Can be the same is the name in the FITS file, or not. fits_keyword : str Name of the FITS keyword that contains the name of the software. fits_name : list of str Name of the software as written in the FITS file major_version : int Major version number of the software. minor_version : int Minor version number of the software. bad_keywords : list of strings Names of any keywords that should be removed from the FITS before further processing. purged_flag_keyword : str, optional Name of the keyword which indicates whether bad keywords have already been purged. Default value is 'PURGED' """ def __init__(self, name, fits_name=None, major_version=None, minor_version=None, bad_keywords=None, fits_keyword=None, purged_flag_keyword=None): self.name = name self.fits_name = fits_name self.major_version = major_version self.minor_version = minor_version self.bad_keywords = bad_keywords self.fits_keyword = fits_keyword self.purged_flag_keyword = purged_flag_keyword or "PURGED"
[docs] def created_this(self, version_string): """ Indicate whether version string matches this software Parameters ---------- version_string : str String from FITS header that indicates software version. Returns ------- bool ``True`` if the version string matches the software instance. """ return version_string in self.fits_name
[docs]class MaximDL4(ImageSoftware): """ Represents MaximDL version 4, all sub-versions """ def __init__(self): fits_name = ['MaxIm DL Version 4.10'] super(MaximDL4, self).__init__("MaxImDL", fits_name=fits_name, major_version=4, minor_version=10, bad_keywords=['OBSERVER'], fits_keyword='SWCREATE')
[docs]class MaximDL5(ImageSoftware): """ Represents MaximDL version 5, all sub-versions. Subversions are included by listing the FITS names of all versions that have been used at Feder Observatory. """ def __init__(self): bad_keys = ['OBJECT', 'JD', 'JD-HELIO', 'OBJCTALT', 'OBJCTAZ', 'OBJCTHA', 'AIRMASS', 'OBSERVER'] fits_name = ['MaxIm DL Version 5.21 130912 01A17', 'MaxIm DL Version 5.21 120829 2R1M0', 'MaxIm DL Version 5.23 130912 01A17', 'MaxIm DL Version 5.15'] super(MaximDL5, self).__init__("MaxImDL", fits_name=fits_name, major_version=5, minor_version=21, bad_keywords=bad_keys, fits_keyword='SWCREATE' )
class MaximDL6(ImageSoftware): """ Represents MaximDL version 6, all sub-versions. Subversions are included by listing the FITS names of all versions that have been used at Feder Observatory. """ def __init__(self): bad_keys = ['OBJECT', 'JD', 'JD-HELIO', 'OBJCTALT', 'OBJCTAZ', 'OBJCTHA', 'AIRMASS', 'OBSERVER'] fits_name = ['MaxIm DL Version 6.16 190601 00KPP', 'MaxIm DL Version 6.17 190601 00KPP', 'MaxIm DL Version 6.18 190601 00KPP'] super(MaximDL6, self).__init__("MaxImDL", fits_name=fits_name, major_version=6, minor_version=17, bad_keywords=bad_keys, fits_keyword='SWCREATE' ) class SBIGCCDOps(ImageSoftware): """ Represents software used to create images from the SBIG spectrometer. """ def __init__(self): bad_keys = [] fits_name = ['SBIG Win CCDOPS Version 5.47 Build 6-NT'] super(SBIGCCDOps, self).__init__("SBIG CCDOps", fits_name=fits_name, major_version=5, minor_version=47, bad_keywords=[], fits_keyword='SWCREATE') class CelestronAstroFX(ImageSoftware): """ Represents software used to create images from the SBIG spectrometer. """ def __init__(self): bad_keys = [] fits_name = ['Celestron AstroFX V1.06'] super(CelestronAstroFX, self).__init__("Celestron AstroFX", fits_name=fits_name, major_version=1, minor_version="06", bad_keywords=bad_keys, fits_keyword='SWCREATE') class Feder(object): """ Class encapsulating site, instrument, and software information for Feder Observatory. Attributes ---------- site : feder.FederSite instance instrument : dict Instruments available; key is name, value is an :py:class:`Instrument` software : dict Software available; key is name, value is an :class:`ImageSoftware` software_FITS_keywords : list of str FITS names of all software available. keywords_for_all_files keywords_for_light_files keywords_for_overscan """ def __init__(self): self.site = FederSite() self._apogee_alta_u9 = ApogeeAltaU9() self._sbig_spectrometer = SBIGSpectrometer() self._celestron_10100 = CelestronNightscape10100() self._apogee_aspen = ApogeeAspenCG16() self._instrument_objects = [self._apogee_alta_u9, self._sbig_spectrometer, self._celestron_10100, self._apogee_aspen] self.instruments = {} for instrument in self._instrument_objects: for name in instrument.fits_names: self.instruments[name] = instrument self._maximdl4 = MaximDL4() self._maximdl5 = MaximDL5() self._maximdl6 = MaximDL6() self._sbig_ccdops = SBIGCCDOps() self._astrofx = CelestronAstroFX() self._software_objects = [ self._maximdl4, self._maximdl5, self._maximdl6, self._sbig_ccdops, self._astrofx ] self.software = {} self.software_FITS_keywords = [] for software in self._software_objects: for name in software.fits_name: self.software[name] = software self.software_FITS_keywords.append(software.fits_keyword) self.software_FITS_keywords = list(set(self.software_FITS_keywords)) self._keywords_for_all_files = [] self._set_site_keywords_values() self._time_keywords_to_set() self._keywords_for_light_files = [] self._define_keywords_for_light_files() self._overscan_keywords = [] self._define_overscan_keywords() for key in chain(self.keywords_for_all_files, self.keywords_for_light_files, self.keywords_for_overscan): name = key.name name = name.replace('-', '_') setattr(self, name, key) @property def keywords_for_all_files(self): """ List of :class:`~msumastro.header_processing.fitskeyword.FITSKeyword` s whose values need to be set for all image types. """ return self._keywords_for_all_files @property def keywords_for_light_files(self): """ List of :class:`~msumastro.header_processing.fitskeyword.FITSKeyword` s whose values need to be set only for light image types. """ return self._keywords_for_light_files @property def keywords_for_overscan(self): """ List of :class:`~msumastro.header_processing.fitskeyword.FITSKeyword` s related to overscan. """ return self._overscan_keywords def _define_keywords_for_light_files(self): RA = FITSKeyword(name='ra', comment='Approximate RA at EQUINOX', synonyms=['objctra']) Dec = FITSKeyword(name='DEC', comment='Approximate DEC at EQUINOX', synonyms=['objctdec']) target_object = FITSKeyword(name='object', comment='Target of the observations') hour_angle = FITSKeyword(name='ha', comment='Hour angle') airmass = FITSKeyword(name='airmass', comment='Airmass (Sec(Z)) at start of observation', synonyms=['secz']) altitude = FITSKeyword(name='alt-obj', comment='[degrees] Altitude of object, no refraction') azimuth = FITSKeyword(name='az-obj', comment='[degrees] Azimuth of object, no refraction') self._keywords_for_light_files.append(RA) self._keywords_for_light_files.append(Dec) self._keywords_for_light_files.append(target_object) self._keywords_for_light_files.append(hour_angle) self._keywords_for_light_files.append(airmass) self._keywords_for_light_files.append(altitude) self._keywords_for_light_files.append(azimuth) def _set_site_keywords_values(self): latitude = FITSKeyword(name="latitude", comment='[degrees] Observatory latitude', synonyms=['sitelat']) longitude = FITSKeyword(name='longitud', comment='[degrees east] Observatory longitude', synonyms='sitelong') obs_altitude = FITSKeyword(name='altitude', comment='[meters] Observatory altitude') lat_lon_format = {'sep': ':', 'pad': True, 'alwayssign': True} latitude.value = self.site.lat.to_string(**lat_lon_format) longitude.value = self.site.lon.to_string(**lat_lon_format) obs_altitude.value = self.site.height.value self._keywords_for_all_files.extend([latitude, longitude, obs_altitude]) def _time_keywords_to_set(self): LST = FITSKeyword(name='LST', comment='Local Sidereal Time at start of observation') JD = FITSKeyword(name='jd-obs', comment='Julian Date at start of observation') MJD = FITSKeyword(name='mjd-obs', comment='Modified Julian date at start of observation') self._keywords_for_all_files.extend([LST, JD, MJD]) def _define_overscan_keywords(self): overscan_region = FITSKeyword(name='biassec', comment='Useful region of the overscan') trim_region = FITSKeyword(name='trimsec', comment=('Region to keep after trimming overscan')) self._overscan_keywords.extend([overscan_region, trim_region])