diff --git a/CHANGES.rst b/CHANGES.rst index e3e9aa7f19..1874c21b4c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ 1.13.1dev (6 June 2023) ------------------------ +- Add support for Gemini/GNIRS (IFU) +- Added a script to convert a wavelength solution into something that can be placed in the reid archive. +- Hotfix for GTC/OSIRIS lamp list +- Hotfix for Arc1D stats annotations on the QA - Hotfix for metadata (correctly set config_independent frames when multiple configurations are being setup) - Hotfix for rebin (speed-up and conserves flux) - Hotfix for skysub regions GUI that used np.bool diff --git a/doc/include/spectrographs_table.rst b/doc/include/spectrographs_table.rst index abb5d645f0..82de9081ed 100644 --- a/doc/include/spectrographs_table.rst +++ b/doc/include/spectrographs_table.rst @@ -8,8 +8,9 @@ gemini_gmos_north_e2v :class:`~pypeit.spectrographs.gemini_gmos.GeminiGMOSNE gemini_gmos_north_ham :class:`~pypeit.spectrographs.gemini_gmos.GeminiGMOSNHamSpectrograph` GEMINI-N GMOS-N `Link `__ MultiSlit True False Hamamatsu detector (R400, B600, R831); Used since Feb 2017; see :doc:`gemini_gmos` gemini_gmos_north_ham_ns :class:`~pypeit.spectrographs.gemini_gmos.GeminiGMOSNHamNSSpectrograph` GEMINI-N GMOS-N `Link `__ MultiSlit True False Same as gemini_gmos_north_ham when used in nod-and-shuffle mode; see :doc:`gemini_gmos` gemini_gmos_south_ham :class:`~pypeit.spectrographs.gemini_gmos.GeminiGMOSSHamSpectrograph` GEMINI-S GMOS-S `Link `__ MultiSlit True False Hamamatsu detector (R400, B600, R831); see :doc:`gemini_gmos` -gemini_gnirs :class:`~pypeit.spectrographs.gemini_gnirs.GeminiGNIRSSpectrograph` GEMINI-N GNIRS `Link `__ Echelle True False -gtc_maat :class:`~pypeit.spectrographs.gtc_osiris.GTCMAATSpectrograph` GTC OSIRIS `Link `__ IFU True False See :doc:`gtc_osiris` +gemini_gnirs_echelle :class:`~pypeit.spectrographs.gemini_gnirs.GeminiGNIRSEchelleSpectrograph` GEMINI-N GNIRS `Link `__ Echelle True False +gemini_gnirs_ifu :class:`~pypeit.spectrographs.gemini_gnirs.GNIRSIFUSpectrograph` GEMINI-N GNIRS `Link `__ IFU True False Support for LR-IFU and HR-IFU mode. Further testing still required... +gtc_maat :class:`~pypeit.spectrographs.gtc_osiris.GTCMAATSpectrograph` GTC OSIRIS `Link `__ IFU True False See :doc:`gtc_osiris` gtc_osiris :class:`~pypeit.spectrographs.gtc_osiris.GTCOSIRISSpectrograph` GTC OSIRIS `Link `__ MultiSlit True False See :doc:`gtc_osiris` gtc_osiris_plus :class:`~pypeit.spectrographs.gtc_osiris.GTCOSIRISPlusSpectrograph` GTC OSIRIS `Link `__ MultiSlit True False See :doc:`gtc_osiris` jwst_nircam :class:`~pypeit.spectrographs.jwst_nircam.JWSTNIRCamSpectrograph` JWST NIRCAM `Link `__ MultiSlit False False diff --git a/pypeit/calibrations.py b/pypeit/calibrations.py index 3e958b479b..bb836fb4e4 100644 --- a/pypeit/calibrations.py +++ b/pypeit/calibrations.py @@ -730,7 +730,7 @@ def get_slits(self): self.slits = frame['class'].from_file(cal_file) self.slits.mask = self.slits.mask_init.copy() if self.user_slits is not None: - self.slits.user_mask(detname, self.user_slits) + self.slits.user_mask(detname, self.user_slits) return self.slits # Slits don't exist or we're not resusing them. See if the Edges @@ -744,7 +744,7 @@ def get_slits(self): # Write the slits calibration file self.slits.to_file() if self.user_slits is not None: - self.slits.user_mask(detname, self.user_slits) + self.slits.user_mask(detname, self.user_slits) return self.slits # Need to build everything from scratch. Start with the trace image. @@ -791,7 +791,7 @@ def get_slits(self): edges = None self.slits.to_file() if self.user_slits is not None: - self.slits.user_mask(detname, self.user_slits) + self.slits.user_mask(detname, self.user_slits) return self.slits def get_wv_calib(self): @@ -1039,7 +1039,7 @@ def get_association(fitstbl, spectrograph, caldir, setup, calib_ID, det, must_ex 'dark': [buildimage.DarkImage], 'pixelflat': [flatfield.FlatImages], 'illumflat': [flatfield.FlatImages], - 'lampoffflats': [flatfield.FlatImages], + 'lampoffflats': [flatfield.FlatImages], 'trace': [edgetrace.EdgeTraceSet, slittrace.SlitTraceSet], 'tilt': [buildimage.TiltImage, wavetilts.WaveTilts] } diff --git a/pypeit/core/datacube.py b/pypeit/core/datacube.py index c3726d506e..4531d1e8c9 100644 --- a/pypeit/core/datacube.py +++ b/pypeit/core/datacube.py @@ -1109,11 +1109,11 @@ def generate_image_subpixel(image_wcs, all_ra, all_dec, all_wave, all_sci, all_i Args: image_wcs (`astropy.wcs.wcs.WCS`_): World coordinate system to use for the white light images. - all_ra (`numpy.ndarray`_) + all_ra (`numpy.ndarray`_): 1D flattened array containing the right ascension of each pixel (units = degrees) - all_dec (`numpy.ndarray`_) + all_dec (`numpy.ndarray`_): 1D flattened array containing the declination of each pixel (units = degrees) - all_wave (`numpy.ndarray`_) + all_wave (`numpy.ndarray`_): 1D flattened array containing the wavelength of each pixel (units = Angstroms) all_sci (`numpy.ndarray`_): 1D flattened array containing the counts of each pixel from all spec2d files @@ -1121,22 +1121,22 @@ def generate_image_subpixel(image_wcs, all_ra, all_dec, all_wave, all_sci, all_i 1D flattened array containing the inverse variance of each pixel from all spec2d files all_wghts (`numpy.ndarray`_): 1D flattened array containing the weights of each pixel to be used in the combination - all_spatpos (`numpy.ndarray`_) + all_spatpos (`numpy.ndarray`_): 1D flattened array containing the detector pixel location in the spatial direction - all_specpos (`numpy.ndarray`_) + all_specpos (`numpy.ndarray`_): 1D flattened array containing the detector pixel location in the spectral direction - all_spatid (`numpy.ndarray`_) + all_spatid (`numpy.ndarray`_): 1D flattened array containing the spatid of each pixel - tilts (`numpy.ndarray`_, list) + tilts (`numpy.ndarray`_, list): 2D wavelength tilts frame, or a list of tilt frames (see all_idx) - slits (:class:`pypeit.slittrace.SlitTraceSet`_, list) + slits (:class:`pypeit.slittrace.SlitTraceSet`, list): Information stored about the slits, or a list of SlitTraceSet (see all_idx) - astrom_trans (:class:`pypeit.alignframe.AlignmentSplines`_, list): + astrom_trans (:class:`pypeit.alignframe.AlignmentSplines`, list): A Class containing the transformation between detector pixel coordinates and WCS pixel coordinates, or a list of Alignment Splines (see all_idx) bins (tuple): A 3-tuple (x,y,z) containing the histogram bin edges in x,y spatial and z wavelength coordinates - all_idx (`numpy.ndarray`_, optional) + all_idx (`numpy.ndarray`_, optional): If tilts, slits, and astrom_trans are lists, this should contain a 1D flattened array, of the same length as all_sci, containing the index the tilts, slits, and astrom_trans lists that corresponds to each pixel. Note that, in this case all of these lists need to be the same length. @@ -1230,7 +1230,7 @@ def generate_cube_subpixel(outfile, output_wcs, all_ra, all_dec, all_wave, all_s and WCS pixel coordinates, or a list of Alignment Splines (see all_idx) bins (tuple): A 3-tuple (x,y,z) containing the histogram bin edges in x,y spatial and z wavelength coordinates - all_idx (`numpy.ndarray`_, optional) + all_idx (`numpy.ndarray`_, optional): If tilts, slits, and astrom_trans are lists, this should contain a 1D flattened array, of the same length as all_sci, containing the index the tilts, slits, and astrom_trans lists that corresponds to each pixel. Note that, in this case all of these lists need to be the same length. diff --git a/pypeit/core/wavecal/autoid.py b/pypeit/core/wavecal/autoid.py index 5dce82c7b9..8a648ead46 100644 --- a/pypeit/core/wavecal/autoid.py +++ b/pypeit/core/wavecal/autoid.py @@ -176,8 +176,8 @@ def arc_fit_qa(waveFit, outfile=None, ids_only=False, title=None, # Stats wave_soln_fit = waveFit.pypeitfit.eval(waveFit.pixel_fit/waveFit.xnorm)#, 'legendre',minx=fit['fmin'], maxx=fit['fmax']) - ax_fit.text(0.1*len(arc_spec), 0.90*ymin+(ymax-ymin),r'$\Delta\lambda$={:.3f}$\AA$ (per pix)'.format(waveFit.cen_disp), size='small') - ax_fit.text(0.1*len(arc_spec), 0.80*ymin+(ymax-ymin),'RMS={:.3f} (pixels)'.format(waveFit.rms), size='small') + ax_fit.text(0.1, 0.9, r'$\Delta\lambda$={:.3f}$\AA$ (per pix)'.format(waveFit.cen_disp), size='small', transform=ax_fit.transAxes) + ax_fit.text(0.1, 0.8, 'RMS={:.3f} (pixels)'.format(waveFit.rms), size='small', transform=ax_fit.transAxes) # Arc Residuals ax_res = plt.subplot(gs[1,1]) res = waveFit.wave_fit-wave_soln_fit @@ -2763,14 +2763,14 @@ def report_prelim(self, slit, best_patt_dict, best_final_fit): # Report on the best preliminary result if best_final_fit is None: msgs.warn('---------------------------------------------------' + msgs.newline() + - 'Preliminary report for slit {0:d}/{1:d}:'.format(slit, self._nslit-1) + msgs.newline() + + 'Preliminary report for slit {0:d}/{1:d}:'.format(slit+1, self._nslit) + msgs.newline() + ' No matches! Attempting to cross match.' + msgs.newline() + '---------------------------------------------------') self._all_patt_dict[str(slit)] = None self._all_final_fit[str(slit)] = None elif best_final_fit['rms'] > wvutils.parse_param(self._par, 'rms_threshold', slit): msgs.warn('---------------------------------------------------' + msgs.newline() + - 'Preliminary report for slit {0:d}/{1:d}:'.format(slit, self._nslit-1) + msgs.newline() + + 'Preliminary report for slit {0:d}/{1:d}:'.format(slit+1, self._nslit) + msgs.newline() + ' Poor RMS ({0:.3f})! Attempting to cross match.'.format(best_final_fit['rms']) + msgs.newline() + '---------------------------------------------------') self._all_patt_dict[str(slit)] = None @@ -2783,7 +2783,7 @@ def report_prelim(self, slit, best_patt_dict, best_final_fit): signtxt = 'anitcorrelate' # Report msgs.info('---------------------------------------------------' + msgs.newline() + - 'Preliminary report for slit {0:d}/{1:d}:'.format(slit, self._nslit-1) + msgs.newline() + + 'Preliminary report for slit {0:d}/{1:d}:'.format(slit+1, self._nslit) + msgs.newline() + ' Pixels {:s} with wavelength'.format(signtxt) + msgs.newline() + ' Number of weak lines = {:d}'.format(self._det_weak[str(slit)][0].size) + msgs.newline() + ' Number of strong lines = {:d}'.format(self._det_stro[str(slit)][0].size) + msgs.newline() + @@ -2803,7 +2803,7 @@ def report_final(self): for slit in range(self._nslit): # Prepare a message for bad wavelength solutions badmsg = '---------------------------------------------------' + msgs.newline() +\ - 'Final report for slit {0:d}/{1:d}:'.format(slit, self._nslit-1) + msgs.newline() +\ + 'Final report for slit {0:d}/{1:d}:'.format(slit+1, self._nslit) + msgs.newline() +\ ' Wavelength calibration not performed!' if slit not in self._ok_mask: msgs.warn(badmsg) @@ -2822,7 +2822,7 @@ def report_final(self): centdisp = abs(centwave-tempwave) msgs.info(msgs.newline() + '---------------------------------------------------' + msgs.newline() + - 'Final report for slit {0:d}/{1:d}:'.format(slit, self._nslit-1) + msgs.newline() + + 'Final report for slit {0:d}/{1:d}:'.format(slit+1, self._nslit) + msgs.newline() + ' Pixels {:s} with wavelength'.format(signtxt) + msgs.newline() + ' Number of weak lines = {:d}'.format(self._det_weak[str(slit)][0].size) + msgs.newline() + ' Number of strong lines = {:d}'.format(self._det_stro[str(slit)][0].size) + msgs.newline() + diff --git a/pypeit/data/arc_lines/reid_arxiv/gemini_gnirs_lrifu_H.fits b/pypeit/data/arc_lines/reid_arxiv/gemini_gnirs_lrifu_H.fits new file mode 100644 index 0000000000..c347b9ca14 Binary files /dev/null and b/pypeit/data/arc_lines/reid_arxiv/gemini_gnirs_lrifu_H.fits differ diff --git a/pypeit/data/arc_lines/reid_arxiv/gemini_gnirs_lrifu_K.fits b/pypeit/data/arc_lines/reid_arxiv/gemini_gnirs_lrifu_K.fits new file mode 100644 index 0000000000..e2ed76cf88 Binary files /dev/null and b/pypeit/data/arc_lines/reid_arxiv/gemini_gnirs_lrifu_K.fits differ diff --git a/pypeit/find_objects.py b/pypeit/find_objects.py index a3a6e9c1d7..3bb531a651 100644 --- a/pypeit/find_objects.py +++ b/pypeit/find_objects.py @@ -1184,10 +1184,12 @@ def joint_skysub(self, skymask=None, update_crmask=True, trim_edg=(0,0), counts = global_sky _scale = None if self.sciImg.img_scale is None else self.sciImg.img_scale[thismask] # NOTE: darkcurr must be a float for the call below to work. - var = procimg.variance_model(self.sciImg.base_var[thismask], counts=counts[thismask], - count_scale=_scale, noise_floor=adderr) - model_ivar[thismask] = utils.inverse(var) - + if not self.bkg_redux: + var = procimg.variance_model(self.sciImg.base_var[thismask], counts=counts[thismask], + count_scale=_scale, noise_floor=adderr) + model_ivar[thismask] = utils.inverse(var) + else: + model_ivar[thismask] = self.sciImg.ivar[thismask] # RJC :: Recalculating the global sky and flexure is probably overkill... but please keep this code in for now # Recalculate the sky on each individual slit and redetermine the spectral flexure # global_sky_sep = super().global_skysub(skymask=skymask, update_crmask=update_crmask, @@ -1227,7 +1229,8 @@ def global_skysub(self, skymask=None, update_crmask=True, trim_edg=(0,0), global_sky_sep = super().global_skysub(skymask=skymask, update_crmask=update_crmask, trim_edg=trim_edg, show_fit=show_fit, show=show, show_objs=show_objs) - if np.any(global_sky_sep[skymask] == 0): + # Check if any slits failed + if np.any(global_sky_sep[self.slitmask>=0] == 0) and not self.bkg_redux: # Cannot continue without a sky model for all slits msgs.error("Global sky subtraction has failed for at least one slit.") @@ -1291,7 +1294,10 @@ def calculate_flexure(self, global_sky): mxshft=self.par['flexure']['spec_maxshift'], excess_shft=self.par['flexure']['excessive_shift'], method="slitcen") - this_slitshift = np.ones(self.slits.nslits) * flex_dict_ref['shift'] + this_slitshift = np.zeros(self.slits.nslits) + if flex_dict_ref is not None: + msgs.warn("Only a relative spectral flexure correction will be performed") + this_slitshift = np.ones(self.slits.nslits) * flex_dict_ref['shift'] # Now loop through all slits to calculate the additional shift relative to the reference slit flex_list = [] for slit_idx, slit_spat in enumerate(self.slits.spat_id): diff --git a/pypeit/scripts/arxiv_solution.py b/pypeit/scripts/arxiv_solution.py new file mode 100644 index 0000000000..d0e47f31a2 --- /dev/null +++ b/pypeit/scripts/arxiv_solution.py @@ -0,0 +1,58 @@ +""" +This script enables the user to convert a MasterWaveCalib wavelength solution fits file +into a PypeIt arxiv solution that can be used with the full_template method. + +.. include common links, assuming primary doc root is up one directory +.. include:: ../include/links.rst +""" +import time +from pypeit import msgs +from pypeit import par +from pypeit import inputfiles +from pypeit import utils +from pypeit.scripts import scriptbase + + +class ArxivSolution(scriptbase.ScriptBase): + + @classmethod + def get_parser(cls, width=None): + parser = super().get_parser(description='Read in a MasterWaveCalib solution and convert it into the ' + 'format required for the PypeIt full template archive', width=width) + parser.add_argument('file', type = str, default=None, help='MasterWaveCalib file') + parser.add_argument('binning', type=int, help="Spectral binning") + parser.add_argument('-s', '--slit', default=0, type=int, help='Slit number to use') + parser.add_argument('-v', '--verbosity', type=int, default=1, + help='Verbosity level between 0 [none] and 2 [all]. Default: 1. ' + 'Level 2 writes a log with filename make_arxiv_solution_YYYYMMDD-HHMM.log') + return parser + + @staticmethod + def main(args): + import os + from pypeit.wavecalib import WaveCalib + from pypeit.core.wavecal import wvutils + + # Set the verbosity, and create a logfile if verbosity == 2 + msgs.set_logfile_and_verbosity('arxiv_solution', args.verbosity) + + # Check that a file has been provided + if args.file is None: + msgs.error('You must input a MasterWaveCalib file') + elif not os.path.exists(args.file): + msgs.error("The following MasterWaveCalib file does not exist:" + msgs.newline() + args.file) + + # Load the wavelength calibration file + wv_calib = WaveCalib.from_file(args.file) + wave = wv_calib['wv_fits'][args.slit]['wave_soln'].flatten() + spec = wv_calib['wv_fits'][args.slit]['spec'].flatten() + outname = args.file.replace(".fits", "_arXiv.fits") + wvutils.write_template(wave, spec, args.binning, './', outname) + print("") # Empty line for clarity + msgs.info("To include the newly generated solution in the PypeIt archive," + msgs.newline() + + "move (and appropriately rename) the following file: " + msgs.newline() + + outname + msgs.newline() + + "to the following directory:" + msgs.newline() + + "pypeit/data/arc_lines/reid_arxiv/") + print("") # Empty line for clarity + msgs.info("Please also consider sharing your solution with the PypeIt Developers.") diff --git a/pypeit/spectrographs/gemini_gnirs.py b/pypeit/spectrographs/gemini_gnirs.py index 96ce1c0a4a..8125f2ab1a 100644 --- a/pypeit/spectrographs/gemini_gnirs.py +++ b/pypeit/spectrographs/gemini_gnirs.py @@ -4,10 +4,13 @@ .. include:: ../include/links.rst """ import numpy as np +from astropy import wcs, units +from astropy.coordinates import SkyCoord, EarthLocation +from astropy.time import Time from pypeit import msgs from pypeit import telescopes -from pypeit.core import framematch +from pypeit.core import framematch, parse from pypeit.images import detector_container from pypeit.spectrographs import spectrograph @@ -17,14 +20,16 @@ class GeminiGNIRSSpectrograph(spectrograph.Spectrograph): Child to handle Gemini/GNIRS specific code """ ndet = 1 - name = 'gemini_gnirs' camera = 'GNIRS' url = 'https://www.gemini.edu/instrumentation/gnirs' header_name = 'GNIRS' telescope = telescopes.GeminiNTelescopePar() - pypeline = 'Echelle' - ech_fixed_format = True - supported = True + + def __init__(self): + super().__init__() + + # TODO :: Might consider changing TelescopePar to use the astropy EarthLocation. + self.location = EarthLocation.of_site('Gemini North') def get_detector_par(self, det, hdu=None): """ @@ -62,154 +67,6 @@ def get_detector_par(self, det, hdu=None): ) 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() - - # 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) - - # Flats - par['calibrations']['flatfield']['tweak_slits_thresh'] = 0.90 - par['calibrations']['flatfield']['tweak_slits_maxfrac'] = 0.10 - - # Relatively short slit, so keep the spatial tilt order low - par['calibrations']['tilts']['spat_order'] = 1 - - # Reduce parameters - #par['reduce']['findobj']['snr_thresh'] = 5.0 # Object finding threshold - par['reduce']['findobj']['find_trim_edge'] = [2,2] # Slit is too short to trim 5,5 especially - par['reduce']['skysub']['bspline_spacing'] = 0.8 - par['reduce']['skysub']['global_sky_std'] = False # Do not perform global sky subtraction for standard stars - # TODO: JFH: Is this the correct behavior? (Is why we have sky-subtraction problems for GNIRS?) - par['reduce']['skysub']['no_poly'] = True # Do not use polynomial degree of freedom for global skysub - par['reduce']['extraction']['model_full_slit'] = True # local sky subtraction operates on entire slit - par['reduce']['findobj']['maxnumber_sci'] = 2 # Slit is narrow so allow one object per order - par['reduce']['findobj']['maxnumber_std'] = 1 # Slit is narrow so allow one object per order - # Standards - par['calibrations']['standardframe']['process']['mask_cr'] = False # Do not mask_cr standards - - # Do not correct for flexure - par['flexure']['spec_method'] = 'skip' - - # Set the default exposure time ranges for the frame typing - par['calibrations']['pixelflatframe']['exprng'] = [None, 30] - par['calibrations']['traceframe']['exprng'] = [None, 30] - par['calibrations']['standardframe']['exprng'] = [None, 30] - par['scienceframe']['exprng'] = [30, None] - - # Sensitivity function parameters - par['sensfunc']['algorithm'] = 'IR' - par['sensfunc']['polyorder'] = 6 - par['sensfunc']['IR']['telgridfile'] = 'TelFit_MaunaKea_3100_26100_R20000.fits' - return par - - 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) - - # TODO This is a hack for now until we figure out how to set dispname - # and other meta information in the spectrograph class itself - self.dispname = self.get_meta_value(scifile, 'dispname') - # 32/mmSB_G5533 setup, covering XYJHK with short blue camera - if '32/mm' in self.dispname: - # Edges - par['calibrations']['slitedges']['edge_thresh'] = 20. - par['calibrations']['slitedges']['trace_thresh'] = 10. - par['calibrations']['slitedges']['fit_order'] = 5 - par['calibrations']['slitedges']['max_shift_adj'] = 0.5 - par['calibrations']['slitedges']['fit_min_spec_length'] = 0.5 - par['calibrations']['slitedges']['left_right_pca'] = True - par['calibrations']['slitedges']['pca_order'] = 3 - - # Wavelengths - par['calibrations']['wavelengths']['rms_threshold'] = 1.0 # Might be grating dependent.. - par['calibrations']['wavelengths']['sigdetect'] = [4.0, 5.0, 5.0, 5.0, 5.0, 5.0] #5.0 - par['calibrations']['wavelengths']['lamps'] = ['OH_GNIRS'] - #par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] - par['calibrations']['wavelengths']['n_first'] = 2 - par['calibrations']['wavelengths']['n_final'] = [1, 3, 3, 3, 3, 3] - - # Reidentification parameters - par['calibrations']['wavelengths']['method'] = 'reidentify' - par['calibrations']['wavelengths']['cc_thresh'] = 0.6 - par['calibrations']['wavelengths']['reid_arxiv'] = 'gemini_gnirs.fits' -# par['calibrations']['wavelengths']['ech_fix_format'] = True - # Echelle parameters - # JFH This is provisional these IDs should be checked. - par['calibrations']['wavelengths']['echelle'] = True - par['calibrations']['wavelengths']['ech_nspec_coeff'] = 3 - par['calibrations']['wavelengths']['ech_norder_coeff'] = 4 - par['calibrations']['wavelengths']['ech_sigrej'] = 3.0 - - # Tilts - par['calibrations']['tilts']['tracethresh'] = [5.0, 10, 10, 10, 10, 10] - par['calibrations']['tilts']['sig_neigh'] = 5.0 - par['calibrations']['tilts']['nfwhm_neigh'] = 2.0 - - # 10/mmLBSX_G5532 setup, covering YJHK with the long blue camera and SXD prism - elif '10/mmLBSX' in self.dispname: - # Edges - par['calibrations']['slitedges']['edge_thresh'] = 20. - par['calibrations']['slitedges']['trace_thresh'] = 10. - par['calibrations']['slitedges']['fit_order'] = 2 - par['calibrations']['slitedges']['max_shift_adj'] = 0.5 - par['calibrations']['slitedges']['det_min_spec_length'] = 0.20 - par['calibrations']['slitedges']['fit_min_spec_length'] = 0.20 - par['calibrations']['slitedges']['left_right_pca'] = True # Actually we need a parameter to disable PCA entirely - par['calibrations']['slitedges']['sync_predict'] = 'nearest' - - # Wavelengths - par['calibrations']['wavelengths']['rms_threshold'] = 1.0 # Might be grating dependent.. - par['calibrations']['wavelengths']['sigdetect'] = 5.0 - par['calibrations']['wavelengths']['lamps'] = ['Ar_IR_GNIRS'] - #par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] - par['calibrations']['wavelengths']['n_first'] = 2 - par['calibrations']['wavelengths']['n_final'] = [3, 3, 3, 3] - # Reidentification parameters - par['calibrations']['wavelengths']['method'] = 'reidentify' - par['calibrations']['wavelengths']['cc_thresh'] = 0.6 - par['calibrations']['wavelengths']['reid_arxiv'] = 'gemini_gnirs_10mm_LBSX.fits' -# par['calibrations']['wavelengths']['ech_fix_format'] = True - # Echelle parameters - par['calibrations']['wavelengths']['echelle'] = True - par['calibrations']['wavelengths']['ech_nspec_coeff'] = 3 - par['calibrations']['wavelengths']['ech_norder_coeff'] = 3 - par['calibrations']['wavelengths']['ech_sigrej'] = 3.0 - - # Tilts - par['calibrations']['tilts']['tracethresh'] = [10, 10, 10, 10] - par['calibrations']['tilts']['sig_neigh'] = 5.0 - par['calibrations']['tilts']['nfwhm_neigh'] = 2.0 - else: - msgs.error('Unrecognized GNIRS dispname') - - return par - def init_meta(self): """ Define how metadata are derived from the spectrograph files. @@ -234,6 +91,8 @@ def init_meta(self): self.meta['dithoff'] = dict(card=None, compound=True) # Extras for config and frametyping + self.meta['filter1'] = dict(ext=0, card='FILTER2') + self.meta['slitwid'] = dict(ext=0, compound=True, card=None) self.meta['dispname'] = dict(ext=0, card='GRATING') self.meta['hatch'] = dict(ext=0, card='COVER') self.meta['dispangle'] = dict(ext=0, card='GRATTILT', rtol=1e-4) @@ -259,6 +118,39 @@ def compound_meta(self, headarr, meta_key): return headarr[0].get('QOFFSET') else: return 0.0 + elif meta_key == 'slitwid': + deckname = headarr[0].get('DECKER') + if 'LR-IFU' in deckname: + return 0.15/3600.0 # divide by 3600 for degrees + elif 'HR-IFU' in deckname: + return 0.05/3600.0 # divide by 3600 for degrees + else: + # TODO :: Need to provide a more complete set of options here + return None + elif meta_key == 'obstime': + try: + return Time(headarr[0]['DATE-OBS'] + "T" + headarr[0]['TIME-OBS']) + except KeyError: + msgs.warn("Time of observation is not in header") + return 0.0 + elif meta_key == 'pressure': + try: + return headarr[0]['PRESSURE'] * 0.001 # Must be in astropy.units.bar + except KeyError: + msgs.warn("Pressure is not in header") + return 0.0 + elif meta_key == 'temperature': + try: + return headarr[0]['TAMBIENT'] # Must be in astropy.units.deg_C + except KeyError: + msgs.warn("Temperature is not in header") + return 0.0 + elif meta_key == 'humidity': + try: + return headarr[0]['HUMIDITY'] + except KeyError: + msgs.warn("Humidity is not in header") + return 0.0 else: msgs.error("Not ready for this compound meta") @@ -300,7 +192,7 @@ def raw_header_cards(self): def pypeit_file_keys(self): """ - Define the list of keys to be output into a standard PypeIt file. + Define the list of keys to be output into a standard ``PypeIt`` file. Returns: :obj:`list`: The list of keywords in the relevant @@ -349,6 +241,246 @@ def check_frame_type(self, ftype, fitstbl, exprng=None): msgs.warn('Cannot determine if frames are of type {0}.'.format(ftype)) return np.zeros(len(fitstbl), dtype=bool) + @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) + + # Flats + par['calibrations']['flatfield']['tweak_slits_thresh'] = 0.90 + par['calibrations']['flatfield']['tweak_slits_maxfrac'] = 0.10 + + # Relatively short slit, so keep the spatial tilt order low + par['calibrations']['tilts']['spat_order'] = 1 + + # Reduce parameters + # par['reduce']['findobj']['snr_thresh'] = 5.0 # Object finding threshold + par['reduce']['findobj']['find_trim_edge'] = [2, 2] # Slit is too short to trim 5,5 especially + par['reduce']['skysub']['bspline_spacing'] = 0.8 + par['reduce']['skysub']['global_sky_std'] = False # Do not perform global sky subtraction for standard stars + par['reduce']['skysub']['no_poly'] = True # Do not use polynomial degree of freedom for global skysub + par['reduce']['extraction']['model_full_slit'] = True # local sky subtraction operates on entire slit + par['reduce']['findobj']['maxnumber_sci'] = 2 # Slit is narrow so allow two objects per order + par['reduce']['findobj']['maxnumber_std'] = 1 # Slit is narrow so allow one object per order + # Standards + par['calibrations']['standardframe']['process']['mask_cr'] = False # Do not mask_cr standards + + # Do not correct for flexure + par['flexure']['spec_method'] = 'skip' + + # Set the default exposure time ranges for the frame typing + par['calibrations']['pixelflatframe']['exprng'] = [None, 30] + par['calibrations']['traceframe']['exprng'] = [None, 30] + par['calibrations']['standardframe']['exprng'] = [None, 30] + par['scienceframe']['exprng'] = [30, None] + + # Sensitivity function parameters + par['sensfunc']['algorithm'] = 'IR' + par['sensfunc']['polyorder'] = 6 + par['sensfunc']['IR']['telgridfile'] = 'TelFit_MaunaKea_3100_26100_R20000.fits' + return par + + 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) + # TODO This is a hack for now until we figure out how to set dispname + # and other meta information in the spectrograph class itself + self.dispname = self.get_meta_value(scifile, 'dispname') + # 32/mmSB_G5533 setup, covering XYJHK with short blue camera + if '32/mm' in self.dispname: + # Edges + par['calibrations']['slitedges']['edge_thresh'] = 20. + par['calibrations']['slitedges']['trace_thresh'] = 10. + par['calibrations']['slitedges']['fit_order'] = 5 + par['calibrations']['slitedges']['max_shift_adj'] = 0.5 + par['calibrations']['slitedges']['fit_min_spec_length'] = 0.5 + + # Wavelengths + par['calibrations']['wavelengths']['rms_threshold'] = 1.0 # Might be grating dependent.. + par['calibrations']['wavelengths']['sigdetect'] = 5.0 + par['calibrations']['wavelengths']['lamps'] = ['OH_GNIRS'] + # par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] + par['calibrations']['wavelengths']['n_first'] = 2 + par['calibrations']['wavelengths']['n_final'] = 3 + + # Reidentification parameters + par['calibrations']['wavelengths']['method'] = 'reidentify' + par['calibrations']['wavelengths']['cc_thresh'] = 0.6 + par['calibrations']['wavelengths']['reid_arxiv'] = 'gemini_gnirs.fits' + + # Tilts + par['calibrations']['tilts']['tracethresh'] = 10 + par['calibrations']['tilts']['sig_neigh'] = 5.0 + par['calibrations']['tilts']['nfwhm_neigh'] = 2.0 + # 10/mmLBSX_G5532 setup, covering YJHK with the long blue camera and SXD prism + elif '10/mmLBSX' in self.dispname: + # Edges + par['calibrations']['slitedges']['edge_thresh'] = 20. + par['calibrations']['slitedges']['trace_thresh'] = 10. + par['calibrations']['slitedges']['fit_order'] = 2 + par['calibrations']['slitedges']['max_shift_adj'] = 0.5 + par['calibrations']['slitedges']['det_min_spec_length'] = 0.20 + par['calibrations']['slitedges']['fit_min_spec_length'] = 0.20 + par['calibrations']['slitedges']['sync_predict'] = 'nearest' + + # Wavelengths + par['calibrations']['wavelengths']['rms_threshold'] = 1.0 # Might be grating dependent.. + par['calibrations']['wavelengths']['sigdetect'] = 5.0 + par['calibrations']['wavelengths']['lamps'] = ['Ar_IR_GNIRS'] + # par['calibrations']['wavelengths']['nonlinear_counts'] = self.detector[0]['nonlinear'] * self.detector[0]['saturation'] + par['calibrations']['wavelengths']['n_first'] = 2 + par['calibrations']['wavelengths']['n_final'] = 3 + # Reidentification parameters + par['calibrations']['wavelengths']['method'] = 'reidentify' + par['calibrations']['wavelengths']['cc_thresh'] = 0.6 + par['calibrations']['wavelengths']['reid_arxiv'] = 'gemini_gnirs_10mm_LBSX.fits' + + # Tilts + par['calibrations']['tilts']['tracethresh'] = 10 + par['calibrations']['tilts']['sig_neigh'] = 5.0 + par['calibrations']['tilts']['nfwhm_neigh'] = 2.0 + elif '10/mmLBHR_G5532' in self.dispname: + # TODO :: Need to fill this in + pass + else: + msgs.error(f'Unrecognized GNIRS dispname: {self.dispname}') + + return par + + 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. + """ + msgs.info("Custom bad pixel mask for GNIRS") + # Call the base-class method to generate the empty bpm + bpm_img = super().bpm(filename, det, shape=shape, msbias=msbias) + + # JFH Changed. Dealing with detector scratch + if det == 1: + bpm_img[687:765,12:16] = 1. + bpm_img[671:687,8:13] = 1. + # bpm_img[:, 1000:] = 1. + + return bpm_img + + +class GeminiGNIRSEchelleSpectrograph(GeminiGNIRSSpectrograph): + """ + Child to handle Gemini/GNIRS echelle specific code + """ + name = 'gemini_gnirs_echelle' + pypeline = 'Echelle' + ech_fixed_format = True + + 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) + # TODO This is a hack for now until we figure out how to set dispname + # and other meta information in the spectrograph class itself + self.dispname = self.get_meta_value(scifile, 'dispname') + # 32/mmSB_G5533 setup, covering XYJHK with short blue camera + if '32/mm' in self.dispname: + # Edges + par['calibrations']['slitedges']['left_right_pca'] = True + par['calibrations']['slitedges']['pca_order'] = 3 + + # Wavelengths + par['calibrations']['wavelengths']['sigdetect'] = [4.0, 5.0, 5.0, 5.0, 5.0, 5.0] #5.0 + par['calibrations']['wavelengths']['n_final'] = [1, 3, 3, 3, 3, 3] + + # Echelle parameters + # JFH This is provisional these IDs should be checked. + par['calibrations']['wavelengths']['echelle'] = True + par['calibrations']['wavelengths']['ech_nspec_coeff'] = 3 + par['calibrations']['wavelengths']['ech_norder_coeff'] = 5 + par['calibrations']['wavelengths']['ech_sigrej'] = 3.0 + + # Tilts + par['calibrations']['tilts']['tracethresh'] = [5.0, 10, 10, 10, 10, 10] + # 10/mmLBSX_G5532 setup, covering YJHK with the long blue camera and SXD prism + elif '10/mmLBSX' in self.dispname: + # Edges + par['calibrations']['slitedges']['left_right_pca'] = True # Actually we need a parameter to disable PCA entirely + + # Wavelengths + par['calibrations']['wavelengths']['n_final'] = [3, 3, 3, 3] + # Echelle parameters + par['calibrations']['wavelengths']['echelle'] = True + par['calibrations']['wavelengths']['ech_nspec_coeff'] = 3 + par['calibrations']['wavelengths']['ech_norder_coeff'] = 3 + par['calibrations']['wavelengths']['ech_sigrej'] = 3.0 + + # Tilts + par['calibrations']['tilts']['tracethresh'] = [10, 10, 10, 10] + else: + msgs.error('Unrecognized GNIRS dispname') + + return par + def order_platescale(self, order_vec, binning=None): """ Return the platescale for each echelle order. @@ -435,44 +567,247 @@ def spec_min_max(self): else: msgs.error('Unrecognized disperser') - 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. +class GNIRSIFUSpectrograph(GeminiGNIRSSpectrograph): + # TODO :: A list of steps that could improve the reduction + # * Have a high threshold for detecting slit edges (par['calibrations']['slitedges']['edge_thresh'] = 100.), and have an option when inserting new traces to be the median of all other slit lengths (or a fit to the slit lengths). + # * Need to store a wavelength solution for different grating options (Note, the Holy Grail algorithm works pretty well, most of the time) + name = 'gemini_gnirs_ifu' + pypeline = 'IFU' + + def init_meta(self): + super().init_meta() + 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) + + @classmethod + def default_pypeit_par(cls): + par = super().default_pypeit_par() + + # LACosmics parameters + par['scienceframe']['process']['sigclip'] = 4.0 + par['scienceframe']['process']['objlim'] = 1.5 + par['scienceframe']['process']['use_illumflat'] = False # illumflat is applied when building the relative scale image in reduce.py, so should be applied to scienceframe too. + par['scienceframe']['process']['use_specillum'] = False # apply relative spectral illumination + par['scienceframe']['process']['spat_flexure_correct'] = False # don't correct for spatial flexure - varying spatial illumination profile could throw this correction off. Also, there's no way to do astrometric correction if we can't correct for spatial flexure of the contbars frames + par['scienceframe']['process']['use_biasimage'] = False + par['scienceframe']['process']['use_darkimage'] = False + par['calibrations']['flatfield']['slit_illum_finecorr'] = False + # Don't do 1D extraction for 3D data - it's meaningless because the DAR correction must be performed on the 3D data. + par['reduce']['extraction']['skip_extraction'] = True # Because extraction occurs before the DAR correction, don't extract + + #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'] = 2 # Trim the slit edges + par['calibrations']['slitedges']['pad'] = 2 # Need to pad out the tilts for the astrometric transform when creating a datacube. + + # Decrease the wave tilts order, given the shorter slits of the IFU + par['calibrations']['tilts']['spat_order'] = 1 + par['calibrations']['tilts']['spec_order'] = 1 + + # Make sure that this is reduced as a slit (as opposed to fiber) spectrograph + par['reduce']['cube']['slit_spec'] = True + par['reduce']['cube']['grating_corr'] = False + par['reduce']['cube']['combine'] = False # Make separate spec3d files from the input spec2d files + + # Sky subtraction parameters + par['reduce']['skysub']['no_poly'] = True + par['reduce']['skysub']['bspline_spacing'] = 0.6 + par['reduce']['skysub']['joint_fit'] = False + + # Don't correct flexure by default since the OH lines are used for wavelength calibration + # If someone does want to do a spectral flexure correction, you should use slitcen, + # because this is a slit-based IFU where no objects are extracted. + par['flexure']['spec_method'] = 'skip' + par['flexure']['spec_maxshift'] = 0 # The sky lines are used for calibration - don't allow flexure + + # Flux calibration parameters + par['sensfunc']['UVIS']['extinct_correct'] = False # This must be False - the extinction correction is performed when making the datacube + + return par + + def config_specific_par(self, scifile, inp_par=None): + """ + Modify the ``PypeIt`` parameters to hard-wired values used for + specific instrument configurations. 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 + 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: - `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. + :class:`~pypeit.par.parset.ParSet`: The PypeIt parameter set + adjusted for configuration specific parameter values. """ - msgs.info("Custom bad pixel mask for GNIRS") - # Call the base-class method to generate the empty bpm - bpm_img = super().bpm(filename, det, shape=shape, msbias=msbias) + par = super().config_specific_par(scifile, inp_par=inp_par) + # Obtain a header keyword to determine which range is being used + filter = self.get_meta_value(scifile, 'filter1') + par['calibrations']['slitedges']['edge_thresh'] = 30. + # TODO :: The following wavelength solutions are not general enough - need to implement a solution for each setup+grating + # TODO BEFORE PR MERGE :: The full_template solutions below were generated (quickly!) from holy-grail... might want to redo this... + if filter == 'X_G0518': # H band + par['calibrations']['wavelengths']['method'] = 'holy-grail' + elif filter == 'J_G0517': # K band + par['calibrations']['wavelengths']['method'] = 'holy-grail' + elif filter == 'H_G0516': # H band + par['calibrations']['wavelengths']['method'] = 'full_template' + par['calibrations']['wavelengths']['reid_arxiv'] = 'gemini_gnirs_lrifu_H.fits' + elif filter == 'K_G0515': # K band + par['calibrations']['wavelengths']['method'] = 'full_template' + par['calibrations']['wavelengths']['reid_arxiv'] = 'gemini_gnirs_lrifu_K.fits' + else: + par['calibrations']['wavelengths']['method'] = 'holy-grail' - # JFH Changed. Dealing with detector scratch - if det == 1: - bpm_img[687:765,12:16] = 1. - bpm_img[671:687,8:13] = 1. - # bpm_img[:, 1000:] = 1. + return par - return bpm_img + def get_wcs(self, hdr, slits, platescale, wave0, dwv, spatial_scale=None): + """ + 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 + msgs.work("NEED TO WORK OUT SLICER SCALE AND PIXEL SCALE") + slscl = self.get_meta_value([hdr], 'slitwid') + if spatial_scale is not None: + if pxscl > spatial_scale / 3600.0: + msgs.warn("Spatial scale requested ({0:f}'') is less than the pixel scale ({1:f}'')".format(spatial_scale, pxscl*3600.0)) + # Update the pixel scale + pxscl = spatial_scale / 3600.0 # 3600 is to convert arcsec to degrees + + # 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.get_meta_value([hdr], 'ra') + decval = self.get_meta_value([hdr], 'dec') + + # Create a coordinate + coord = SkyCoord(raval, decval, unit=(units.deg, units.deg)) + + # Get rotator position + msgs.warn("CURRENTLY A HACK --- NEED TO FIGURE OUT RPOS and RREF FOR HRIFU FROM HEADER INFO") + 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 + 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 = 11 # i.e. see get_datacube_bins (11 is used as the reference point - somewhere in the middle of the FOV) + crpix2 = slitlength / 2. + crpix3 = 1. + # Get the offset + msgs.warn("HACK FOR HRIFU --- Need to obtain offset from header?") + off1 = 0. + off2 = 0. + off1 /= binspec + off2 /= binspat + crpix1 += off1 + crpix2 += off2 + + # Create a new WCS object. + msgs.info("Generating GNIRS IFU WCS") + w = wcs.WCS(naxis=3) + w.wcs.equinox = hdr['EQUINOX'] + w.wcs.name = 'GNIRS IFU' + w.wcs.radesys = 'FK5' + # Insert the coordinate frame + w.wcs.cname = ['RA', 'DEC', 'Wavelength'] + w.wcs.cunit = [units.degree, units.degree, units.Angstrom] + w.wcs.ctype = ["RA---TAN", "DEC--TAN", "WAVE"] # Note, WAVE is in vacuum + 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)`. + """ + # TODO :: The HRIFU might have 25 slits with 13 being the reference + xbins = np.arange(1 + 21) - 11.0 - 0.5 # 21 is for 21 slices, and 11 is the reference slit + 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 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`. + """ + return super().pypeit_file_keys() + ['filter'] diff --git a/pypeit/spectrographs/gtc_osiris.py b/pypeit/spectrographs/gtc_osiris.py index 8b972d5dae..6efe17ad99 100644 --- a/pypeit/spectrographs/gtc_osiris.py +++ b/pypeit/spectrographs/gtc_osiris.py @@ -100,7 +100,7 @@ def default_pypeit_par(cls): par['calibrations']['pixelflatframe']['process']['combine'] = 'median' # Wavelength calibration methods par['calibrations']['wavelengths']['method'] = 'full_template' - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI,ArI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI','ArI'] # Set the default exposure time ranges for the frame typing par['scienceframe']['exprng'] = [90, None] @@ -320,7 +320,7 @@ def config_specific_par(self, scifile, inp_par=None): # Wavelength calibration and setup-dependent parameters if self.get_meta_value(scifile, 'dispname') == 'R300B': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R300B.fits' par['reduce']['findobj']['find_min_max'] = [750, 2051] par['calibrations']['slitedges']['det_min_spec_length'] = 0.25 @@ -330,7 +330,7 @@ def config_specific_par(self, scifile, inp_par=None): par['reduce']['cube']['wave_min'] = 3600.0 par['reduce']['cube']['wave_max'] = 7200.0 elif self.get_meta_value(scifile, 'dispname') == 'R300R': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R300R.fits' par['reduce']['findobj']['find_min_max'] = [750, 2051] par['calibrations']['slitedges']['det_min_spec_length'] = 0.25 @@ -340,38 +340,38 @@ def config_specific_par(self, scifile, inp_par=None): par['reduce']['cube']['wave_min'] = 4800.0 par['reduce']['cube']['wave_max'] = 10000.0 elif self.get_meta_value(scifile, 'dispname') == 'R500B': - par['calibrations']['wavelengths']['lamps'] = ['HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R500B.fits' par['reduce']['findobj']['find_min_max'] = [500, 2051] par['reduce']['cube']['wave_min'] = 3600.0 par['reduce']['cube']['wave_max'] = 7200.0 elif self.get_meta_value(scifile, 'dispname') == 'R500R': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R500R.fits' par['reduce']['findobj']['find_min_max'] = [450, 2051] par['reduce']['cube']['wave_min'] = 4800.0 par['reduce']['cube']['wave_max'] = 10000.0 elif self.get_meta_value(scifile, 'dispname') == 'R1000B': - par['calibrations']['wavelengths']['lamps'] = ['ArI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['ArI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R1000B.fits' elif self.get_meta_value(scifile, 'dispname') == 'R1000R': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R1000R.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2000B': par['calibrations']['wavelengths']['fwhm'] = 15.0 - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2000B.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500U': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500U.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500V': par['calibrations']['wavelengths']['lamps'] = ['HgI','NeI','XeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500V.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500R': - par['calibrations']['wavelengths']['lamps'] = ['ArI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['ArI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500R.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500I': - par['calibrations']['wavelengths']['lamps'] = ['ArI,XeI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['ArI','XeI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500I.fits' par['sensfunc']['algorithm'] = 'IR' par['sensfunc']['IR']['telgridfile'] = "TelFit_MaunaKea_3100_26100_R20000.fits" @@ -711,7 +711,7 @@ def default_pypeit_par(cls): par['calibrations']['pixelflatframe']['process']['combine'] = 'median' # Wavelength calibration methods par['calibrations']['wavelengths']['method'] = 'full_template' - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI,ArI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI','ArI'] # Set the default exposure time ranges for the frame typing par['scienceframe']['exprng'] = [90, None] @@ -906,41 +906,41 @@ def config_specific_par(self, scifile, inp_par=None): # Wavelength calibrations if self.get_meta_value(scifile, 'dispname') == 'R300B': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R300B.fits' par['reduce']['findobj']['find_min_max']=[750,2051] elif self.get_meta_value(scifile, 'dispname') == 'R300R': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R300R.fits' par['reduce']['findobj']['find_min_max']=[750,2051] elif self.get_meta_value(scifile, 'dispname') == 'R500B': - par['calibrations']['wavelengths']['lamps'] = ['HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R500B.fits' par['reduce']['findobj']['find_min_max']=[500,2051] elif self.get_meta_value(scifile, 'dispname') == 'R500R': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R500R.fits' par['reduce']['findobj']['find_min_max']=[450,2051] elif self.get_meta_value(scifile, 'dispname') == 'R1000B': - par['calibrations']['wavelengths']['lamps'] = ['ArI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['ArI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R1000B.fits' elif self.get_meta_value(scifile, 'dispname') == 'R1000R': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R1000R.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2000B': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2000B.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500U': - par['calibrations']['wavelengths']['lamps'] = ['XeI,HgI'] + par['calibrations']['wavelengths']['lamps'] = ['XeI','HgI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500U.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500V': par['calibrations']['wavelengths']['lamps'] = ['HgI','NeI','XeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500V.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500R': - par['calibrations']['wavelengths']['lamps'] = ['ArI,HgI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['ArI','HgI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500R.fits' elif self.get_meta_value(scifile, 'dispname') == 'R2500I': - par['calibrations']['wavelengths']['lamps'] = ['ArI,XeI,NeI'] + par['calibrations']['wavelengths']['lamps'] = ['ArI','XeI','NeI'] par['calibrations']['wavelengths']['reid_arxiv'] = 'gtc_osiris_R2500I.fits' par['sensfunc']['algorithm'] = 'IR' par['sensfunc']['IR']['telgridfile'] = "TelFit_MaunaKea_3100_26100_R20000.fits" diff --git a/pypeit/tests/test_pypeitpar.py b/pypeit/tests/test_pypeitpar.py index 5115faf81d..dfb71b59d2 100644 --- a/pypeit/tests/test_pypeitpar.py +++ b/pypeit/tests/test_pypeitpar.py @@ -155,7 +155,7 @@ def test_telescope(): pypeitpar.TelescopePar() def test_fail_badpar(): - p = load_spectrograph('gemini_gnirs').default_pypeit_par() + p = load_spectrograph('gemini_gnirs_echelle').default_pypeit_par() # Faults because there's no junk parameter cfg_lines = ['[calibrations]', '[[biasframe]]', '[[[process]]]', 'junk = True'] @@ -164,7 +164,7 @@ def test_fail_badpar(): merge_with=cfg_lines) # Once as list def test_fail_badlevel(): - p = load_spectrograph('gemini_gnirs').default_pypeit_par() + p = load_spectrograph('gemini_gnirs_echelle').default_pypeit_par() # Faults because process isn't at the right level (i.e., there's no # process parameter for CalibrationsPar) diff --git a/setup.cfg b/setup.cfg index 4e6faba7e1..1e3a44bae7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,6 +89,7 @@ dev = console_scripts = # non-GUI scripts + pypeit_arxiv_solution = pypeit.scripts.arxiv_solution:ArxivSolution.entry_point pypeit_cache_github_data = pypeit.scripts.cache_github_data:CacheGithubData.entry_point pypeit_chk_for_calibs = pypeit.scripts.chk_for_calibs:ChkForCalibs.entry_point pypeit_chk_noise_1dspec = pypeit.scripts.chk_noise_1dspec:ChkNoise1D.entry_point