def __init__(self): # Get it started super(KeckNIRESSpectrograph, self).__init__() self.spectrograph = 'keck_nires' self.telescope = telescopes.KeckTelescopePar() self.camera = 'NIRES' self.numhead = 3
def __init__(self): # Get it started super(KeckNIRESSpectrograph, self).__init__() self.spectrograph = 'keck_nires' self.telescope = telescopes.KeckTelescopePar() self.camera = 'NIRES' self.numhead = 3 self.detector = [ # Detector 1 pypeitpar.DetectorPar( specaxis=1, specflip=True, xgap=0., ygap=0., ysize=1., platescale=0.15, darkcurr=0.01, saturation= 1e6, # I'm not sure we actually saturate with the DITs??? nonlinear=0.76, numamplifiers=1, gain=3.8, ronoise=5.0, datasec='[:,:]', oscansec='[980:1024,:]' # Is this a hack?? ) ]
def __init__(self): # Get it started super(KeckMOSFIRESpectrograph, self).__init__() self.telescope = telescopes.KeckTelescopePar() self.spectrograph = 'keck_mosfire' self.camera = 'MOSFIRE' self.detector = [ # Detector 1 pypeitpar.DetectorPar( dataext=0, specaxis=1, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.193, darkcurr=0.8, saturation=1e9, # ADU, this is hacked for now nonlinear= 1.00, # docs say linear to 90,000 but our flats are usually higher numamplifiers=1, gain=2.15, # Taken from MOSFIRE detector webpage ronoise= 5.8, # This is for 16 non-destructuve reads, the default readout mode datasec='[:,:]', oscansec='[:,:]') ] self.numhead = 1
def __init__(self): # Get it started super(KeckNIRSPECSpectrograph, self).__init__() self.telescope = telescopes.KeckTelescopePar() self.camera = 'NIRSPEC' self.detector = [ # Detector 1 pypeitpar.DetectorPar( dataext = 0, specaxis = 0, specflip = False, xgap = 0., ygap = 0., ysize = 1., platescale = 0.193, darkcurr = 0.8, saturation = 100000., nonlinear = 1.00, # docs say linear to 90,000 but our flats are usually higher numamplifiers = 1, gain = 5.8, ronoise = 23, datasec = '[:,:]', oscansec = '[:,:]' )] self.numhead = 1
def __init__(self): # Get it started super(KeckKCWISpectrograph, self).__init__() self.spectrograph = 'keck_kcwi' self.telescope = telescopes.KeckTelescopePar() self.camera = 'KCWI' # self.detector = [pypeitpar.DetectorPar( # dataext = 0, # specaxis = 0, # specflip = False, # xgap = 0., # ygap = 0., # ysize = 1., # platescale = 0.147, # arcsec/pixel # darkcurr = None, # <-- TODO : Need to set this # saturation = 65535., # nonlinear = 0.95, # For lack of a better number! # numamplifiers = 4, # <-- This is provided in the header # gain = [0]*4, # <-- This is provided in the header # ronoise = [0]*4, # <-- TODO : Need to set this for other setups # datasec = ['']*4, # <-- This is provided in the header # oscansec = ['']*4, # <-- This is provided in the header # suffix = '_01' # )] self.numhead = 1 # Uses default timeunit # Uses default primary_hdrext # self.sky_file ? # Don't instantiate these until they're needed self.grating = None self.optical_model = None self.detector_map = None
def __init__(self): # Get it started super(KeckDEIMOSSpectrograph, self).__init__() self.spectrograph = 'keck_deimos' self.telescope = telescopes.KeckTelescopePar() self.camera = 'DEIMOS' # Don't instantiate these until they're needed self.grating = None self.optical_model = None self.detector_map = None
def __init__(self): # Get it started super(KeckKCWISpectrograph, self).__init__() self.spectrograph = 'keck_kcwi' self.telescope = telescopes.KeckTelescopePar() self.camera = 'KCWI' # Uses default timeunit # Uses default primary_hdrext # self.sky_file ? # Don't instantiate these until they're needed self.grating = None self.optical_model = None self.detector_map = None
def __init__(self): # Get it started # TODO :: Might need to change the tolerance of disperser angle in pypeit setup (two BH2 nights where sufficiently different that this was important). super(KeckKCWISpectrograph, self).__init__() self.spectrograph = 'keck_kcwi' self.telescope = telescopes.KeckTelescopePar() self.camera = 'KCWI' self.location = EarthLocation.of_site( 'Keck Observatory' ) # TODO :: Might consider changing TelescopePar to use the astropy EarthLocation # Uses default timeunit # Uses default primary_hdrext # self.sky_file ? # Don't instantiate these until they're needed self.grating = None self.optical_model = None self.detector_map = None
def __init__(self): # Get it started super(KeckLRISSpectrograph, self).__init__() self.spectrograph = 'keck_lris_base' self.telescope = telescopes.KeckTelescopePar()
def __init__(self): # Get it started super(KECKHIRESSpectrograph, self).__init__() self.spectrograph = 'keck_hires_base' self.telescope = telescopes.KeckTelescopePar()
def __init__(self): # Get it started super(MMTBINOSPECSpectrograph, self).__init__() self.spectrograph = 'mmt_binospec' self.telescope = telescopes.KeckTelescopePar() self.camera = 'BINOSPEC' self.detector = [ # Detector 1 pypeitpar.DetectorPar( dataext=1, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=4.19, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.226, ronoise=2.570, datasec='', # These are provided by read_deimos oscansec='', suffix='_01'), # Detector 2 pypeitpar.DetectorPar( dataext=2, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=3.46, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.188, ronoise=2.491, datasec='', # These are provided by read_deimos oscansec='', suffix='_02'), # Detector 3 pypeitpar.DetectorPar( dataext=3, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=4.03, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.248, ronoise=2.618, datasec='', # These are provided by read_deimos oscansec='', suffix='_03'), # Detector 4 pypeitpar.DetectorPar( dataext=4, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=3.80, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.220, ronoise=2.557, datasec='', # These are provided by read_deimos oscansec='', suffix='_04'), # Detector 5 pypeitpar.DetectorPar( dataext=5, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=4.71, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.184, ronoise=2.482, datasec='', # These are provided by read_deimos oscansec='', suffix='_05'), # Detector 6 pypeitpar.DetectorPar( dataext=6, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=4.28, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.177, ronoise=2.469, datasec='', # These are provided by read_deimos oscansec='', suffix='_06'), # Detector 7 pypeitpar.DetectorPar( dataext=7, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=3.33, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.201, ronoise=2.518, datasec='', # These are provided by read_deimos oscansec='', suffix='_07'), # Detector 8 pypeitpar.DetectorPar( dataext=8, specaxis=0, specflip=False, xgap=0., ygap=0., ysize=1., platescale=0.1185, darkcurr=3.69, saturation=65535., nonlinear=0.86, numamplifiers=1, gain=1.230, ronoise=2.580, datasec='', # These are provided by read_deimos oscansec='', suffix='_08') ] self.numhead = 9 # Uses default timeunit # Uses default primary_hdrext # self.sky_file ? # Don't instantiate these until they're needed self.grating = None self.optical_model = None self.detector_map = None
class KECKHIRESSpectrograph(spectrograph.Spectrograph): """ Child to handle KECK/HIRES specific code. This spectrograph is not yet supported. """ ndet = 1 telescope = telescopes.KeckTelescopePar() pypeline = 'Echelle' @classmethod def default_pypeit_par(cls): """ Return the default parameters to use for this instrument. Returns: :class:`~pypeit.par.pypeitpar.PypeItPar`: Parameters required by all of ``PypeIt`` methods. """ par = super().default_pypeit_par() # Correct for flexure using the default approach par['flexure'] = pypeitpar.FlexurePar() return par def init_meta(self): """ Define how metadata are derived from the spectrograph files. That is, this associates the ``PypeIt``-specific metadata keywords with the instrument-specific header cards using :attr:`meta`. """ self.meta = {} # Required (core) self.meta['ra'] = dict(ext=0, card='RA') self.meta['dec'] = dict(ext=0, card='DEC') self.meta['target'] = dict(ext=0, card='OBJECT') self.meta['decker'] = dict(ext=0, card='DECKNAME') self.meta['binning'] = dict(ext=0, card='BINNING') self.meta['mjd'] = dict(ext=0, card='MJD') self.meta['exptime'] = dict(ext=0, card='EXPTIME') self.meta['airmass'] = dict(ext=0, card='AIRMASS') self.meta['dispname'] = dict(ext=0, card='ECHNAME') # Extras for config and frametyping # self.meta['echangl'] = dict(ext=0, card='ECHANGL') # self.meta['xdangl'] = dict(ext=0, card='XDANGL') def check_frame_type(self, ftype, fitstbl, exprng=None): """ Check for frames of the provided type. Args: ftype (:obj:`str`): Type of frame to check. Must be a valid frame type; see frame-type :ref:`frame_type_defs`. fitstbl (`astropy.table.Table`_): The table with the metadata for one or more frames to check. exprng (:obj:`list`, optional): Range in the allowed exposure time for a frame of type ``ftype``. See :func:`pypeit.core.framematch.check_frame_exptime`. Returns: `numpy.ndarray`_: Boolean array with the flags selecting the exposures in ``fitstbl`` that are ``ftype`` type frames. """ good_exp = framematch.check_frame_exptime(fitstbl['exptime'], exprng) # TODO: Allow for 'sky' frame type, for now include sky in # 'science' category if ftype == 'science': return good_exp & (fitstbl['idname'] == 'Object') if ftype == 'standard': return good_exp & ((fitstbl['idname'] == 'Std') | (fitstbl['idname'] == 'Object')) if ftype == 'bias': return good_exp & (fitstbl['idname'] == 'Bias') if ftype == 'dark': return good_exp & (fitstbl['idname'] == 'Dark') if ftype in ['pixelflat', 'trace']: # Flats and trace frames are typed together return good_exp & ((fitstbl['idname'] == 'Flat') | (fitstbl['idname'] == 'IntFlat')) if ftype in ['arc', 'tilt']: return good_exp & (fitstbl['idname'] == 'Line') msgs.warn('Cannot determine if frames are of type {0}.'.format(ftype)) return np.zeros(len(fitstbl), dtype=bool)
def __init__(self): # Get it started super(KeckNIRSPECSpectrograph, self).__init__() self.telescope = telescopes.KeckTelescopePar() self.camera = 'NIRSPEC'
class KeckMOSFIRESpectrograph(spectrograph.Spectrograph): """ Child to handle Keck/MOSFIRE specific code """ ndet = 1 name = 'keck_mosfire' telescope = telescopes.KeckTelescopePar() camera = 'MOSFIRE' supported = True comment = 'Gratings tested: Y, J, K' def get_detector_par(self, hdu, det): """ Return metadata for the selected detector. Args: hdu (`astropy.io.fits.HDUList`_): The open fits file with the raw image of interest. det (:obj:`int`): 1-indexed detector number. Returns: :class:`~pypeit.images.detector_container.DetectorContainer`: Object with the detector metadata. """ # Detector 1 detector_dict = dict( binning = '1,1', det = 1, dataext = 0, specaxis = 1, specflip = False, spatflip = False, platescale = 0.1798, darkcurr = 0.8, saturation = 1e9, # ADU, this is hacked for now nonlinear = 1.00, # docs say linear to 90,000 but our flats are usually higher numamplifiers = 1, mincounts = -1e10, gain = np.atleast_1d(2.15), # Taken from MOSFIRE detector webpage ronoise = np.atleast_1d(5.8), # This is for 16 non-destructuve reads, the default readout mode datasec = np.atleast_1d('[:,:]'), oscansec = np.atleast_1d('[:,:]') ) return detector_container.DetectorContainer(**detector_dict) @classmethod def default_pypeit_par(cls): """ Return the default parameters to use for this instrument. Returns: :class:`~pypeit.par.pypeitpar.PypeItPar`: Parameters required by all of ``PypeIt`` methods. """ par = super().default_pypeit_par() # Wavelengths # 1D wavelength solution par['calibrations']['wavelengths']['rms_threshold'] = 0.30 #0.20 # Might be grating dependent.. par['calibrations']['wavelengths']['sigdetect']=5.0 par['calibrations']['wavelengths']['fwhm']= 5.0 par['calibrations']['wavelengths']['n_final']= 4 par['calibrations']['wavelengths']['lamps'] = ['OH_NIRES'] #par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] par['calibrations']['wavelengths']['method'] = 'holy-grail' # Reidentification parameters #par['calibrations']['wavelengths']['reid_arxiv'] = 'keck_nires.fits' par['calibrations']['slitedges']['edge_thresh'] = 50. par['calibrations']['slitedges']['sync_predict'] = 'nearest' # Flats # Do not illumination correct. We should also not be flat fielding given the bars. # TODO Implement imaging flats for MOSFIRE. Do test with/without illumination flats. # Turn of illumflat turn_off = dict(use_biasimage=False, use_overscan=False, use_darkimage=False) par.reset_all_processimages_par(**turn_off) # Extraction par['reduce']['skysub']['bspline_spacing'] = 0.8 par['reduce']['extraction']['sn_gauss'] = 4.0 # Flexure par['flexure']['spec_method'] = 'skip' par['scienceframe']['process']['sigclip'] = 20.0 par['scienceframe']['process']['satpix'] ='nothing' # Set the default exposure time ranges for the frame typing par['calibrations']['standardframe']['exprng'] = [None, 20] par['calibrations']['arcframe']['exprng'] = [20, None] par['calibrations']['darkframe']['exprng'] = [20, None] par['scienceframe']['exprng'] = [20, None] # Sensitivity function parameters par['sensfunc']['algorithm'] = 'IR' par['sensfunc']['polyorder'] = 8 par['sensfunc']['IR']['telgridfile'] = resource_filename('pypeit', '/data/telluric/TelFit_MaunaKea_3100_26100_R20000.fits') return par def init_meta(self): """ Define how metadata are derived from the spectrograph files. That is, this associates the ``PypeIt``-specific metadata keywords with the instrument-specific header cards using :attr:`meta`. """ self.meta = {} # Required (core) self.meta['ra'] = dict(ext=0, card='RA') self.meta['dec'] = dict(ext=0, card='DEC') self.meta['target'] = dict(ext=0, card='TARGNAME') self.meta['decker'] = dict(ext=0, card='MASKNAME') self.meta['binning'] = dict(ext=0, card=None, default='1,1') self.meta['mjd'] = dict(ext=0, card='MJD-OBS') self.meta['exptime'] = dict(ext=0, card='TRUITIME') self.meta['airmass'] = dict(ext=0, card='AIRMASS') # Extras for config and frametyping self.meta['dispname'] = dict(ext=0, card='OBSMODE') self.meta['idname'] = dict(card=None, compound=True) # Filter self.meta['filter1'] = dict(ext=0, card='FILTER') # Lamps lamp_names = ['FLATSPEC'] for kk,lamp_name in enumerate(lamp_names): self.meta['lampstat{:02d}'.format(kk+1)] = dict(ext=0, card=lamp_name) def compound_meta(self, headarr, meta_key): """ Methods to generate metadata requiring interpretation of the header data, instead of simply reading the value of a header card. Args: headarr (:obj:`list`): List of `astropy.io.fits.Header`_ objects. meta_key (:obj:`str`): Metadata keyword to construct. Returns: object: Metadata value read from the header(s). """ if meta_key == 'idname': if headarr[0].get('KOAIMTYP', None) is not None: return headarr[0].get('KOAIMTYP') else: try: # TODO: This should be changed to except on a specific error. FLATSPEC = int(headarr[0].get('FLATSPEC')) PWSTATA7 = int(headarr[0].get('PWSTATA7')) PWSTATA8 = int(headarr[0].get('PWSTATA8')) if FLATSPEC == 0 and PWSTATA7 == 0 and PWSTATA8 == 0: return 'object' elif FLATSPEC == 1: return 'flatlamp' elif PWSTATA7 == 1 or PWSTATA8 == 1: return 'arclamp' except: return 'unknown' else: msgs.error("Not ready for this compound meta") def configuration_keys(self): """ Return the metadata keys that define a unique instrument configuration. This list is used by :class:`~pypeit.metadata.PypeItMetaData` to identify the unique configurations among the list of frames read for a given reduction. Returns: :obj:`list`: List of keywords of data pulled from file headers and used to constuct the :class:`~pypeit.metadata.PypeItMetaData` object. """ return ['decker', 'dispname', 'filter1'] def pypeit_file_keys(self): """ Define the list of keys to be output into a standard ``PypeIt`` file. Returns: :obj:`list`: The list of keywords in the relevant :class:`~pypeit.metadata.PypeItMetaData` instance to print to the :ref:`pypeit_file`. """ pypeit_keys = super().pypeit_file_keys() # TODO: Why are these added here? See # pypeit.metadata.PypeItMetaData.set_pypeit_cols pypeit_keys += ['calib', 'comb_id', 'bkg_id'] return pypeit_keys def check_frame_type(self, ftype, fitstbl, exprng=None): """ Check for frames of the provided type. Args: ftype (:obj:`str`): Type of frame to check. Must be a valid frame type; see frame-type :ref:`frame_type_defs`. fitstbl (`astropy.table.Table`_): The table with the metadata for one or more frames to check. exprng (:obj:`list`, optional): Range in the allowed exposure time for a frame of type ``ftype``. See :func:`pypeit.core.framematch.check_frame_exptime`. Returns: `numpy.ndarray`_: Boolean array with the flags selecting the exposures in ``fitstbl`` that are ``ftype`` type frames. """ good_exp = framematch.check_frame_exptime(fitstbl['exptime'], exprng) if ftype in ['science', 'standard']: return good_exp & (fitstbl['idname'] == 'object') if ftype in ['bias', 'dark']: return good_exp & self.lamps(fitstbl, 'off') & (fitstbl['idname'] == 'dark') if ftype in ['pixelflat', 'trace']: # Flats and trace frames are typed together return good_exp & self.lamps(fitstbl, 'dome') & (fitstbl['idname'] == 'flatlamp') if ftype == 'pinhole': # Don't type pinhole frames return np.zeros(len(fitstbl), dtype=bool) if ftype in ['arc', 'tilt']: # TODO: This is a kludge. Allow science frames to also be # classified as arcs is_arc = self.lamps(fitstbl, 'arcs') & (fitstbl['idname'] == 'arclamp') is_obj = self.lamps(fitstbl, 'off') & (fitstbl['idname'] == 'object') return good_exp & (is_arc | is_obj) msgs.warn('Cannot determine if frames are of type {0}.'.format(ftype)) return np.zeros(len(fitstbl), dtype=bool) def lamps(self, fitstbl, status): """ Check the lamp status. Args: fitstbl (`astropy.table.Table`_): The table with the fits header meta data. status (:obj:`str`): The status to check. Can be ``'off'``, ``'arcs'``, or ``'dome'``. Returns: `numpy.ndarray`_: A boolean array selecting fits files that meet the selected lamp status. Raises: ValueError: Raised if the status is not one of the valid options. """ if status == 'off': # Check if all are off return np.all(np.array([fitstbl[k] == 0 for k in fitstbl.keys() if 'lampstat' in k]), axis=0) if status == 'arcs': # Check if any arc lamps are on arc_lamp_stat = [ 'lampstat{0:02d}'.format(i) for i in range(1,6) ] return np.any(np.array([ fitstbl[k] == 1 for k in fitstbl.keys() if k in arc_lamp_stat]), axis=0) if status == 'dome': return fitstbl['lampstat01'] == '1' raise ValueError('No implementation for status = {0}'.format(status))
class KeckNIRSPECSpectrograph(spectrograph.Spectrograph): """ Child to handle Keck/NIRSPEC specific code """ ndet = 1 telescope = telescopes.KeckTelescopePar() camera = 'NIRSPEC' def get_detector_par(self, hdu, det): """ Return metadata for the selected detector. Args: hdu (`astropy.io.fits.HDUList`_): The open fits file with the raw image of interest. det (:obj:`int`): 1-indexed detector number. Returns: :class:`~pypeit.images.detector_container.DetectorContainer`: Object with the detector metadata. """ detector_dict = dict( det=1, binning ='1,1', # No binning allowed dataext = 0, specaxis = 0, specflip = False, spatflip = False, platescale = 0.193, darkcurr = 0.8, saturation = 100000., nonlinear = 1.00, # docs say linear to 90,000 but our flats are usually higher numamplifiers = 1, mincounts = -1e10, gain = np.atleast_1d(5.8), ronoise = np.atleast_1d(23.), datasec = np.atleast_1d('[:,:]'), oscansec = np.atleast_1d('[:,:]') ) return detector_container.DetectorContainer(**detector_dict) @classmethod def default_pypeit_par(cls): """ Return the default parameters to use for this instrument. Returns: :class:`~pypeit.par.pypeitpar.PypeItPar`: Parameters required by all of ``PypeIt`` methods. """ par = super().default_pypeit_par() # Wavelengths # 1D wavelength solution par['calibrations']['wavelengths']['rms_threshold'] = 0.20 #0.20 # Might be grating dependent.. par['calibrations']['wavelengths']['sigdetect']=5.0 par['calibrations']['wavelengths']['fwhm']= 5.0 par['calibrations']['wavelengths']['n_final']= 4 par['calibrations']['wavelengths']['lamps'] = ['OH_NIRES'] #par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] par['calibrations']['wavelengths']['method'] = 'holy-grail' # Reidentification parameters #par['calibrations']['wavelengths']['reid_arxiv'] = 'keck_nires.fits' par['calibrations']['slitedges']['edge_thresh'] = 200. par['calibrations']['slitedges']['sync_predict'] = 'nearest' # Flats par['calibrations']['flatfield']['tweak_slits_thresh'] = 0.80 # Extraction par['reduce']['skysub']['bspline_spacing'] = 0.8 par['reduce']['extraction']['sn_gauss'] = 4.0 # Flexure par['flexure']['spec_method'] = 'skip' par['scienceframe']['process']['sigclip'] = 20.0 par['scienceframe']['process']['satpix'] ='nothing' # Should be we be illumflattening? # Flats turn_off = dict(use_illumflat=False, use_biasimage=False, use_overscan=False, use_darkimage=False) par.reset_all_processimages_par(**turn_off) #turn_off = dict(use_biasimage=False, use_overscan=False) #par.reset_all_processimages_par(**turn_off) # The settings below enable NIRSPEC dark subtraction from the # traceframe and pixelflatframe, but enforce that this bias won't be # subtracted from other images. It is a hack for now, because # eventually we want to perform this operation with the dark frame # class, and we want to attach individual sets of darks to specific # images. #par['calibrations']['biasframe']['useframe'] = 'bias' #par['calibrations']['traceframe']['process']['bias'] = 'force' #par['calibrations']['pixelflatframe']['process']['bias'] = 'force' #par['calibrations']['arcframe']['process']['bias'] = 'skip' #par['calibrations']['tiltframe']['process']['bias'] = 'skip' #par['calibrations']['standardframe']['process']['bias'] = 'skip' #par['scienceframe']['process']['bias'] = 'skip' # Set the default exposure time ranges for the frame typing par['calibrations']['standardframe']['exprng'] = [None, 20] par['calibrations']['arcframe']['exprng'] = [20, None] par['calibrations']['darkframe']['exprng'] = [20, None] par['scienceframe']['exprng'] = [20, None] # Sensitivity function parameters par['sensfunc']['algorithm'] = 'IR' par['sensfunc']['polyorder'] = 8 par['sensfunc']['IR']['telgridfile'] \ = resource_filename('pypeit', '/data/telluric/TelFit_MaunaKea_3100_26100_R20000.fits') return par def init_meta(self): """ Define how metadata are derived from the spectrograph files. That is, this associates the ``PypeIt``-specific metadata keywords with the instrument-specific header cards using :attr:`meta`. """ self.meta = {} # Required (core) self.meta['ra'] = dict(ext=0, card='RA') self.meta['dec'] = dict(ext=0, card='DEC') self.meta['target'] = dict(ext=0, card='TARGNAME') self.meta['decker'] = dict(ext=0, card='SLITNAME') self.meta['binning'] = dict(ext=0, card=None, default='1,1') self.meta['mjd'] = dict(ext=0, card='MJD-OBS') self.meta['exptime'] = dict(ext=0, card='ELAPTIME') self.meta['airmass'] = dict(ext=0, card='AIRMASS') # Extras for config and frametyping self.meta['dispname'] = dict(ext=0, card='DISPERS') self.meta['hatch'] = dict(ext=0, card='CALMPOS') self.meta['idname'] = dict(ext=0, card='IMAGETYP') # Lamps lamp_names = ['NEON', 'ARGON', 'KRYPTON', 'XENON', 'ETALON', 'FLAT'] for kk,lamp_name in enumerate(lamp_names): self.meta['lampstat{:02d}'.format(kk+1)] = dict(ext=0, card=lamp_name) def configuration_keys(self): """ Return the metadata keys that define a unique instrument configuration. This list is used by :class:`~pypeit.metadata.PypeItMetaData` to identify the unique configurations among the list of frames read for a given reduction. Returns: :obj:`list`: List of keywords of data pulled from file headers and used to constuct the :class:`~pypeit.metadata.PypeItMetaData` object. """ return ['decker', 'dispname'] def pypeit_file_keys(self): """ Define the list of keys to be output into a standard ``PypeIt`` file. Returns: :obj:`list`: The list of keywords in the relevant :class:`~pypeit.metadata.PypeItMetaData` instance to print to the :ref:`pypeit_file`. """ pypeit_keys = super().pypeit_file_keys() # TODO: Why are these added here? See # pypeit.metadata.PypeItMetaData.set_pypeit_cols pypeit_keys += ['calib', 'comb_id', 'bkg_id'] return pypeit_keys def check_frame_type(self, ftype, fitstbl, exprng=None): """ Check for frames of the provided type. Args: ftype (:obj:`str`): Type of frame to check. Must be a valid frame type; see frame-type :ref:`frame_type_defs`. fitstbl (`astropy.table.Table`_): The table with the metadata for one or more frames to check. exprng (:obj:`list`, optional): Range in the allowed exposure time for a frame of type ``ftype``. See :func:`pypeit.core.framematch.check_frame_exptime`. Returns: `numpy.ndarray`_: Boolean array with the flags selecting the exposures in ``fitstbl`` that are ``ftype`` type frames. """ good_exp = framematch.check_frame_exptime(fitstbl['exptime'], exprng) if ftype in ['science', 'standard']: return good_exp & self.lamps(fitstbl, 'off') & (fitstbl['hatch'] == 0) \ & (fitstbl['idname'] == 'object') if ftype in ['bias', 'dark']: return good_exp & self.lamps(fitstbl, 'off') & (fitstbl['hatch'] == 0) \ & (fitstbl['idname'] == 'dark') if ftype in ['pixelflat', 'trace']: # Flats and trace frames are typed together return good_exp & self.lamps(fitstbl, 'dome') & (fitstbl['hatch'] == 1) \ & (fitstbl['idname'] == 'flatlamp') if ftype == 'pinhole': # Don't type pinhole frames return np.zeros(len(fitstbl), dtype=bool) if ftype in ['arc', 'tilt']: # TODO: This is a kludge. Allow science frames to also be # classified as arcs is_arc = self.lamps(fitstbl, 'arcs') & (fitstbl['hatch'] == 1) \ & (fitstbl['idname'] == 'arclamp') is_obj = self.lamps(fitstbl, 'off') & (fitstbl['hatch'] == 0) \ & (fitstbl['idname'] == 'object') return good_exp & (is_arc | is_obj) msgs.warn('Cannot determine if frames are of type {0}.'.format(ftype)) return np.zeros(len(fitstbl), dtype=bool) def lamps(self, fitstbl, status): """ Check the lamp status. Args: fitstbl (`astropy.table.Table`_): The table with the fits header meta data. status (:obj:`str`): The status to check. Can be ``'off'``, ``'arcs'``, or ``'dome'``. Returns: `numpy.ndarray`_: A boolean array selecting fits files that meet the selected lamp status. Raises: ValueError: Raised if the status is not one of the valid options. """ if status == 'off': # Check if all are off return np.all(np.array([fitstbl[k] == 0 for k in fitstbl.keys() if 'lampstat' in k]), axis=0) if status == 'arcs': # Check if any arc lamps are on arc_lamp_stat = [ 'lampstat{0:02d}'.format(i) for i in range(1,6) ] return np.any(np.array([ fitstbl[k] == 1 for k in fitstbl.keys() if k in arc_lamp_stat]), axis=0) if status == 'dome': return fitstbl['lampstat06'] == 1 raise ValueError('No implementation for status = {0}'.format(status)) def bpm(self, filename, det, shape=None, msbias=None): """ Generate a default bad-pixel mask. Even though they are both optional, either the precise shape for the image (``shape``) or an example file that can be read to get the shape (``filename`` using :func:`get_image_shape`) *must* be provided. Args: filename (:obj:`str` or None): An example file to use to get the image shape. det (:obj:`int`): 1-indexed detector number to use when getting the image shape from the example file. shape (tuple, optional): Processed image shape Required if filename is None Ignored if filename is not None msbias (`numpy.ndarray`_, optional): Master bias frame used to identify bad pixels Returns: `numpy.ndarray`_: An integer array with a masked value set to 1 and an unmasked value set to 0. All values are set to 0. """ # Call the base-class method to generate the empty bpm bpm_img = super().bpm(filename, det, shape=shape, msbias=msbias) # Edges of the detector are junk msgs.info("Custom bad pixel mask for NIRSPEC") bpm_img[:, :20] = 1. bpm_img[:, 1000:] = 1. return bpm_img
class KeckKCWISpectrograph(spectrograph.Spectrograph): """ Child to handle Keck/KCWI specific code .. todo:: Need to apply spectral flexure and heliocentric correction to waveimg """ ndet = 1 name = 'keck_kcwi' telescope = telescopes.KeckTelescopePar() camera = 'KCWI' pypeline = 'IFU' supported = True comment = 'Supported setups: BM, BH2; see :doc:`keck_kcwi`' def __init__(self): super().__init__() # TODO :: Might need to change the tolerance of disperser angle in # pypeit setup (two BH2 nights where sufficiently different that this # was important). # TODO :: Might consider changing TelescopePar to use the astropy # EarthLocation. KBW: Fine with me! self.location = EarthLocation.of_site('Keck Observatory') def get_detector_par(self, hdu, det): """ Return metadata for the selected detector. Args: hdu (`astropy.io.fits.HDUList`_): The open fits file with the raw image of interest. det (:obj:`int`): 1-indexed detector number. Returns: :class:`~pypeit.images.detector_container.DetectorContainer`: Object with the detector metadata. """ # Some properties of the image head0 = hdu[0].header binning = self.compound_meta(self.get_headarr(hdu), "binning") numamps = head0['NVIDINP'] specflip = True if head0['AMPID1'] == 2 else False gainmul, gainarr = head0['GAINMUL'], np.zeros(numamps) ronarr = np.zeros( numamps ) # Set this to zero (determine the readout noise from the overscan regions) dsecarr = np.array([''] * numamps) for ii in range(numamps): # Assign the gain for this amplifier gainarr[ii] = head0["GAIN{0:1d}".format(ii + 1)] # * gainmul detector = dict( det=det, binning=binning, dataext=0, specaxis=0, specflip=specflip, spatflip=False, platescale=0.145728, # arcsec/pixel darkcurr=None, # <-- TODO : Need to set this mincounts=-1e10, saturation=65535., nonlinear=0.95, # For lack of a better number! numamplifiers=numamps, gain=gainarr, ronoise=ronarr, datasec=dsecarr.copy(), # <-- This is provided in the header oscansec=dsecarr.copy(), # <-- This is provided in the header ) # Return return detector_container.DetectorContainer(**detector) def config_specific_par(self, scifile, inp_par=None): """ Modify the ``PypeIt`` parameters to hard-wired values used for specific instrument configurations. Args: scifile (:obj:`str`): File to use when determining the configuration and how to adjust the input parameters. inp_par (:class:`~pypeit.par.parset.ParSet`, optional): Parameter set used for the full run of PypeIt. If None, use :func:`default_pypeit_par`. Returns: :class:`~pypeit.par.parset.ParSet`: The PypeIt parameter set adjusted for configuration specific parameter values. """ par = super().config_specific_par(scifile, inp_par=inp_par) headarr = self.get_headarr(scifile) # Templates if self.get_meta_value(headarr, 'dispname') == 'BH2': par['calibrations']['wavelengths'][ 'method'] = 'full_template' # 'full_template' par['calibrations']['wavelengths'][ 'reid_arxiv'] = 'keck_kcwi_BH2_4200.fits' par['calibrations']['wavelengths']['lamps'] = [ 'FeI', 'ArI', 'ArII' ] elif self.get_meta_value(headarr, 'dispname') == 'BM': par['calibrations']['wavelengths']['method'] = 'full_template' par['calibrations']['wavelengths'][ 'reid_arxiv'] = 'keck_kcwi_BM.fits' par['calibrations']['wavelengths']['lamps'] = [ 'FeI', 'ArI', 'ArII' ] # FWHM # binning = parse.parse_binning(self.get_meta_value(headarr, 'binning')) # par['calibrations']['wavelengths']['fwhm'] = 6.0 / binning[1] # Return return par def init_meta(self): """ Define how metadata are derived from the spectrograph files. That is, this associates the ``PypeIt``-specific metadata keywords with the instrument-specific header cards using :attr:`meta`. """ self.meta = {} # Required (core) self.meta['ra'] = dict(ext=0, card=None, compound=True) self.meta['dec'] = dict(ext=0, card=None, compound=True) self.meta['target'] = dict(ext=0, card='TARGNAME') self.meta['dispname'] = dict(ext=0, card='BGRATNAM') self.meta['decker'] = dict(ext=0, card='IFUNAM') self.meta['binning'] = dict(card=None, compound=True) self.meta['mjd'] = dict(ext=0, card='MJD') self.meta['exptime'] = dict(ext=0, card='ELAPTIME') self.meta['airmass'] = dict(ext=0, card='AIRMASS') # Extras for config and frametyping self.meta['hatch'] = dict(ext=0, card='HATNUM') self.meta['idname'] = dict(ext=0, card='CALXPOS') self.meta['dispangle'] = dict(ext=0, card='BGRANGLE', rtol=0.01) self.meta['slitwid'] = dict(card=None, compound=True) # Get atmospheric conditions (note, these are the conditions at the end of the exposure) self.meta['obstime'] = dict(card=None, compound=True, required=False) self.meta['pressure'] = dict(card=None, compound=True, required=False) self.meta['temperature'] = dict(card=None, compound=True, required=False) self.meta['humidity'] = dict(card=None, compound=True, required=False) # Lamps lamp_names = ['LMP0', 'LMP1', 'LMP2', 'LMP3'] # FeAr, ThAr, Aux, Continuum for kk, lamp_name in enumerate(lamp_names): self.meta['lampstat{:02d}'.format(kk + 1)] = dict(ext=0, card=lamp_name + 'STAT') for kk, lamp_name in enumerate(lamp_names): if lamp_name == 'LMP3': # There is no shutter on LMP3 self.meta['lampshst{:02d}'.format(kk + 1)] = dict(ext=0, card=None, default=1) continue self.meta['lampshst{:02d}'.format(kk + 1)] = dict(ext=0, card=lamp_name + 'SHST') @classmethod def default_pypeit_par(cls): """ Return the default parameters to use for this instrument. Returns: :class:`~pypeit.par.pypeitpar.PypeItPar`: Parameters required by all of ``PypeIt`` methods. """ par = super().default_pypeit_par() # Subtract the detector pattern from certain frames par['calibrations']['biasframe']['process']['use_pattern'] = True par['calibrations']['darkframe']['process']['use_pattern'] = True par['calibrations']['pixelflatframe']['process']['use_pattern'] = False par['calibrations']['illumflatframe']['process']['use_pattern'] = True par['calibrations']['standardframe']['process']['use_pattern'] = True par['scienceframe']['process']['use_pattern'] = True # Make sure the overscan is subtracted from the dark par['calibrations']['darkframe']['process']['use_overscan'] = True # Set the slit edge parameters par['calibrations']['slitedges']['fit_order'] = 4 # Always correct for flexure, starting with default parameters # slitcen must be used, because this is a slit-based IFU where # no objects are extracted. par['flexure']['spec_method'] = 'slitcen' # Alter the method used to combine pixel flats par['calibrations']['pixelflatframe']['process']['combine'] = 'median' par['calibrations']['flatfield']['spec_samp_coarse'] = 20.0 #par['calibrations']['flatfield']['tweak_slits'] = False # Do not tweak the slit edges (we want to use the full slit) par['calibrations']['flatfield'][ 'tweak_slits_thresh'] = 0.0 # Make sure the full slit is used (i.e. when the illumination fraction is > 0.5) par['calibrations']['flatfield'][ 'tweak_slits_maxfrac'] = 0.0 # Make sure the full slit is used (i.e. no padding) par['calibrations']['flatfield'][ 'slit_trim'] = 3 # Trim the slit edges par['calibrations']['flatfield'][ 'slit_illum_relative'] = True # Calculate the relative slit illumination # Set the default exposure time ranges for the frame typing par['calibrations']['biasframe']['exprng'] = [None, 0.01] par['calibrations']['darkframe']['exprng'] = [0.01, None] par['calibrations']['pinholeframe']['exprng'] = [999999, None ] # No pinhole frames par['calibrations']['pixelflatframe']['exprng'] = [None, 30] par['calibrations']['traceframe']['exprng'] = [None, 30] par['scienceframe']['exprng'] = [30, None] # Set the number of alignments in the align frames par['calibrations']['alignment']['locations'] = [ 0.1, 0.3, 0.5, 0.7, 0.9 ] # TODO:: Check this!! # LACosmics parameters par['scienceframe']['process']['sigclip'] = 4.0 par['scienceframe']['process']['objlim'] = 1.5 par['scienceframe']['process'][ 'use_illumflat'] = True # illumflat is applied when building the relative scale image in reduce.py, so should be applied to scienceframe too. par['scienceframe']['process'][ 'use_specillum'] = True # apply relative spectral illumination par['scienceframe']['process'][ 'spat_flexure_correct'] = True # correct for spatial flexure par['scienceframe']['process']['use_biasimage'] = False par['scienceframe']['process']['use_darkimage'] = False # Don't do optimal extraction for 3D data. par['reduce']['extraction']['skip_optimal'] = True # Make sure that this is reduced as a slit (as opposed to fiber) spectrograph par['reduce']['cube']['slit_spec'] = True # Sky subtraction parameters par['reduce']['skysub']['no_poly'] = True par['reduce']['skysub']['bspline_spacing'] = 0.6 par['reduce']['skysub']['joint_fit'] = True return par def compound_meta(self, headarr, meta_key): """ Methods to generate metadata requiring interpretation of the header data, instead of simply reading the value of a header card. Args: headarr (:obj:`list`): List of `astropy.io.fits.Header`_ objects. meta_key (:obj:`str`): Metadata keyword to construct. Returns: object: Metadata value read from the header(s). """ if meta_key == 'binning': binspatial, binspec = parse.parse_binning(headarr[0]['BINNING']) binning = parse.binning2string(binspec, binspatial) return binning elif meta_key == 'slitwid': # Get the slice scale slicescale = 0.00037718 # Degrees per 'large slicer' slice ifunum = headarr[0]['IFUNUM'] if ifunum == 2: slicescale /= 2.0 elif ifunum == 3: slicescale /= 4.0 return slicescale elif meta_key == 'ra' or meta_key == 'dec': try: if self.is_nasmask(headarr[0]): hdrstr = 'RABASE' if meta_key == 'ra' else 'DECBASE' else: hdrstr = 'RA' if meta_key == 'ra' else 'DEC' except KeyError: try: hdrstr = 'TARGRA' if meta_key == 'ra' else 'TARGDEC' except KeyError: hdrstr = '' return headarr[0][hdrstr] elif meta_key == 'pressure': return headarr[0]['WXPRESS'] * 0.001 * units.bar elif meta_key == 'temperature': return headarr[0]['WXOUTTMP'] * units.deg_C elif meta_key == 'humidity': return headarr[0]['WXOUTHUM'] / 100.0 elif meta_key == 'obstime': return Time(headarr[0]['DATE-END']) else: msgs.error("Not ready for this compound meta") def configuration_keys(self): """ Return the metadata keys that define a unique instrument configuration. This list is used by :class:`~pypeit.metadata.PypeItMetaData` to identify the unique configurations among the list of frames read for a given reduction. Returns: :obj:`list`: List of keywords of data pulled from file headers and used to constuct the :class:`~pypeit.metadata.PypeItMetaData` object. """ return ['dispname', 'decker', 'binning', 'dispangle'] def check_frame_type(self, ftype, fitstbl, exprng=None): """ Check for frames of the provided type. Args: ftype (:obj:`str`): Type of frame to check. Must be a valid frame type; see frame-type :ref:`frame_type_defs`. fitstbl (`astropy.table.Table`_): The table with the metadata for one or more frames to check. exprng (:obj:`list`, optional): Range in the allowed exposure time for a frame of type ``ftype``. See :func:`pypeit.core.framematch.check_frame_exptime`. Returns: `numpy.ndarray`_: Boolean array with the flags selecting the exposures in ``fitstbl`` that are ``ftype`` type frames. """ good_exp = framematch.check_frame_exptime(fitstbl['exptime'], exprng) if ftype == 'science': return good_exp & self.lamps(fitstbl, 'off') & ( fitstbl['hatch'] == '1') #hatch=1,0=open,closed if ftype == 'bias': return good_exp & self.lamps(fitstbl, 'off') & (fitstbl['hatch'] == '0') if ftype in ['pixelflat', 'illumflat', 'trace']: # Flats and trace frames are typed together return good_exp & self.lamps(fitstbl, 'dome_noarc') & ( fitstbl['hatch'] == '0') & (fitstbl['idname'] == '6') if ftype in ['dark']: # Dark frames return good_exp & self.lamps(fitstbl, 'off') & (fitstbl['hatch'] == '0') if ftype in ['align']: # Alignment frames return good_exp & self.lamps(fitstbl, 'dome') & ( fitstbl['hatch'] == '0') & (fitstbl['idname'] == '4') if ftype in ['arc', 'tilt']: return good_exp & self.lamps(fitstbl, 'arcs') & (fitstbl['hatch'] == '0') if ftype in ['pinhole']: # Don't type pinhole frames return np.zeros(len(fitstbl), dtype=bool) msgs.warn('Cannot determine if frames are of type {0}.'.format(ftype)) return np.zeros(len(fitstbl), dtype=bool) def lamps(self, fitstbl, status): """ Check the lamp status. Args: fitstbl (`astropy.table.Table`_): The table with the fits header meta data. status (:obj:`str`): The status to check. Can be ``'off'``, ``'arcs'``, or ``'dome'``. Returns: `numpy.ndarray`_: A boolean array selecting fits files that meet the selected lamp status. Raises: ValueError: Raised if the status is not one of the valid options. """ if status == 'off': # Check if all are off lampstat = np.array([(fitstbl[k] == '0') | (fitstbl[k] == 'None') for k in fitstbl.keys() if 'lampstat' in k]) lampshst = np.array([(fitstbl[k] == '0') | (fitstbl[k] == 'None') for k in fitstbl.keys() if 'lampshst' in k]) return np.all(lampstat, axis=0) # Lamp has to be off # return np.all(lampstat | lampshst, axis=0) # i.e. either the shutter is closed or the lamp is off if status == 'arcs': # Check if any arc lamps are on (FeAr | ThAr) arc_lamp_stat = ['lampstat{0:02d}'.format(i) for i in range(1, 3)] arc_lamp_shst = ['lampshst{0:02d}'.format(i) for i in range(1, 3)] lamp_stat = np.array([ fitstbl[k] == '1' for k in fitstbl.keys() if k in arc_lamp_stat ]) lamp_shst = np.array([ fitstbl[k] == '1' for k in fitstbl.keys() if k in arc_lamp_shst ]) # Make sure the continuum frames are off dome_lamps = ['lampstat{0:02d}'.format(i) for i in range(4, 5)] dome_lamp_stat = np.array( [fitstbl[k] == '0' for k in fitstbl.keys() if k in dome_lamps]) return np.any(lamp_stat & lamp_shst & dome_lamp_stat, axis=0) # i.e. lamp on and shutter open if status in ['dome_noarc', 'dome']: # Check if any dome lamps are on (Continuum) - Ignore lampstat03 (Aux) - not sure what this is used for dome_lamp_stat = ['lampstat{0:02d}'.format(i) for i in range(4, 5)] lamp_stat = np.array([ fitstbl[k] == '1' for k in fitstbl.keys() if k in dome_lamp_stat ]) if status == 'dome_noarc': # Make sure arcs are off - it seems even with the shutter closed, the arcs arc_lamps = ['lampstat{0:02d}'.format(i) for i in range(1, 3)] arc_lamp_stat = np.array([ fitstbl[k] == '0' for k in fitstbl.keys() if k in arc_lamps ]) lamp_stat = lamp_stat & arc_lamp_stat return np.any(lamp_stat, axis=0) # i.e. lamp on raise ValueError('No implementation for status = {0}'.format(status)) def get_lamps_status(self, headarr): """ Return a string containing the information on the lamp status. Args: headarr (:obj:`list`): A list of 1 or more `astropy.io.fits.Header`_ objects. Returns: :obj:`str`: A string that uniquely represents the lamp status. """ # Loop through all lamps and collect their status kk = 1 lampstat = [] while True: lampkey1 = 'lampstat{:02d}'.format(kk) if lampkey1 not in self.meta.keys(): break ext1, card1 = self.meta[lampkey1]['ext'], self.meta[lampkey1][ 'card'] lampkey2 = 'lampshst{:02d}'.format(kk) if self.meta[lampkey2]['card'] is None: lampstat += [str(headarr[ext1][card1])] else: ext2, card2 = self.meta[lampkey2]['ext'], self.meta[lampkey2][ 'card'] lampstat += [ "{0:s}-{1:s}".format(str(headarr[ext1][card1]), str(headarr[ext2][card2])) ] kk += 1 return "_".join(lampstat) def get_rawimage(self, raw_file, det): """ Read a raw KCWI data frame NOTE: The amplifiers are arranged as follows: | (0,ny) --------- (nx,ny) | | 3 | 4 | | --------- | | 1 | 2 | | (0,0) --------- (nx, 0) Parameters ---------- raw_file : :obj:`str` File to read det : :obj:`int` 1-indexed detector to read Returns ------- detector_par : :class:`pypeit.images.detector_container.DetectorContainer` Detector metadata parameters. raw_img : `numpy.ndarray`_ Raw image for this detector. hdu : `astropy.io.fits.HDUList`_ Opened fits file exptime : :obj:`float` Exposure time read from the file header rawdatasec_img : `numpy.ndarray`_ Data (Science) section of the detector as provided by setting the (1-indexed) number of the amplifier used to read each detector pixel. Pixels unassociated with any amplifier are set to 0. oscansec_img : `numpy.ndarray`_ Overscan section of the detector as provided by setting the (1-indexed) number of the amplifier used to read each detector pixel. Pixels unassociated with any amplifier are set to 0. """ # Check for file; allow for extra .gz, etc. suffix fil = glob.glob(raw_file + '*') if len(fil) != 1: msgs.error("Found {:d} files matching {:s}".format( len(fil), raw_file)) # Read msgs.info("Reading KCWI file: {:s}".format(fil[0])) hdu = io.fits_open(fil[0]) detpar = self.get_detector_par(hdu, det if det is None else 1) head0 = hdu[0].header raw_img = hdu[detpar['dataext']].data.astype(float) # Some properties of the image numamps = head0['NVIDINP'] # Exposure time (used by ProcessRawImage) headarr = self.get_headarr(hdu) exptime = self.get_meta_value(headarr, 'exptime') # get the x and y binning factors... #binning = self.get_meta_value(headarr, 'binning') # Always assume normal FITS header formatting one_indexed = True include_last = True for section in ['DSEC', 'BSEC']: # Initialize the image (0 means no amplifier) pix_img = np.zeros(raw_img.shape, dtype=int) for i in range(numamps): # Get the data section sec = head0[section + "{0:1d}".format(i + 1)] # Convert the data section from a string to a slice # TODO :: RJC - I think something has changed here... and the BPM is flipped (or not flipped) for different amp modes. # TODO :: RJC - Note, KCWI records binned sections, so there's no need to pass binning in as an arguement datasec = parse.sec2slice(sec, one_indexed=one_indexed, include_end=include_last, require_dim=2) #, binning=binning) # Flip the datasec datasec = datasec[::-1] # Assign the amplifier pix_img[datasec] = i + 1 # Finish if section == 'DSEC': rawdatasec_img = pix_img.copy() elif section == 'BSEC': oscansec_img = pix_img.copy() # Calculate the pattern frequency hdu = self.calc_pattern_freq(raw_img, rawdatasec_img, oscansec_img, hdu) # Return return detpar, raw_img, hdu, exptime, rawdatasec_img, oscansec_img def calc_pattern_freq(self, frame, rawdatasec_img, oscansec_img, hdu): """ Calculate the pattern frequency using the overscan region that covers the overscan and data sections. Using a larger range allows the frequency to be pinned down with high accuracy. NOTE: The amplifiers are arranged as follows: | (0,ny) --------- (nx,ny) | | 3 | 4 | | --------- | | 1 | 2 | | (0,0) --------- (nx, 0) .. todo:: PATTERN FREQUENCY ALGORITHM HAS NOT BEEN TESTED WHEN BINNING != 1x1 Parameters ---------- frame : `numpy.ndarray`_ Raw data frame to be used to estimate the pattern frequency. rawdatasec_img : `numpy.ndarray`_ Array the same shape as ``frame``, used as a mask to identify the data pixels (0 is no data, non-zero values indicate the amplifier number). oscansec_img : `numpy.ndarray`_ Array the same shape as ``frame``, used as a mask to identify the overscan pixels (0 is no data, non-zero values indicate the amplifier number). hdu : `astropy.io.fits.HDUList`_ Opened fits file. Returns ------- hdu : `astropy.io.fits.HDUList`_ The input HDUList, with header updated to include the frequency of each amplifier. """ msgs.info("Calculating pattern noise frequency") # Make a copy of te original frame raw_img = frame.copy() # Get a unique list of the amplifiers unq_amps = np.sort(np.unique( oscansec_img[np.where(oscansec_img >= 1)])) num_amps = unq_amps.size # Loop through amplifiers and calculate the frequency for amp in unq_amps: # Grab the pixels where the amplifier has data pixs = np.where((rawdatasec_img == amp) | (oscansec_img == amp)) rmin, rmax = np.min(pixs[1]), np.max(pixs[1]) # Deal with the different locations of the overscan regions in 2- and 4- amp mode if num_amps == 2: cmin = 1 + np.max(pixs[0]) frame = raw_img[cmin:, rmin:rmax].astype(np.float64) elif num_amps == 4: if amp in [1, 2]: pixalt = np.where((rawdatasec_img == amp + 2) | (oscansec_img == amp + 2)) cmin = 1 + np.max(pixs[0]) cmax = ( np.min(pixalt[0]) + cmin ) // 2 # Average of the bottom of the top amp, and top of the bottom amp else: pixalt = np.where((rawdatasec_img == amp - 2) | (oscansec_img == amp - 2)) cmax = 1 + np.min(pixs[0]) cmin = (np.max(pixalt[0]) + cmax) // 2 frame = raw_img[cmin:cmax, rmin:rmax].astype(np.float64) # Calculate the pattern frequency freq = procimg.pattern_frequency(frame) msgs.info( "Pattern frequency of amplifier {0:d}/{1:d} = {2:f}".format( amp, num_amps, freq)) # Add the frequency to the zeroth header hdu[0].header['PYPFRQ{0:02d}'.format(amp)] = freq # Return the updated HDU return hdu def bpm(self, filename, det, shape=None, msbias=None): """ Generate a default bad-pixel mask. Even though they are both optional, either the precise shape for the image (``shape``) or an example file that can be read to get the shape (``filename`` using :func:`get_image_shape`) *must* be provided. Args: filename (:obj:`str` or None): An example file to use to get the image shape. det (:obj:`int`): 1-indexed detector number to use when getting the image shape from the example file. shape (tuple, optional): Processed image shape Required if filename is None Ignored if filename is not None msbias (`numpy.ndarray`_, optional): Master bias frame used to identify bad pixels. **This is ignored for KCWI.** Returns: `numpy.ndarray`_: An integer array with a masked value set to 1 and an unmasked value set to 0. All values are set to 0. """ # Call the base-class method to generate the empty bpm; msbias is always set to None. bpm_img = super().bpm(filename, det, shape=shape, msbias=None) # Extract some header info #msgs.info("Reading AMPMODE and BINNING from KCWI file: {:s}".format(filename)) head0 = fits.getheader(filename, ext=0) ampmode = head0['AMPMODE'] binning = head0['BINNING'] # Construct a list of the bad columns # Note: These were taken from v1.1.0 (REL) Date: 2018/06/11 of KDERP # KDERP store values and in the code (stage1) subtract 1 from the badcol data files. # Instead of this, I have already pre-subtracted the values in the following arrays. bc = None if ampmode == 'ALL': if binning == '1,1': bc = [[3676, 3676, 2056, 2244]] elif binning == '2,2': bc = [[1838, 1838, 1028, 1121]] elif ampmode == 'TBO': if binning == '1,1': bc = [[2622, 2622, 619, 687], [2739, 2739, 1748, 1860], [3295, 3300, 2556, 2560], [3675, 3676, 2243, 4111]] elif binning == '2,2': bc = [[1311, 1311, 310, 354], [1369, 1369, 876, 947], [1646, 1650, 1278, 1280], [1838, 1838, 1122, 2055]] if ampmode == 'TUP': if binning == '1,1': bc = [[2622, 2622, 3492, 3528], [3295, 3300, 1550, 1555], [3676, 3676, 1866, 4111]] elif binning == '2,2': bc = [[1311, 1311, 1745, 1788], [1646, 1650, 775, 777], [1838, 1838, 933, 2055]] if bc is None: msgs.warn( "Bad pixel mask is not available for ampmode={0:s} binning={1:s}" .format(ampmode, binning)) bc = [] # Apply these bad columns to the mask for bb in range(len(bc)): bpm_img[bc[bb][2]:bc[bb][3] + 1, bc[bb][0]:bc[bb][1] + 1] = 1 return np.flipud(bpm_img) @staticmethod def is_nasmask(hdr): """ Determine if a frame used nod-and-shuffle. Args: hdr (`astropy.io.fits.Header`_): The header of the raw frame. Returns: :obj:`bool`: True if NAS used. """ return 'Mask' in hdr['BNASNAM'] def get_wcs(self, hdr, slits, platescale, wave0, dwv): """ Construct/Read a World-Coordinate System for a frame. Args: hdr (`astropy.io.fits.Header`_): The header of the raw frame. The information in this header will be extracted and returned as a WCS. slits (:class:`~pypeit.slittrace.SlitTraceSet`): Slit traces. platescale (:obj:`float`): The platescale of an unbinned pixel in arcsec/pixel (e.g. detector.platescale). wave0 (:obj:`float`): The wavelength zeropoint. dwv (:obj:`float`): Change in wavelength per spectral pixel. Returns: `astropy.wcs.wcs.WCS`_: The world-coordinate system. """ msgs.info("Calculating the WCS") # Get the x and y binning factors, and the typical slit length binspec, binspat = parse.parse_binning( self.get_meta_value([hdr], 'binning')) # Get the pixel and slice scales pxscl = platescale * binspat / 3600.0 # Need to convert arcsec to degrees slscl = self.get_meta_value([hdr], 'slitwid') # Get the typical slit length (this changes by ~0.3% over all slits, so a constant is fine for now) slitlength = int( np.round( np.median(slits.get_slitlengths(initial=True, median=True)))) # Get RA/DEC raval = self.compound_meta([hdr], 'ra') decval = self.compound_meta([hdr], 'dec') # Create a coordinate coord = SkyCoord(raval, decval, unit=(units.deg, units.deg)) # Get rotator position if 'ROTPOSN' in hdr: rpos = hdr['ROTPOSN'] else: rpos = 0. if 'ROTREFAN' in hdr: rref = hdr['ROTREFAN'] else: rref = 0. # Get the offset and PA rotoff = 0.0 # IFU-SKYPA offset (degrees) skypa = rpos + rref # IFU position angle (degrees) crota = np.radians(-(skypa + rotoff)) # Calculate the fits coordinates cdelt1 = -slscl #*(24/23) # The factor (24/23) is a hack - It is introduced because the centre of 1st and 24th slices are 23 slices apart... TODO :: Need to think of a better way to deal with this cdelt2 = pxscl if coord is None: ra = 0. dec = 0. crota = 1 else: ra = coord.ra.degree dec = coord.dec.degree # Calculate the CD Matrix cd11 = cdelt1 * np.cos(crota) # RA degrees per column cd12 = abs(cdelt2) * np.sign(cdelt1) * np.sin( crota) # RA degrees per row cd21 = -abs(cdelt1) * np.sign(cdelt2) * np.sin( crota) # DEC degress per column cd22 = cdelt2 * np.cos(crota) # DEC degrees per row # Get reference pixels (set these to the middle of the FOV) crpix1 = 12. # i.e. 24 slices/2 crpix2 = slitlength / 2. crpix3 = 1. # Get the offset porg = hdr['PONAME'] ifunum = hdr['IFUNUM'] if 'IFU' in porg: if ifunum == 1: # Large slicer off1 = 1.0 off2 = 4.0 elif ifunum == 2: # Medium slicer off1 = 1.0 off2 = 5.0 elif ifunum == 3: # Small slicer off1 = 0.05 off2 = 5.6 else: msgs.warn("Unknown IFU number: {0:d}".format(ifunum)) off1 = 0. off2 = 0. off1 /= binspec off2 /= binspat crpix1 += off1 crpix2 += off2 # Create a new WCS object. msgs.info("Generating KCWI WCS") w = wcs.WCS(naxis=3) w.wcs.equinox = hdr['EQUINOX'] w.wcs.name = 'KCWI' w.wcs.radesys = 'FK5' # Insert the coordinate frame w.wcs.cname = ['KCWI RA', 'KCWI DEC', 'KCWI Wavelength'] w.wcs.cunit = [units.degree, units.degree, units.Angstrom] w.wcs.ctype = ["RA---TAN", "DEC--TAN", "AWAV"] w.wcs.crval = [ra, dec, wave0] # RA, DEC, and wavelength zeropoints w.wcs.crpix = [crpix1, crpix2, crpix3] # RA, DEC, and wavelength reference pixels w.wcs.cd = np.array([[cd11, cd12, 0.0], [cd21, cd22, 0.0], [0.0, 0.0, dwv]]) w.wcs.lonpole = 180.0 # Native longitude of the Celestial pole w.wcs.latpole = 0.0 # Native latitude of the Celestial pole return w def get_datacube_bins(self, slitlength, minmax, num_wave): r""" Calculate the bin edges to be used when making a datacube. Args: slitlength (:obj:`int`): Length of the slit in pixels minmax (`numpy.ndarray`_): An array with the minimum and maximum pixel locations on each slit relative to the reference location (usually the centre of the slit). Shape must be :math:`(N_{\rm slits},2)`, and is typically the array returned by :func:`~pypeit.slittrace.SlitTraceSet.get_radec_image`. num_wave (:obj:`int`): Number of wavelength steps. Given by:: int(round((wavemax-wavemin)/delta_wave)) Args: :obj:`tuple`: Three 1D `numpy.ndarray`_ providing the bins to use when constructing a histogram of the spec2d files. The elements are :math:`(x,y,\lambda)`. """ xbins = np.arange(1 + 24) - 12.0 - 0.5 ybins = np.linspace(np.min(minmax[:, 0]), np.max(minmax[:, 1]), 1 + slitlength) - 0.5 spec_bins = np.arange(1 + num_wave) - 0.5 return xbins, ybins, spec_bins
def __init__(self): # Get it started super(KeckMOSFIRESpectrograph, self).__init__() self.telescope = telescopes.KeckTelescopePar() self.spectrograph = 'keck_mosfire' self.camera = 'MOSFIRE'
class KeckNIRESSpectrograph(spectrograph.Spectrograph): """ Child to handle Keck/NIRES specific code """ ndet = 1 name = 'keck_nires' telescope = telescopes.KeckTelescopePar() camera = 'NIRES' header_name = 'NIRES' pypeline = 'Echelle' supported = True def get_detector_par(self, det, hdu=None): """ Return metadata for the selected detector. Args: det (:obj:`int`): 1-indexed detector number. This is not used because NIRES only has one detector! hdu (`astropy.io.fits.HDUList`_, optional): The open fits file with the raw image of interest. If not provided, frame-dependent parameters are set to a default. Returns: :class:`~pypeit.images.detector_container.DetectorContainer`: Object with the detector metadata. """ # Detector 1 detector_dict = dict( binning='1,1', det=1, dataext=0, specaxis=1, specflip=True, spatflip=False, platescale=0.15, darkcurr=0.01, saturation=1e6, # I'm not sure we actually saturate with the DITs??? nonlinear=0.76, mincounts=-1e10, numamplifiers=1, gain=np.atleast_1d(3.8), ronoise=np.atleast_1d(5.0), datasec=np.atleast_1d('[:,:]'), oscansec=None, #np.atleast_1d('[980:1024,:]') # Is this a hack?? ) return detector_container.DetectorContainer(**detector_dict) @classmethod def default_pypeit_par(cls): """ Return the default parameters to use for this instrument. Returns: :class:`~pypeit.par.pypeitpar.PypeItPar`: Parameters required by all of ``PypeIt`` methods. """ par = super().default_pypeit_par() # Wavelengths # 1D wavelength solution par['calibrations']['wavelengths'][ 'rms_threshold'] = 0.20 #0.20 # Might be grating dependent.. par['calibrations']['wavelengths']['sigdetect'] = 5.0 par['calibrations']['wavelengths']['fwhm'] = 5.0 par['calibrations']['wavelengths']['n_final'] = [3, 4, 4, 4, 4] par['calibrations']['wavelengths']['lamps'] = ['OH_NIRES'] #par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] par['calibrations']['wavelengths']['method'] = 'reidentify' # Reidentification parameters par['calibrations']['wavelengths']['reid_arxiv'] = 'keck_nires.fits' par['calibrations']['wavelengths']['ech_fix_format'] = True # Echelle parameters par['calibrations']['wavelengths']['echelle'] = True par['calibrations']['wavelengths']['ech_nspec_coeff'] = 4 par['calibrations']['wavelengths']['ech_norder_coeff'] = 6 par['calibrations']['wavelengths']['ech_sigrej'] = 3.0 par['calibrations']['slitedges']['trace_thresh'] = 10. par['calibrations']['slitedges']['fit_min_spec_length'] = 0.4 par['calibrations']['slitedges']['left_right_pca'] = True par['calibrations']['slitedges']['fwhm_gaussian'] = 4.0 # Tilt parameters par['calibrations']['tilts']['tracethresh'] = 10.0 #par['calibrations']['tilts']['spat_order'] = 3 #par['calibrations']['tilts']['spec_order'] = 3 # Processing steps turn_off = dict(use_illumflat=False, use_biasimage=False, use_overscan=False, use_darkimage=False) par.reset_all_processimages_par(**turn_off) # Extraction par['reduce']['skysub']['bspline_spacing'] = 0.8 par['reduce']['extraction']['sn_gauss'] = 4.0 # Flexure par['flexure']['spec_method'] = 'skip' par['scienceframe']['process']['sigclip'] = 20.0 par['scienceframe']['process']['satpix'] = 'nothing' par['reduce']['extraction']['boxcar_radius'] = 0.75 # arcsec # Set the default exposure time ranges for the frame typing par['calibrations']['standardframe']['exprng'] = [None, 60] par['calibrations']['arcframe']['exprng'] = [100, None] par['calibrations']['tiltframe']['exprng'] = [100, None] par['calibrations']['darkframe']['exprng'] = [60, None] par['scienceframe']['exprng'] = [60, None] # Sensitivity function parameters par['sensfunc']['algorithm'] = 'IR' par['sensfunc']['polyorder'] = 8 par['sensfunc']['IR']['maxiter'] = 2 par['sensfunc']['IR'][ 'telgridfile'] = 'TelFit_MaunaKea_3100_26100_R20000.fits' return par def init_meta(self): """ Define how metadata are derived from the spectrograph files. That is, this associates the ``PypeIt``-specific metadata keywords with the instrument-specific header cards using :attr:`meta`. """ self.meta = {} # Required (core) self.meta['ra'] = dict(ext=0, card='RA') self.meta['dec'] = dict(ext=0, card='DEC') self.meta['target'] = dict(ext=0, card='OBJECT') self.meta['decker'] = dict(ext=0, card=None, default='0.55 slit') self.meta['binning'] = dict(ext=0, card=None, default='1,1') self.meta['mjd'] = dict(ext=0, card='MJD-OBS') self.meta['exptime'] = dict(ext=0, card='ITIME') self.meta['airmass'] = dict(ext=0, card='AIRMASS') # Extras for config and frametyping self.meta['dispname'] = dict(ext=0, card='INSTR') self.meta['idname'] = dict(ext=0, card='OBSTYPE') self.meta['frameno'] = dict(ext=0, card='FRAMENUM') self.meta['instrument'] = dict(ext=0, card='INSTRUME') def configuration_keys(self): """ Return the metadata keys that define a unique instrument configuration. This list is used by :class:`~pypeit.metadata.PypeItMetaData` to identify the unique configurations among the list of frames read for a given reduction. Returns: :obj:`list`: List of keywords of data pulled from file headers and used to constuct the :class:`~pypeit.metadata.PypeItMetaData` object. """ return ['dispname'] def pypeit_file_keys(self): """ Define the list of keys to be output into a standard ``PypeIt`` file. Returns: :obj:`list`: The list of keywords in the relevant :class:`~pypeit.metadata.PypeItMetaData` instance to print to the :ref:`pypeit_file`. """ pypeit_keys = super().pypeit_file_keys() # TODO: Why are these added here? See # pypeit.metadata.PypeItMetaData.set_pypeit_cols pypeit_keys += ['frameno', 'calib', 'comb_id', 'bkg_id'] return pypeit_keys def check_frame_type(self, ftype, fitstbl, exprng=None): """ Check for frames of the provided type. Args: ftype (:obj:`str`): Type of frame to check. Must be a valid frame type; see frame-type :ref:`frame_type_defs`. fitstbl (`astropy.table.Table`_): The table with the metadata for one or more frames to check. exprng (:obj:`list`, optional): Range in the allowed exposure time for a frame of type ``ftype``. See :func:`pypeit.core.framematch.check_frame_exptime`. Returns: `numpy.ndarray`_: Boolean array with the flags selecting the exposures in ``fitstbl`` that are ``ftype`` type frames. """ good_exp = framematch.check_frame_exptime(fitstbl['exptime'], exprng) if ftype in ['pinhole', 'bias']: # No pinhole or bias frames return np.zeros(len(fitstbl), dtype=bool) if ftype == 'standard': return good_exp & ((fitstbl['idname'] == 'object') | (fitstbl['idname'] == 'Object')) if ftype == 'dark': return good_exp & (fitstbl['idname'] == 'dark') if ftype in ['pixelflat', 'trace']: return fitstbl['idname'] == 'domeflat' if ftype in 'science': return good_exp & ((fitstbl['idname'] == 'object') | (fitstbl['idname'] == 'Object')) if ftype in ['arc', 'tilt']: return good_exp & ((fitstbl['idname'] == 'object') | (fitstbl['idname'] == 'Object')) return np.zeros(len(fitstbl), dtype=bool) def bpm(self, filename, det, shape=None, msbias=None): """ Generate a default bad-pixel mask. Even though they are both optional, either the precise shape for the image (``shape``) or an example file that can be read to get the shape (``filename`` using :func:`get_image_shape`) *must* be provided. Args: filename (:obj:`str` or None): An example file to use to get the image shape. det (:obj:`int`): 1-indexed detector number to use when getting the image shape from the example file. shape (tuple, optional): Processed image shape Required if filename is None Ignored if filename is not None msbias (`numpy.ndarray`_, optional): Master bias frame used to identify bad pixels. Returns: `numpy.ndarray`_: An integer array with a masked value set to 1 and an unmasked value set to 0. All values are set to 0. """ msgs.info("Custom bad pixel mask for NIRES") # Call the base-class method to generate the empty bpm bpm_img = super().bpm(filename, det, shape=shape, msbias=msbias) if det == 1: bpm_img[:, :20] = 1. bpm_img[:, 1000:] = 1. return bpm_img @property def norders(self): """ Number of orders for this spectograph. Should only defined for echelle spectrographs, and it is undefined for the base class. """ return 5 @property def order_spat_pos(self): """ Return the expected spatial position of each echelle order. """ return np.array( [0.22773035, 0.40613574, 0.56009658, 0.70260714, 0.86335914]) @property def orders(self): """ Return the order number for each echelle order. """ return np.arange(7, 2, -1, dtype=int) @property def spec_min_max(self): """ Return the minimum and maximum spectral pixel expected for the spectral range of each order. """ spec_max = np.asarray([np.inf] * self.norders) spec_min = np.asarray([1024, -np.inf, -np.inf, -np.inf, -np.inf]) return np.vstack((spec_min, spec_max)) def order_platescale(self, order_vec, binning=None): """ Return the platescale for each echelle order. Note that NIRES has no binning. Args: order_vec (`numpy.ndarray`_): The vector providing the order numbers. binning (:obj:`str`, optional): The string defining the spectral and spatial binning. **This is always ignored.** Returns: `numpy.ndarray`_: An array with the platescale for each order provided by ``order``. """ return np.full(order_vec.size, 0.15)