Source code for pypeit.spectrographs.mmt_mmirs

"""
Module for MMT MMIRS

.. include:: ../include/links.rst
"""
import numpy as np
from scipy.signal import savgol_filter

from astropy.time import Time
from astropy.io import fits
from astropy.stats import sigma_clipped_stats

from pypeit import msgs
from pypeit import telescopes
from pypeit import utils
from pypeit import io
from pypeit.core import parse
from pypeit.core import framematch
from pypeit.images import detector_container
from pypeit.spectrographs import spectrograph


[docs]class MMTMMIRSSpectrograph(spectrograph.Spectrograph): """ Child to handle MMT/MMIRS specific code """ ndet = 1 name = 'mmt_mmirs' telescope = telescopes.MMTTelescopePar() camera = 'MMIRS' url = 'https://lweb.cfa.harvard.edu/mmti/mmirs.html' header_name = 'mmirs' supported = True
[docs] 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=1, card='RA') self.meta['dec'] = dict(ext=1, card='DEC') self.meta['target'] = dict(ext=1, card='OBJECT') self.meta['decker'] = dict(ext=1, card='APERTURE') self.meta['dichroic'] = dict(ext=1, card='FILTER') self.meta['binning'] = dict(ext=1, card=None, default='1,1') self.meta['mjd'] = dict(ext=0, card=None, compound=True) self.meta['exptime'] = dict(ext=1, card='EXPTIME') self.meta['airmass'] = dict(ext=1, card='AIRMASS') # Extras for config and frametyping self.meta['dispname'] = dict(ext=1, card='DISPERSE') self.meta['idname'] = dict(ext=1, card='IMAGETYP') self.meta['instrument'] = dict(ext=1, card='INSTRUME')
[docs] 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). """ # TODO: This should be how we always deal with timeunit = 'isot'. Are # we doing that for all the relevant spectrographs? if meta_key == 'mjd': time = headarr[1]['DATE-OBS'] ttime = Time(time, format='isot') return ttime.mjd msgs.error("Not ready for this compound meta")
[docs] def raw_header_cards(self): """ Return additional raw header cards to be propagated in downstream output files for configuration identification. The list of raw data FITS keywords should be those used to populate the :meth:`~pypeit.spectrographs.spectrograph.Spectrograph.configuration_keys` or are used in :meth:`~pypeit.spectrographs.spectrograph.Spectrograph.config_specific_par` for a particular spectrograph, if different from the name of the PypeIt metadata keyword. This list is used by :meth:`~pypeit.spectrographs.spectrograph.Spectrograph.subheader_for_spec` to include additional FITS keywords in downstream output files. Returns: :obj:`list`: List of keywords from the raw data files that should be propagated in output files. """ return ['DISPERSE']
[docs] def get_detector_par(self, det, hdu=None): """ Return metadata for the selected detector. Args: det (:obj:`int`): 1-indexed detector number. 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 = 1, specaxis = 0, specflip = False, spatflip = False, platescale = 0.2012, darkcurr = 36.0, # e-/pixel/hour (=0.01 e-/pixel/s) saturation = 700000., #155400., nonlinear = 1.0, mincounts = -1e10, numamplifiers = 1, gain = np.atleast_1d(0.95), ronoise = np.atleast_1d(3.14), datasec = np.atleast_1d('[:,:]'), oscansec = None, #np.atleast_1d('[:,:]') ) return detector_container.DetectorContainer(**detector_dict)
[docs] @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() # Image processing steps turn_off = dict(use_illumflat=False, use_biasimage=False, use_overscan=False, use_darkimage=False) par.reset_all_processimages_par(**turn_off) #par['calibrations']['traceframe']['process']['use_darkimage'] = True #par['calibrations']['pixelflatframe']['process']['use_darkimage'] = True #par['calibrations']['illumflatframe']['process']['use_darkimage'] = True #par['scienceframe']['process']['use_darkimage'] = True par['scienceframe']['process']['use_illumflat'] = True # Wavelengths # 1D wavelength solution with arc lines par['calibrations']['wavelengths']['rms_thresh_frac_fwhm'] = 0.125 par['calibrations']['wavelengths']['sigdetect']=5 par['calibrations']['wavelengths']['fwhm'] = 4. par['calibrations']['wavelengths']['n_first']=2 par['calibrations']['wavelengths']['n_final']=4 par['calibrations']['wavelengths']['lamps'] = ['OH_NIRES'] par['calibrations']['wavelengths']['match_toler']=5.0 # Set slits and tilts parameters par['calibrations']['tilts']['tracethresh'] = 5 par['calibrations']['tilts']['spat_order'] = 7 par['calibrations']['tilts']['spec_order'] = 5 par['calibrations']['slitedges']['trace_thresh'] = 10. par['calibrations']['slitedges']['edge_thresh'] = 100. par['calibrations']['slitedges']['fit_min_spec_length'] = 0.4 par['calibrations']['slitedges']['sync_predict'] = 'nearest' par['calibrations']['slitedges']['bound_detector'] = True # Set the default exposure time ranges for the frame typing par['calibrations']['standardframe']['exprng'] = [None, 60] par['calibrations']['tiltframe']['exprng'] = [60, None] par['calibrations']['arcframe']['exprng'] = [60, None] par['calibrations']['darkframe']['exprng'] = [30, None] par['scienceframe']['exprng'] = [30, None] # dark # TODO: This is now the default. par['calibrations']['darkframe']['process']['apply_gain'] = True # cosmic ray rejection par['scienceframe']['process']['sigclip'] = 5.0 par['scienceframe']['process']['objlim'] = 2.0 par['scienceframe']['process']['grow'] = 0.5 # Science reduction par['reduce']['findobj']['snr_thresh'] = 5.0 par['reduce']['skysub']['sky_sigrej'] = 5.0 par['reduce']['findobj']['find_trim_edge'] = [5,5] # Do not correct for flexure par['flexure']['spec_method'] = 'skip' # Sensitivity function parameters par['sensfunc']['algorithm'] = 'IR' par['sensfunc']['polyorder'] = 8 # ToDo: replace the telluric grid file for MMT site. par['sensfunc']['IR']['telgridfile'] = 'TellPCA_3000_26000_R10000.fits' return par
[docs] 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. """ # Start with instrument wide par = super().config_specific_par(scifile, inp_par=inp_par) if (self.get_meta_value(scifile, 'dispname')=='HK') and (self.get_meta_value(scifile, 'dichroic')=='zJ'): par['calibrations']['wavelengths']['method'] = 'full_template' par['calibrations']['wavelengths']['reid_arxiv'] = 'mmt_mmirs_HK_zJ.fits' elif (self.get_meta_value(scifile, 'dispname')=='K3000') and (self.get_meta_value(scifile, 'dichroic')=='Kspec'): par['calibrations']['wavelengths']['method'] = 'full_template' par['calibrations']['wavelengths']['reid_arxiv'] = 'mmt_mmirs_K3000_Kspec.fits' elif (self.get_meta_value(scifile, 'dispname')=='J') and (self.get_meta_value(scifile, 'dichroic')=='zJ'): par['calibrations']['wavelengths']['method'] = 'full_template' par['calibrations']['wavelengths']['reid_arxiv'] = 'mmt_mmirs_J_zJ.fits' return par
[docs] 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 in ['pixelflat', 'trace', 'illumflat']: return good_exp & (fitstbl['idname'] == 'flat') if ftype == 'standard': return good_exp & (fitstbl['idname'] == 'object') if ftype == 'science': return good_exp & (fitstbl['idname'] == 'object') if ftype in ['arc', 'tilt']: return good_exp & (fitstbl['idname'] == 'object') if ftype == 'dark': return good_exp & (fitstbl['idname'] == 'dark') msgs.warn('Cannot determine if frames are of type {0}.'.format(ftype)) return np.zeros(len(fitstbl), dtype=bool)
[docs] 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): Processed 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) msgs.info("Using hard-coded BPM for det=1 on MMIRS") # Get the binning hdu = io.fits_open(filename) binning = hdu[1].header['CCDSUM'] hdu.close() # Apply the mask xbin, ybin = int(binning.split(' ')[0]), int(binning.split(' ')[1]) bpm_img[:, 187 // ybin] = 1 return bpm_img
[docs] def get_rawimage(self, raw_file, det): """ Read raw images and generate a few other bits and pieces that are key for image processing. 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. """ fil = utils.find_single_file(f'{raw_file}*', required=True) # Read msgs.info(f'Reading MMIRS file: {fil}') hdu = io.fits_open(fil) head1 = fits.getheader(fil,1) detector_par = self.get_detector_par(det if det is not None else 1, hdu=hdu) # get the x and y binning factors... binning = head1['CCDSUM'] xbin, ybin = [int(ibin) for ibin in binning.split(' ')] # First read over the header info to determine the size of the output array... datasec = head1['DATASEC'] x1, x2, y1, y2 = np.array(parse.load_sections(datasec, fmt_iraf=False)).flatten() # ToDo: I am currently using the standard double correlated frame, that is a difference between # the first and final read-outs. In the future need to explore up-the-ramp fitting. if len(hdu)>2: data = mmirs_read_amp(hdu[1].data.astype('float64')) - mmirs_read_amp(hdu[2].data.astype('float64')) else: data = mmirs_read_amp(hdu[1].data.astype('float64')) array = data[x1-1:x2,y1-1:y2] ## ToDo: This is a hack. Need to solve this issue. I cut at 998 due to the HK zero order contaminating ## the blue part of the zJ+HK spectrum. For other setup, you do not need to cut the detector. if (head1['FILTER']=='zJ') and (head1['DISPERSE']=='HK'): array = array[:int(998/ybin),:] rawdatasec_img = np.ones_like(array,dtype='int') # NOTE: If there is no overscan, must be set to 0s oscansec_img = np.zeros_like(array,dtype='int') # Need the exposure time exptime = hdu[self.meta['exptime']['ext']].header[self.meta['exptime']['card']] # Return, transposing array back to orient the overscan properly return detector_par, np.flipud(array), hdu, exptime, np.flipud(rawdatasec_img),\ np.flipud(np.flipud(oscansec_img))
[docs]def mmirs_read_amp(img, namps=32): """ MMIRS has 32 reading out channels. Need to deal with this issue a little bit. I am not using the pypeit overscan subtraction since we need to do the up-the-ramp fitting in the future. Imported from MMIRS IDL pipeline refpix.pro """ # number of channels for reading out if namps is None: namps = 32 data_shape = np.shape(img) ampsize = int(data_shape[0] / namps) refpix1 = np.array([1, 2, 3]) refpix2 = np.arange(4) + data_shape[0] - 4 refpix_all = np.hstack([[0, 1, 2, 3], np.arange(4) + data_shape[0] - 4]) refvec = np.sum(img[:, refpix_all], axis=1) / np.size(refpix_all) svec = savgol_filter(refvec, 11, polyorder=5) refvec_2d = np.reshape(np.repeat(svec, data_shape[0], axis=0), data_shape) img_out = img - refvec_2d for amp in range(namps): img_out_ref = img_out[np.hstack([refpix1, refpix2]), :] ref1, med1, std1 = sigma_clipped_stats(img_out_ref[:, amp * ampsize + 2 * np.arange(int(ampsize / 2))], sigma=3) ref2, med2, std2 = sigma_clipped_stats(img_out_ref[:, amp * ampsize + 2 * np.arange(int(ampsize / 2)) + 1], sigma=3) ref12 = (ref1 + ref2) / 2. img_out[:, amp * ampsize:(amp + 1) * ampsize] -= ref12 return img_out