"""
Module for Magellan/LDSS3 specific codes
.. include:: ../include/links.rst
"""
import glob
import numpy as np
from pypeit import log
from pypeit import PypeItError
from pypeit import telescopes
from pypeit import io
from pypeit.core import framematch
from pypeit.par import pypeitpar
from pypeit.spectrographs import spectrograph
from pypeit.core import parse
from pypeit.images import detector_container
[docs]
class MagellanLDSS3Spectrograph(spectrograph.Spectrograph):
"""
Child to handle Magellan/LDSS3 specific code
"""
ndet = 1
name = 'magellan_ldss3'
telescope = telescopes.MMTTelescopePar()
camera = 'LDSS3'
supported = True
comment = 'Longslit only (so far)'
[docs]
def get_detector_par(self, det, hdu=None):
"""
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.
"""
# Binning
binning = '1,1' if hdu is None else self.get_meta_value(self.get_headarr(hdu), 'binning')
detector_dict = dict(
binning = binning,
det = 1,
dataext = 1,
specaxis = 0,
specflip = False,
spatflip = False,
xgap = 0.,
ygap = 0.,
ysize = 1.,
platescale = 0.189,
darkcurr = 25.0,
saturation = 205000.,
nonlinear = 0.85,
mincounts = -1e10,
numamplifiers = 2,
gain = np.atleast_1d([1.65,1.47]),
ronoise = np.atleast_1d([4.67,5.06]),
)
# Instantiate
return detector_container.DetectorContainer(**detector_dict)
[docs]
def default_pypeit_par(self):
"""
Return the default parameters to use for this instrument.
Returns:
:class:`~pypeit.par.pypeitpar.PypeItPar`: Parameters required by
all of ``PypeIt`` methods.
"""
par = pypeitpar.PypeItPar()
par['rdx']['spectrograph'] = 'magellan_ldss3'
# Wavelengths
# 1D wavelength solution
par['calibrations']['wavelengths']['rms_thresh_frac_fwhm'] = 0.5
par['calibrations']['wavelengths']['sigdetect'] = 6.
par['calibrations']['wavelengths']['fwhm']= 5.0
par['calibrations']['wavelengths']['match_toler'] = 2.5
par['calibrations']['wavelengths']['n_first'] = 3
par['calibrations']['wavelengths']['n_final'] = 5
par['calibrations']['wavelengths']['method'] = 'holy-grail'
# Tilt and slit parameters
par['calibrations']['tilts']['tracethresh'] = 20.0
par['calibrations']['tilts']['spat_order'] = 6
par['calibrations']['tilts']['spec_order'] = 6
# edges
par['calibrations']['slitedges']['edge_thresh'] = 20.
# Processing steps
turn_off = dict(use_biasimage=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
## Do not perform global sky subtraction for standard stars
par['reduce']['skysub']['global_sky_std'] = False
# Flexure
par['flexure']['spec_method'] = 'boxcar'
# cosmic ray rejection parameters for science frames
par['scienceframe']['process']['sigclip'] = 5.0
par['scienceframe']['process']['objlim'] = 2.0
# Set the default exposure time ranges for the frame typing
par['calibrations']['standardframe']['exprng'] = [10, 500]
par['calibrations']['arcframe']['exprng'] = [0, 10]
par['calibrations']['tiltframe']['exprng'] = [0, 10]
par['calibrations']['darkframe']['exprng'] = [0, None]
par['scienceframe']['exprng'] = [200, None]
# Sensitivity function parameters
par['sensfunc']['algorithm'] = 'IR'
par['sensfunc']['polyorder'] = 7
par['sensfunc']['IR']['telgridfile'] = 'TellPCA_3000_26000_R15000.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.
"""
par = self.default_pypeit_par() if inp_par is None else inp_par
headarr = scifile
#print(self.get_meta_value(headarr, 'decker'))
#print(headarr[0])
# Turn PCA off for long slits
# if ('center' in self.get_meta_value(headarr, 'decker')) or \
# ('Center' in self.get_meta_value(headarr, 'decker')) or \
# ('red' in self.get_meta_value(headarr, 'decker')) or \
# ('Red' in self.get_meta_value(headarr, 'decker')) or \
# ('blue' in self.get_meta_value(headarr, 'decker')) or \
# ('Blue' in self.get_meta_value(headarr, 'decker')) :
# decker is None when this is called on a reduced spec1d/spec2d
decker = self.get_meta_value(headarr, 'decker')
if decker is not None and ('longslit' in decker or 'center' in decker):
par['calibrations']['slitedges']['sync_predict'] = 'nearest'
# Turn on the use of mask design
else:
pass
# Templates
if self.get_meta_value(headarr, 'dispname') == 'VPH-Red':
#par['calibrations']['wavelengths']['method'] = 'full_template'
#par['calibrations']['wavelengths']['reid_arxiv'] = 'keck_deimos_600.fits'
par['calibrations']['wavelengths']['lamps'] = ['OH_MODS']
par['calibrations']['wavelengths']['method'] = 'full_template'
par['calibrations']['wavelengths']['reid_arxiv'] = 'magellan_ldss3_vphred.fits'
elif self.get_meta_value(headarr, 'dispname') == 'VPH-All':
par['calibrations']['wavelengths']['lamps'] = ['HeNeAr']
par['calibrations']['wavelengths']['method'] = 'full_template'
par['calibrations']['wavelengths']['reid_arxiv'] = 'magellan_ldss3_VPH-ALL_7100.fits'
else:
par['calibrations']['wavelengths']['lamps'] = ['HeI', 'NeI', 'ArI', 'ArII']
# FWHM
binning = parse.parse_binning(self.get_meta_value(headarr, 'binning'))
par['calibrations']['wavelengths']['fwhm'] = 6.0 / binning[1]
# Return
return par
[docs]
def configuration_keys(self):
return ['dispname']
[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 == 'science':
return good_exp & (fitstbl['idname'] != 'HeNeAr') & (fitstbl['idname'] != 'FlatQH') & (fitstbl['exptime'] > 100.0)#& (fitstbl['amp'] == '1')
if ftype == 'standard':
return good_exp & (fitstbl['idname'] != 'HeNeAr') & (fitstbl['idname'] != 'FlatQH') & (fitstbl['exptime'] < 100.0)
if ftype in ['arc', 'tilt']:
arc1 = good_exp & (fitstbl['idname'] == 'HeNeAr')
return arc1
if ftype in ['pixelflat', 'trace', 'illumflat']:
return good_exp & (fitstbl['idname'] == 'FlatQH') #& (fitstbl['amp'] == '1')
log.warning('Cannot determine if frames are of type {0}.'.format(ftype))
return np.zeros(len(fitstbl), dtype=bool)
[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.
"""
raw_file2 = raw_file.replace('c1.fits','c2.fits')
# Check for file; allow for extra .gz, etc. suffix
fil = glob.glob(raw_file + '*')
if len(fil) != 1:
raise PypeItError("Found {:d} files matching {:s}".format(len(fil), raw_file))
# Check for file; allow for extra .gz, etc. suffix
fil2 = glob.glob(raw_file2 + '*')
if len(fil2) != 1:
log.warning("Found {:d} files matching {:s}".format(len(fil2), raw_file))
log.warning("Proceeding without the second amplifier")
have_file2 = False
else:
have_file2 = True
# detector par
hdu = io.fits_open(fil[0])
detector_par = self.get_detector_par(det if det is None else 1, hdu=hdu)
data1, overscan1, datasec1, biassec1, x1_1, x2_1, nxb1 = ldss3_read_amp(fil[0])
if have_file2:
data2, overscan2, datasec2, biassec2, x1_2, x2_2, nxb2 = ldss3_read_amp(fil2[0])
else:
nxb2 = nxb1
x2_2 = x2_1
nx, ny = x2_1 + x2_2 + nxb1 + nxb2, data1.shape[1]
# allocate output array...
array = np.zeros((nx, ny))
rawdatasec_img = np.zeros_like(array, dtype=int)
oscansec_img = np.zeros_like(array, dtype=int)
## For amplifier 1
array[:nxb1,:] = overscan1
array[nxb1:x2_1+nxb1,:] = data1
rawdatasec_img[nxb1:x2_1+nxb1, :] = 1
oscansec_img[:nxb1,:] = 1 # exclude the first pixel since it always has problem
## For amplifier 2
if have_file2:
array[x2_1+nxb1+x2_2:x2_1+nxb1+x2_2+nxb2,:] = np.flipud(overscan2)
array[x2_1+nxb1:x2_1+nxb1+x2_2,:] = np.flipud(data2)
rawdatasec_img[x2_1+nxb1:x2_1+nxb1+x2_2, :] = 2
oscansec_img[x2_1+nxb1+x2_2:x2_1+nxb1+x2_2+nxb2,:] = 2 # exclude the first pixel since it always has problem
# Transpose now (helps with debuggin)
array = array.T
rawdatasec_img = rawdatasec_img.T
oscansec_img = oscansec_img.T
# 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,array, hdu, exptime, rawdatasec_img, oscansec_img
[docs]
def ldss3_read_amp(fil:str):
""" Read a single amp of LDSS3 data
Args:
fil (str): filename
Returns:
tuple: data, overscan, datasec, biassec, x1, x2, nxb
"""
log.info("Reading LDSS3 file: {:s}".format(fil))
hdu = io.fits_open(fil)
head1 = hdu[0].header
# ToDo: Need to check binned data
# get the x and y binning factors...
binning = head1['BINNING']
xbin, ybin = [int(ibin) for ibin in binning.split('x')]
# 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()
biassec = head1['BIASSEC']
b1, b2, b3, b4 = np.array(parse.load_sections(biassec, fmt_iraf=False)).flatten()
nxb = b2 - b1 + 1
# determine the output array size...
nx = (x2 - x1 + 1) + nxb
ny = y2 - y1 + 1
# allocate output array...
array = hdu[0].data.T[:, :ny] * 1.0
data = array[:nx-nxb,:]
overscan = array[nx-nxb:,:]
return data, overscan, datasec, biassec, x1, x2, nxb