def load_STIS_data(self): """Load the right data according to `instr_config` for HST/STIS instrument""" # define pixel scales; values obtained from STScI # these values must be consistent with the given LSFs pixel_scale_dict = { 'G140L': 0.60 * u.AA, 'G140M': 0.05 * u.AA, 'G230L': 1.58 * u.AA, 'G230M': 0.09 * u.AA, 'E140M': 'lambda/91700', 'E140H': 'lambda/228000', 'E230M': 'lambda/60000', 'E230H': 'lambda/228000', 'G230LB': 1.35 * u.AA, 'G230MB': 0.15 * u.AA, 'G430L': 2.73 * u.AA, 'G430M': 0.28 * u.AA, 'G750L': 4.92 * u.AA, 'G750M': 0.56 * u.AA } # define channel based on grating name channel_dict = { 'G140L': 'FUV-MAMA', 'G140M': 'FUV-MAMA', 'G230L': 'NUV-MAMA', 'G230M': 'NUV_MAMA', 'E140M': 'FUV-MAMA', 'E140H': 'FUV-MAMA', 'E230M': 'NUV-MAMA', 'E230H': 'NUV-MAMA', 'G230LB': 'CCD', 'G230MB': 'CCD', 'G430L': 'CCD', 'G430M': 'CCD', 'G750L': 'CCD', 'G750M': 'CCD' } # also need slits available_slits = { 'G140L': ['52x0.1', '52x0.2', '52x0.5', '52x2.0'], 'G140M': ['52x0.1', '52x0.2', '52x0.5', '52x2.0'], 'G230L': ['52x0.1', '52x0.2', '52x0.5', '52x2.0'], 'G230M': ['52x0.1', '52x0.2', '52x0.5', '52x2.0'], 'E140H': ['0.1x0.03', '0.2x0.09', '0.2x0.2', '6x0.2'], 'E140M': ['0.1x0.03', '0.2x0.06', '0.2x0.2', '6x0.2'], 'E230H': ['0.1x0.03', '0.1x0.09', '0.1x0.2', '6x0.2'], 'E230M': ['0.1x0.03', '0.2x0.06', '0.2x0.2', '6x0.2'], 'G430L': ['52x0.1', '52x0.2', '52x0.5', '52x2.0'], 'G750L': ['52x0.1', '52x0.2', '52x0.5', '52x2.0'] } try: grating = self.instr_config['grating'] except: raise SyntaxError( '`grating` keyword missing in `instr_config` dictionary.') if grating not in channel_dict.keys(): raise NotImplementedError( 'Not ready for this HST/STIS grating: {}. ' 'Available gratings for HST/STIS are: {}'.format( grating, channel_dict.keys())) if grating in ['G750M', 'G430M', 'G430L']: raise NotImplementedError( '{} not implemented yet; coming soon...'.format(grating)) # We also need to know the slit width try: slit = self.instr_config['slit'] except: raise SyntaxError( '`slit` keyword missing in `instr_config` dictionary.') if slit not in available_slits[grating]: raise NotImplementedError( 'Not ready for this HST/STIS slit: {}. ' 'Available slits for HST/STIS grating {} are: {}'.format( slit, grating, available_slits[grating])) # now we need to read the right files (new ones for echelle modes) if grating[0] == 'E': lsf_files = glob.glob( lt_path + '/data/lsf/STIS/stis_LSF_{}_????_LTmod.txt'.format(grating)) # figure relevant wavelengths from file names wa_names = [ fname.split('/')[-1].split('_')[-2].split('.')[0] for fname in lsf_files ] else: lsf_files = glob.glob( lt_path + '/data/lsf/STIS/stis_LSF_{}_????.txt'.format(grating)) # figure relevant wavelengths from file names wa_names = [ fname.split('/')[-1].split('_')[-1].split('.')[0] for fname in lsf_files ] # sort them sorted_inds = np.argsort(wa_names) lsf_files = np.array(lsf_files)[sorted_inds] wa_names = np.array(wa_names)[sorted_inds] # read the relevant kernels; they may have different rel_pix values depending on wave kernels_dict = dict() for ii, file_name in enumerate(lsf_files): # get the column names f = open(file_name, 'r') lines = f.readlines() # get all lines f.close() col_names = lines[1] # column names are in second line # get rid of '\n' col_names = col_names.split('\n')[0] # split by blank space(s) and remove the first element col_names = col_names.split()[1:] # rename new first column col_names[0] = 'rel_pix' # get original data data_aux = ascii.read(file_name, data_start=2, names=col_names) # reformat rel_pix data_aux['rel_pix'] = self.check_and_reformat_relpix( data_aux['rel_pix']) #TODO: remove the following block upon testing ''' # handle asymmetric STIS LSF if data_aux['rel_pix'][len(data_aux['rel_pix']) // 2] == 0.: pass elif data_aux['rel_pix'][(len(data_aux['rel_pix']) // 2) - 1 ] ==0.: data_aux.insert_row(0,data_aux[-1]) data_aux['rel_pix'][0]=-data_aux['rel_pix'][-1] elif data_aux['rel_pix'][(len(data_aux['rel_pix']) // 2) + 1 ] ==0.: data_aux.add_row(data_aux[0]) data_aux['rel_pix'][-1]=-data_aux['rel_pix'][0] ''' # create column with absolute wavelength based on pixel scales if isinstance(pixel_scale_dict[grating], np.unicode): # deal with wavelength-dependent pixel scale (i.e., STIS echelle) scalefac = 1. / float(pixel_scale_dict[grating].split('/')[-1]) wave_aux = float(wa_names[ii]) * u.AA * ( 1. + data_aux['rel_pix'] * scalefac) else: wave_aux = float(wa_names[ii]) * u.AA + data_aux[ 'rel_pix'] * pixel_scale_dict[grating] data_aux[ 'wv'] = wave_aux # not used for now, but may be useful with a different approach # create column with normalized kernel for relevant slit kernel = data_aux[slit] / np.sum(data_aux[slit]) data_aux['kernel'] = kernel # store only rel_pix, wv, kernel as Table kernels_dict[wa_names[ii]] = data_aux['rel_pix', 'wv', 'kernel'] # at this point the kernels_dict have the original information as given by the STScI # tables, in their various formats... # project to a single rel_pix scale using interpolation; for simplicity use a # custom rel_pix because STScI sometimes provide them as non-constant pixel fractions. # This new rel_pix grid will apply to all the wavelengths rel_pix = kernels_dict[wa_names[0]][ 'rel_pix'] # use the first one as reference rel_pix = np.linspace(-1 * np.max(rel_pix), np.max(rel_pix), len(rel_pix)) # impose them linear and symmetric rel_pix = self.check_and_reformat_relpix( rel_pix) # again just in case something odd happened data_table = Table() data_table['rel_pix'] = rel_pix for wa_name in wa_names: kernel_aux = interp_Akima(data_table['rel_pix'], kernels_dict[wa_name]['rel_pix'], kernels_dict[wa_name]['kernel']) data_table['{}A'.format(wa_name)] = kernel_aux pixel_scale = pixel_scale_dict[ grating] # read from dictionary defined above # import pdb; pdb.set_trace() # todo: work out a cleverer approach to this whole issue of having different rel_pix, pixel_scales, etc return pixel_scale, data_table
def interpolate_to_wv_array(self, wv_array, kind='Akima', debug=False): """ Interpolate an LSF to a wavelength array. Given `wv_array` this function interpolates an LSF to match both scale and extent of `wv_array` using the Akima or cubic-spline interpolators (default is Akima). Some checks are performed too. Parameters ---------- wv_array : Quantity numpy.ndarray, shape(N,) Wavelength array for which the LSF kernel is defined. The central wavelength value of `wv_array` define the wavelength at which the LSF is defined, while the limits of `wv_array` define the extent of the kernel. kind : str, optional Specifies the kind of interpolation as a string either ('cubic', 'Akima'); default is `Akima`. Returns ------- lsf_table : Table The interpolated lsf using at the central wavelength of `wv_array`, using the same pixel scale as `wv_array`. This table has two columns: 'wv' and 'kernel'. (lst_table['wv'] is equal to `wv_array` by construction.) """ # Check correct format if not ((isinstance(wv_array, np.ndarray)) or (isinstance(wv_array, Quantity))): raise SyntaxError('`wv_array` must be Quantity numpy.ndarray') elif len(wv_array.shape) != 1: raise SyntaxError( '`wv_array` must be of shape(N,), i.e. 1-dimensional array') if kind not in ['cubic', 'Akima', 'akima']: raise ValueError( 'Only `cubic` or `Akima` interpolation available.') # define useful quantities wv_min = np.min(wv_array) wv_max = np.max(wv_array) wv0 = 0.5 * (wv_max + wv_min) # interpolate over tabulated LSF values if provided if len(self._data.colnames) > 2.: lsf_tab = self.interpolate_to_wv0(wv0) else: lsf_tab = self.shift_to_wv0(wv0) # make sure the wv_array is dense enough to sample the LSF kernel kernel_wvmin = np.min(lsf_tab['wv']) * u.AA kernel_wvmax = np.max(lsf_tab['wv']) * u.AA cond = (wv_array >= kernel_wvmin) & (wv_array <= kernel_wvmax) if np.sum( cond) < 10: # this number is somewhat arbitrary but reasonable raise ValueError( 'The input `wv_array` is undersampling the LSF kernel! Try a finer grid.' ) # convert to Angstroms wv_array_AA = np.array([wv.to('AA').value for wv in wv_array]) # interpolate to wv_array if kind == 'cubic': f = interp1d(lsf_tab['wv'], lsf_tab['kernel'], kind='cubic', bounds_error=False, fill_value=0) lsf_vals = f(wv_array_AA) elif kind in ('Akima', 'akima'): # f = Akima1DInterpolator(lsf_tab['wv'],lsf_tab['kernel']) # NT: I tried Akima interpolator from scipy.interpolate # and is not robust in extreme situations where the # wv_array is large compared to the kernel FWHM. # Let's try linetools.analysis.interp Akima version lsf_vals = interp_Akima(wv_array_AA, lsf_tab['wv'], lsf_tab['kernel']) # make sure the kernel is never negative cond = lsf_vals < 0 if np.sum(cond) > 0: warnings.warn( 'The interpolated kernel has negative values; imposing them to be 0.' ) if debug: import matplotlib.pyplot as plt plt.plot(wv_array_AA, lsf_vals, 'k-') # import pdb; pdb.set_trace() lsf_vals = np.where(lsf_vals < 0, 0., lsf_vals) # normalize lsf_vals /= np.sum(lsf_vals) # re-define Table lsf_tab = Table() lsf_tab.add_column(Column(name='wv', data=wv_array)) lsf_tab.add_column(Column(name='kernel', data=lsf_vals)) return lsf_tab
def interpolate_to_wv_array(self,wv_array, kind='Akima'): """ Interpolate an LSF to a wavelength array. Given `wv_array` this function interpolates an LSF to match both scale and extent of `wv_array` using the Akima or cubic-spline interpolators (default is Akima). Some checks are performed too. Parameters ---------- wv_array : Quantity numpy.ndarray, shape(N,) Wavelength array for which the LSF kernel is defined. The central wavelength value of `wv_array` define the wavelength at which the LSF is defined, while the limits of `wv_array` define the extent of the kernel. kind : str, optional Specifies the kind of interpolation as a string either ('cubic', 'Akima'); default is `Akima`. Returns ------- lsf_table : Table The interpolated lsf using at the central wavelength of `wv_array`, using the same pixel scale as `wv_array`. This table has two columns: 'wv' and 'kernel'. (lst_table['wv'] is equal to `wv_array` by construction.) """ # Check correct format if not ((isinstance(wv_array, np.ndarray)) or (isinstance(wv_array, Quantity))): raise SyntaxError('`wv_array` must be Quantity numpy.ndarray') elif len(wv_array.shape) != 1: raise SyntaxError('`wv_array` must be of shape(N,), i.e. 1-dimensional array') if kind not in ['cubic','Akima','akima']: raise ValueError('Only `cubic` or `Akima` interpolation available.') #define useful quantities wv_min = np.min(wv_array) wv_max = np.max(wv_array) wv0 = 0.5 * (wv_max + wv_min) lsf_tab = self.interpolate_to_wv0(wv0) #convert to Angstroms wv_array_AA = np.array([wv.to('AA').value for wv in wv_array]) #interpolate to wv_array if kind == 'cubic': f = interp1d(lsf_tab['wv'],lsf_tab['kernel'],kind='cubic',bounds_error=False,fill_value=0) lsf_vals = f(wv_array_AA) elif kind in ('Akima','akima'): # f = Akima1DInterpolator(lsf_tab['wv'],lsf_tab['kernel']) # NT: I tried Akima interpolator from scipy.interpolate # and is not robust in extreme situations where the # wv_array is large compared to the kernel FWHM. #Let's try linetools.analysis.interp Akima version lsf_vals = interp_Akima(wv_array_AA,lsf_tab['wv'],lsf_tab['kernel']) #normalize lsf_vals /= np.max(lsf_vals) #re-define Table lsf_tab = Table() lsf_tab.add_column(Column(name='wv',data=wv_array)) lsf_tab.add_column(Column(name='kernel',data=lsf_vals)) return lsf_tab