"""
Module for Keck/NIRSPEC specific methods.
.. include:: ../include/links.rst
"""
import numpy as np
from pypeit import msgs
from pypeit.images import detector_container
from pypeit import telescopes
from pypeit.core import framematch
from pypeit.spectrographs import spectrograph
[docs]class KeckNIRSPECSpectrograph(spectrograph.Spectrograph):
"""
Child to handle Keck/NIRSPEC specific code
"""
ndet = 1
telescope = telescopes.KeckTelescopePar()
camera = 'NIRSPEC'
url = 'https://www2.keck.hawaii.edu/inst/nirspec/'
header_name = 'NIRSPEC'
[docs] 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 NIRSPEC
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_dict = dict(
det=1,
binning ='1,1', # No binning allowed
dataext = 0,
specaxis = 0,
specflip = False,
spatflip = False,
platescale = 0.193,
darkcurr = 2520.0, # e-/pixel/hour (=0.7 e-/pixel/s)
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 = 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()
# Wavelengths
# 1D wavelength solution
par['calibrations']['wavelengths']['rms_thresh_frac_fwhm'] = 0.06 #0.20 # Might be grating dependent..
par['calibrations']['wavelengths']['sigdetect']=5.0
par['calibrations']['wavelengths']['fwhm']= 3.5
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'] = 'TellPCA_3000_26000_R25000.fits'
par['sensfunc']['IR']['pix_shift_bounds'] = (-8.0,8.0)
# Telluric parameters
par['telluric']['pix_shift_bounds'] = (-8.0,8.0)
return par
[docs] 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']
[docs] 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 += ['comb_id', 'bkg_id']
return pypeit_keys
[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)
hatch = fitstbl['hatch'].data.astype(int)
if ftype in ['science', 'standard']:
return good_exp & self.lamps(fitstbl, 'off') & (hatch == 0) \
& (fitstbl['idname'] == 'object')
if ftype in ['bias', 'dark']:
return good_exp & self.lamps(fitstbl, 'off') & (hatch == 0) \
& (fitstbl['idname'] == 'dark')
if ftype in ['pixelflat', 'trace']:
# Flats and trace frames are typed together
return good_exp & self.lamps(fitstbl, 'dome') & (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') & (hatch == 1) \
& (fitstbl['idname'] == 'arclamp')
is_obj = self.lamps(fitstbl, 'off') & (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)
[docs] 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
lamp_stat = [k for k in fitstbl.keys() if 'lampstat' in k]
retarr = np.zeros((len(lamp_stat), len(fitstbl)), dtype=bool)
for kk, key in enumerate(lamp_stat):
retarr[kk,:] = fitstbl[key].data.astype(int) == 0
return np.all(retarr, axis=0)
if status == 'arcs':
# Check if any arc lamps are on
lamp_stat = [ 'lampstat{0:02d}'.format(i) for i in range(1,6) ]
retarr = np.zeros((len(lamp_stat), len(fitstbl)))
for kk, key in enumerate(lamp_stat):
retarr[kk,:] = fitstbl[key].data.astype(int) == 1
return np.any(retarr, axis=0)
if status == 'dome':
return fitstbl['lampstat06'].data.astype(int) == 1
raise ValueError('No implementation for status = {0}'.format(status))
[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)
# 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
[docs]class KeckNIRSPECLowSpectrograph(KeckNIRSPECSpectrograph):
"""
Child to handle NIRSPEC low-dispersion specific code
"""
name = 'keck_nirspec_low'
supported = True
comment = 'Low-dispersion grating'