def CRmasker(parfit, fitobj): ''' Identify cosmic rays and hot pixels in spectrum, as well as places where the model does not have the ability to reflect the data. Inputs: parfit : Best fit spectral model parameters fitobj : Class containing data to be fit and stellar and telluric templates Outputs: CRmaskF : Pixels to be masked ''' fit, chi = fmod(parfit, fitobj) # Everywhere where data protrudes high above model, check whether slope surrounding protrusion is /\ and mask if sufficiently steep residual = fitobj.s / fit MAD = np.median(np.abs(np.median(residual) - residual)) CRmask = np.array(np.where(residual > np.median(residual) + 2 * MAD)[0]) CRmaskF = [] CRmask = list(CRmask) for hit in [0, len(fitobj.x) - 1]: if hit in CRmask: CRmaskF.append(hit) CRmask.remove(hit) CRmask = np.array(CRmask, dtype=np.int) CRmaskF = np.array(CRmaskF, dtype=np.int) for group in mit.consecutive_groups(CRmask): group = np.array(list(group)) if len(group) == 1: gL = group - 1 gR = group + 1 else: peaks = detect_peaks(fitobj.s[group]) if len(peaks) < 1: group = np.concatenate((np.array([group[0] - 1]), group, np.array([group[-1] + 1]))) peaks = detect_peaks(fitobj.s[group]) if len(peaks) < 1: continue if len(peaks) > 1: continue gL = group[:peaks[0]] gR = group[peaks[0] + 1:] slopeL = (fitobj.s[gL + 1] - fitobj.s[gL]) / (fitobj.x[gL + 1] - fitobj.x[gL]) slopeR = (fitobj.s[gR] - fitobj.s[gR - 1]) / (fitobj.x[gR] - fitobj.x[gR - 1]) try: if (np.min(slopeL) > 300) and (np.max(slopeR) < -300) and len(group) < 6: CRmaskF = np.concatenate((CRmaskF, group)) except ValueError: if (slopeL > 300) and (slopeR < -300): CRmaskF = np.concatenate((CRmaskF, group)) return CRmaskF
def outplotter(parfit, fitobj, title): fit, chi = fmod(parfit, fitobj) w = parfit[6] + parfit[7] * fitobj.x + parfit[8] * ( fitobj.x**2.) + parfit[9] * (fitobj.x**3.) fig, axes = plt.subplots(1, 1, figsize=(5, 3), facecolor='white', dpi=300) axes.plot(w, fitobj.s, '-', c='k', lw=0.5, label='data', alpha=.6) axes.plot(w, fit, '--', c='tab:red', lw=0.5, label='model', alpha=.6) axes.set_title(title, size=5, style='normal', family='sans-serif') axes.set_ylabel(r'Normalized Flux', size=5, style='normal', family='sans-serif') axes.set_xlabel(r'Wavelength [$\AA$]', size=5, style='normal', family='sans-serif') axes.tick_params(axis='both', labelsize=4.5, right=True, top=True, direction='in') axes.legend(fontsize=4, edgecolor='white') fig.savefig('{}/figs_{}/{}.png'.format(inparam.outpath, args.band, title), bbox_inches='tight', format='png', overwrite=True)
def outplotter_tel(parfit, fitobj, title, inparam, args, order): ''' Plots model fit to telluric standard observation. Inputs: parfit : Best fit parameters fitobj : Class containing spectral data to be fit and templates for use in fit title : Title of plot file inparam : Class containing variety of information (e.g. on observing conditions) order : Echelle order, as characterized by file index (as opposed to m number; for conversion between the two, see Stahl et al. 2021) args : Information as input by user from command line ''' fit, chi = fmod(parfit, fitobj) npars = len(parfit) mask = np.ones_like(fitobj.s, dtype=bool) mask[(fitobj.s < .0)] = False if len(fitobj.mask) != 0: for maskbounds in fitobj.mask: mask[(fitobj.x > maskbounds[0]) & (fitobj.x < maskbounds[1])] = False mask[fitobj.CRmask] = False if args.band == 'H': if np.int(order) in [13]: npars -= 4 elif np.int(order) in [6, 14, 21]: npars -= 3 else: pass else: # print("We haven't determined what polynomial orders for K band yet and hardcoded this!") if np.int(order) in [3]: npars -= 4 elif np.int(order) in [4, 5]: npars -= 3 else: pass if fitobj.masterbeam == 'B': npars -= 5 npars -= 6 # subtract 6 from npars total: 2 for linear/quadratic IP, 1 for RV_telluric, 2 fot stellar template power and RV, 1 for vsini chi_new = chi * (len(fitobj.s[mask]) - len(parfit)) / ( len(fitobj.s[mask]) - npars) # correct reduce chisq w = parfit[6] + parfit[7] * fitobj.x + parfit[8] * ( fitobj.x**2.) + parfit[9] * (fitobj.x**3.) cont = parfit[10] + parfit[11] * fitobj.x + parfit[12] * ( fitobj.x**2) + parfit[20] * (fitobj.x**3) + parfit[21] * ( fitobj.x**4) + parfit[22] * (fitobj.x**5) + parfit[23] * (fitobj.x **6) if fitobj.masterbeam == 'A': bucket = np.zeros_like(cont) bucket[(fitobj.x >= (parfit[15] - parfit[16] / 2)) & (fitobj.x <= (parfit[15] + parfit[16] / 2))] = parfit[17] bucket[(fitobj.x >= (parfit[15] + parfit[16] / 2 - parfit[18])) & (fitobj.x <= (parfit[15] + parfit[16] / 2))] += parfit[19] cont -= bucket cont *= fitobj.continuum mask2 = np.ones_like(fitobj.x, dtype=bool) mask2[fitobj.CRmask] = False fig, axes = plt.subplots(1, 1, figsize=(6, 3), facecolor='white', dpi=300) axes.plot(w, fitobj.s, '-', c='k', lw=0.7, label='data', alpha=.3) axes.plot(w[mask2], fitobj.s[mask2], '-', c='k', lw=0.7, label='data (emission removed)', alpha=.8) axes.plot(w[mask2], fit[mask2], '--', c='tab:red', lw=0.7, label='model', alpha=.8) axes.plot(w[mask2], cont[mask2], '--', c='tab:blue', lw=0.7, label='cont', alpha=.8) axes.set_title(title, size=6, style='normal', family='sans-serif') axes.set_ylabel(r'Flux', size=6, style='normal', family='sans-serif') axes.set_xlabel(r'Wavelength [$\AA$]', size=6, style='normal', family='sans-serif') axes.xaxis.set_minor_locator(AutoMinorLocator(5)) axes.yaxis.set_minor_locator(AutoMinorLocator(2)) axes.tick_params(axis='both', which='both', labelsize=6, right=True, top=True, direction='in') axes.legend(fontsize=5, edgecolor='white') fig.text(0.65, 0.2, r'$\rm \chi^{{2}}_{{\nu}}$ = {:1.2f}'.format(chi_new), size=6, style='normal', family='sans-serif') fig.savefig('{}/figs_{}/{}.png'.format(inparam.outpath, args.band, title), bbox_inches='tight', format='png', overwrite=True)
def outplotter_23(parfit, fitobj, title, trk, inparam, args, step2or3, order): ''' Plots model fit to science target observation. Inputs: parfit : Best fit parameters fitobj : Class containing spectral data to be fit and templates for use in fit title : Title of plot file trk : Number of run (e.g. RV_results_1, RV_results_2) inparam : Class containing variety of information (e.g. on observing conditions) args : Information as input by user from command line step2or3 : Whether run is Step 2 or Step 3 order : Echelle order, as characterized by file index (as opposed to m number; for conversion between the two, see Stahl et al. 2021) ''' fit, chi = fmod(parfit, fitobj) npars = len(parfit) mask = np.ones_like(fitobj.s, dtype=bool) mask[(fitobj.s < .0)] = False if len(fitobj.mask) != 0: for maskbounds in fitobj.mask: mask[(fitobj.x > maskbounds[0]) & (fitobj.x < maskbounds[1])] = False mask[fitobj.CRmask] = False if args.band == 'H': if np.int(order) in [13]: npars -= 4 elif np.int(order) in [6, 14, 21]: npars -= 3 else: pass else: # print("We haven't determined what polynomial orders for K band yet and hardcoded this!") if np.int(order) in [3]: npars -= 4 elif np.int(order) in [4, 5]: npars -= 3 else: pass if fitobj.masterbeam == 'B': npars -= 5 npars -= 3 # subtract 3 from npars total, 2 for linear/quadratic IP and 1 for RV_telluric chi_new = chi * (len(fitobj.s[mask]) - len(parfit)) / (len(fitobj.s[mask]) - npars) w = parfit[6] + parfit[7] * fitobj.x + parfit[8] * ( fitobj.x**2.) + parfit[9] * (fitobj.x**3.) cont = parfit[10] + parfit[11] * fitobj.x + parfit[12] * ( fitobj.x**2) + parfit[20] * (fitobj.x**3) + parfit[21] * ( fitobj.x**4) + parfit[22] * (fitobj.x**5) + parfit[23] * (fitobj.x **6) if fitobj.masterbeam == 'A': bucket = np.zeros_like(cont) bucket[(fitobj.x >= (parfit[15] - parfit[16] / 2)) & (fitobj.x <= (parfit[15] + parfit[16] / 2))] = parfit[17] bucket[(fitobj.x >= (parfit[15] + parfit[16] / 2 - parfit[18])) & (fitobj.x <= (parfit[15] + parfit[16] / 2))] += parfit[19] cont -= bucket cont *= fitobj.continuum fig, axes = plt.subplots(1, 1, figsize=(6, 3), facecolor='white', dpi=300) mask2 = np.ones_like(fitobj.x, dtype=bool) mask2[fitobj.CRmask] = False n = len(fitobj.mask) if n > 0: widths = [fitobj.mask[0][0] - fitobj.x[0]] for m in range(n - 1): widths.append(fitobj.mask[m + 1][0] - fitobj.mask[m][1]) widths.append(fitobj.x[-1] - fitobj.mask[n - 1][1]) gs = gridspec.GridSpec(1, n + 1, width_ratios=widths) for m in range(n + 1): ax0 = plt.subplot(gs[m]) ax0.plot(w, fitobj.s, '--', c='k', lw=0.7, label='data', alpha=.3) ax0.plot(w[mask2], fitobj.s[mask2], '-', c='k', lw=0.7, label='data (emission removed)', alpha=.8) ax0.plot(w[mask2], fit[mask2], '--', c='tab:red', lw=0.7, label='model', alpha=.8) ax0.plot(w[mask2], cont[mask2], '--', c='tab:blue', lw=0.7, label='cont', alpha=.8) kwargs = dict(transform=ax0.transAxes, color='k', clip_on=False, lw=0.6) if m == 0: ax0.tick_params(axis='both', labelsize=6, right=False, top=True, direction='in') left = w[0] right = parfit[6] + parfit[7] * fitobj.mask[m][0] + parfit[ 8] * (fitobj.mask[m][0]** 2.) + parfit[9] * (fitobj.mask[m][0]**3.) ax0.plot([right, right], [min(fitobj.s), max(fitobj.s)], '--k', lw=0.75) elif m == n: ax0.tick_params(axis='both', labelsize=6, left=False, right=True, top=True, direction='in') ax0.set_yticklabels([]) #ax0.vlines([left+1],'--k',lw=0.75) left = parfit[6] + parfit[7] * fitobj.mask[m - 1][1] + parfit[ 8] * (fitobj.mask[m - 1][1]** 2.) + parfit[9] * (fitobj.mask[m - 1][1]**3.) right = w[-1] else: ax0.tick_params(axis='both', labelsize=6, right=False, left=False, top=True, direction='in') ax0.set_yticklabels([]) #ax0.vlines([left+1],'--k',lw=0.75) ax0.plot([right, right], [min(fitobj.s), max(fitobj.s)], '--k', lw=0.75) left = parfit[6] + parfit[7] * fitobj.mask[m - 1][1] + parfit[ 8] * (fitobj.mask[m - 1][1]** 2.) + parfit[9] * (fitobj.mask[m - 1][1]**3.) right = parfit[6] + parfit[7] * fitobj.mask[m][0] + parfit[ 8] * (fitobj.mask[m][0]** 2.) + parfit[9] * (fitobj.mask[m][0]**3.) ax0.set_xlim(left, right) if m != 0: ax0.spines['left'].set_visible(False) if m != n: ax0.spines['right'].set_visible(False) fig.tight_layout(pad=0.0) fig.suptitle(title, x=0.5, y=1.05, size=6, style='normal', family='sans-serif') fig.text(0.5, -0.04, r'Wavelength [$\rm\AA$]', ha='center', size=6, style='normal', family='sans-serif') fig.text(-0.04, 0.5, r'Flux', va='center', rotation='vertical', size=6, style='normal', family='sans-serif') fig.text(0.65, 0.2, r'$\rm \chi^{{2}}_{{\nu}}$ = {:1.2f}'.format(chi_new), size=6, style='normal', family='sans-serif') ax0.legend(fontsize=5, edgecolor='white') else: fig, axes = plt.subplots(1, 1, figsize=(6, 3), facecolor='white', dpi=300) axes.plot(w, fitobj.s, '--', c='k', lw=0.7, label='data', alpha=.3) axes.plot(w[mask2], fitobj.s[mask2], '-', c='k', lw=0.7, label='data (emission removed)', alpha=.8) axes.plot(w[mask2], fit[mask2], '--', c='tab:red', lw=0.7, label='model', alpha=.8) axes.plot(w[mask2], cont[mask2], '--', c='tab:blue', lw=0.7, label='cont', alpha=.8) axes.tick_params(axis='both', labelsize=6, right=True, top=True, direction='in') axes.set_title(title, size=6, style='normal', family='sans-serif') axes.set_ylabel(r'Flux', size=6, style='normal', family='sans-serif') axes.set_xlabel(r'Wavelength [$\rm\AA$]', size=6, style='normal', family='sans-serif') fig.text(0.65, 0.2, r'$\rm \chi^{{2}}_{{\nu}}$ = {:1.2f}'.format(chi_new), size=6, style='normal', family='sans-serif') axes.tick_params(axis='both', labelsize=6, right=True, top=True, direction='in') axes.legend(fontsize=5, edgecolor='white') fig.savefig( f'{inparam.outpath}/figs/main_step{step2or3}_{args.band}_{trk}/{title}.png', bbox_inches='tight', format='png', overwrite=True)
def ini_MPinst(args, inparam, orders, order_use, trk, step2or3, i): # Main function for RV fitting that will be threaded over by multiprocessing nights = inparam.nights night = nights[i] # current looped night order = order_use xbounds = inparam.xbounddict[order] print('Working on order {:02d}, night {:03d}/{:03d} ({}) PID:{}...'.format( int(order), i + 1, len(inparam.nights), night, mp.current_process().pid)) #------------------------------------------------------------------------------- # Collect initial RV guesses if type(inparam.initguesses) == dict: initguesses = inparam.initguesses[night] elif type(inparam.initguesses) == float: initguesses = inparam.initguesses else: sys.exit( 'ERROR! EXPECING SINGAL NUMBER OR FILE FOR INITGUESSES! QUITTING!') # Collect relevant beam and filenum info tagsnight = [] beamsnight = [] for tag in inparam.tagsA[night]: tagsnight.append(tag) beamsnight.append('A') for tag in inparam.tagsB[night]: tagsnight.append(tag) beamsnight.append('B') # Load synthetic telluric template generated during Step 1 # [:8] here is to ensure program works under Night_Split mode A0loc = f'../Output/{args.targname}_{args.band}_tool/A0Fits/{night[:8]}A0_treated_{args.band}.fits' try: hdulist = fits.open(A0loc) except IOError: logger.warning( f' --> No A0-fitted template for night {night}, skipping...') return night, np.nan, np.nan # Find corresponding table in fits file, given the tables do not go sequentially by order number due to multiprocessing in Step 1 num_orders = 0 for i in range(25): try: hdulist[i].columns[0].name[9:] num_orders += 1 except: continue # Check whether Telfit hit critical error in Step 1 for the chosen order with this night. If so, try another order. If all hit the error, skip the night. nexto = 0 ordertry = order while 1 == 1: fits_layer = [ i for i in np.arange(num_orders) + 1 if int(hdulist[i].columns[0].name[9:]) == ordertry ][0] tbdata = hdulist[fits_layer].data flag = np.array(tbdata[f'ERRORFLAG{ordertry}'])[0] if flag == 1: # If Telfit hit unknown critical error in Step 1, this order can't be used for this night. Try another. orderbad = ordertry ordertry = orders[nexto] logger.warning( f' --> TELFIT ENCOUNTERED CRITICAL ERROR IN ORDER: {orderbad} NIGHT: {night}, TRYING ORDER {ordertry} INSTEAD...' ) else: # All good, continue break nexto += 1 if nexto == len(orders): logger.warning( f' --> TELFIT ENCOUNTERED CRITICAL ERROR IN ALL ORDERS FOR NIGHT: {night}, skipping...' ) return night, np.nan, np.nan watm = tbdata['WATM' + str(order)] satm = tbdata['SATM' + str(order)] a0contx = tbdata['X' + str(order)] continuum = tbdata['BLAZE' + str(order)] # Remove extra rows leftover from having columns of unequal length satm = satm[(watm != 0)] watm = watm[(watm != 0)] satm[( satm < 1e-4 )] = 0. # set very low points to zero so that they don't go to NaN when taken to an exponent by template power in fmodel_chi a0contx = a0contx[(continuum != 0)] continuum = continuum[(continuum != 0)] # Use instrumental profile FWHM dictionary corresponding to whether IGRINS mounting was loose or not if int(night[:8]) < 20180401 or int(night[:8]) > 20190531: IPpars = inparam.ips_tightmount_pars[args.band][order] else: IPpars = inparam.ips_loosemount_pars[args.band][order] #------------------------------------------------------------------------------- ### Initialize parameter array for optimization as well as half-range values for each parameter during the various steps of the optimization. ### Many of the parameters initialized here will be changed throughout the code before optimization and in between optimization steps. pars0 = np.array([ np.nan, # 0: The shift of the stellar template (km/s) 0.3, # 1: The scale factor for the stellar template 0.0, # 2: The shift of the telluric template (km/s) 0.6, # 3: The scale factor for the telluric template inparam.initvsini, # 4: vsini (km/s) IPpars[2], # 5: The instrumental resolution (FWHM) in pixels np.nan, # 6: Wavelength 0-pt np.nan, # 7: Wavelength linear component np.nan, # 8: Wavelength quadratic component np.nan, # 9: Wavelength cubic component 1.0, #10: Continuum zero point 0., #11: Continuum linear component 0., #12: Continuum quadratic component IPpars[1], #13: Instrumental resolution linear component IPpars[0] ]) #14: Instrumental resolution quadratic component rvsmini = [] vsinismini = [] # Iterate over all A/B exposures for t in np.arange(len(tagsnight)): tag = tagsnight[t] beam = beamsnight[t] # Retrieve pixel bounds for where within each other significant telluric absorption is present. # If these bounds were not applied, analyzing some orders would give garbage fits. if args.band == 'K': if int(order) in [11, 12, 13, 14]: bound_cut = inparam.bound_cut_dic[args.band][order] else: bound_cut = [150, 150] elif args.band == 'H': if int(order) in [ 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]: bound_cut = inparam.bound_cut_dic[args.band][order] else: bound_cut = [150, 150] # Load target spectrum x, wave, s, u = init_fitsread(f'{inparam.inpath}{night}/{beam}/', 'target', 'separate', night, order, tag, args.band, bound_cut) #------------------------------------------------------------------------------- # Execute S/N cut s2n = s / u if np.nanmedian(s2n) < float(args.SN_cut): logger.warning( ' --> Bad S/N {:1.3f} < {} for {}{} {}, SKIP'.format( np.nanmedian(s2n), args.SN_cut, night, beam, tag)) continue # Trim obvious outliers above the blaze (i.e. cosmic rays) nzones = 5 x = basicclip_above(x, s, nzones) wave = basicclip_above(wave, s, nzones) u = basicclip_above(u, s, nzones) s = basicclip_above(s, s, nzones) x = basicclip_above(x, s, nzones) wave = basicclip_above(wave, s, nzones) u = basicclip_above(u, s, nzones) s = basicclip_above(s, s, nzones) # Cut spectrum to within wavelength regions defined in input list s_piece = s[(x > xbounds[0]) & (x < xbounds[-1])] u_piece = u[(x > xbounds[0]) & (x < xbounds[-1])] wave_piece = wave[(x > xbounds[0]) & (x < xbounds[-1])] x_piece = x[(x > xbounds[0]) & (x < xbounds[-1])] # Trim stellar template to relevant wavelength range mwave_in, mflux_in = stellarmodel_setup(wave_piece, inparam.mwave0, inparam.mflux0) # Trim telluric template to relevant wavelength range satm_in = satm[(watm > min(wave_piece) * 1e4 - 11) & (watm < max(wave_piece) * 1e4 + 11)] watm_in = watm[(watm > min(wave_piece) * 1e4 - 11) & (watm < max(wave_piece) * 1e4 + 11)] # Make sure data is within telluric template range (shouldn't do anything) s_piece = s_piece[(wave_piece * 1e4 > min(watm_in) + 5) & (wave_piece * 1e4 < max(watm_in) - 5)] u_piece = u_piece[(wave_piece * 1e4 > min(watm_in) + 5) & (wave_piece * 1e4 < max(watm_in) - 5)] x_piece = x_piece[(wave_piece * 1e4 > min(watm_in) + 5) & (wave_piece * 1e4 < max(watm_in) - 5)] wave_piece = wave_piece[(wave_piece * 1e4 > min(watm_in) + 5) & (wave_piece * 1e4 < max(watm_in) - 5)] #------------------------------------------------------------------------------- par = pars0.copy() # Get initial guess for cubic wavelength solution from reduction pipeline f = np.polyfit(x_piece, wave_piece, 3) par9in = f[0] * 1e4 par8in = f[1] * 1e4 par7in = f[2] * 1e4 par6in = f[3] * 1e4 par[9] = par9in par[8] = par8in par[7] = par7in par[6] = par6in par[0] = initguesses - inparam.bvcs[ night + tag] # Initial RV with barycentric correction # Arrays defining parameter variations during optimization steps. # Optimization will cycle twice. In the first cycle, the RVs can vary more than in the second. dpars1 = { 'cont': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0., 1e7, 1, 1, 0, 0 ]), 'wave': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 0., 0, 0, 0, 0, 0 ]), 't': np.array([ 0.0, 0.0, 5.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0 ]), 'ip': np.array( [0.0, 0.0, 0.0, 0.0, 0, 0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0]), 's': np.array([ 20.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0 ]), 'v': np.array([ 0.0, 0.0, 0.0, 0.0, inparam.vsinivary, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0 ]) } dpars2 = { 'cont': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0., 1e7, 1, 1, 0, 0 ]), 'wave': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 0., 0, 0, 0, 0, 0 ]), 't': np.array([ 0.0, 0.0, 5.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0 ]), 'ip': np.array( [0.0, 0.0, 0.0, 0.0, 0, 0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0]), 's': np.array([ 5.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0 ]), 'v': np.array([ 0.0, 0.0, 0.0, 0.0, inparam.vsinivary, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0 ]) } continuum_in = rebin_jv(a0contx, continuum, x_piece, False) s_piece /= np.median(s_piece) fitobj = fitobjs(s_piece, x_piece, u_piece, continuum_in, watm_in, satm_in, mflux_in, mwave_in, ast.literal_eval(inparam.maskdict[order])) #------------------------------------------------------------------------------- # Initialize an array that puts hard bounds on vsini and the instrumental resolution to make sure they do not diverge to unphysical values optimize = True par_in = par.copy() hardbounds = [ par_in[4] - dpars1['v'][4], par_in[4] + dpars1['v'][4], par_in[5] - dpars1['ip'][5], par_in[5] + dpars1['ip'][5] ] if hardbounds[0] < 0: hardbounds[0] = 0 if hardbounds[3] < 0: hardbounds[3] = 1 # Begin optimization. Fit the blaze, the wavelength solution, the telluric template power and RV, the stellar template power and RV, the # zero point for the instrumental resolution, and the vsini of the star separately, iterating and cycling between each set of parameter fits. cycles = 2 optgroup = [ 'cont', 'wave', 't', 'cont', 's', 'cont', 'wave', 't', 's', 'cont', 'wave', 'ip', 'v', 'ip', 'v', 't', 's', 't', 's' ] for nc, cycle in enumerate(np.arange(cycles), start=1): if cycle == 0: parstart = par_in.copy() dpars = dpars1 else: dpars = dpars2 for optkind in optgroup: parfit_1 = optimizer(parstart, dpars[optkind], hardbounds, fitobj, optimize) parstart = parfit_1.copy() if args.debug: logger.debug(f'{order}_{tag}_{nc}_{optkind}:\n {parfit_1}') parfit = parfit_1.copy() #------------------------------------------------------------------------------- # if best fit stellar template power is very low, throw out result if parfit[1] < 0.1: logger.warning(f' --> parfit[1] < 0.1, {night} parfit={parfit}') continue # if best fit stellar or telluric template powers are exactly equal to their starting values, optimization failed, throw out result if parfit[1] == par_in[1] or parfit[3] == par_in[3]: logger.warning( f' --> parfit[1] == par_in[1] or parfit[3] == par_in[3], {night}' ) continue # if best fit model dips below zero at any point, we're too close to edge of blaze, fit may be comrpomised, throw out result smod, chisq = fmod(parfit, fitobj) if len(smod[(smod < 0)]) > 0: logger.warning(f' --> len(smod[(smod < 0)]) > 0, {night}') continue rv0 = parfit[0] - parfit[ 2] # Correct for RV of the atmosphere, since we're using that as the basis for the wavelength scale rvsmini.append(rv0 + inparam.bvcs[night + tag] + rv0 * inparam.bvcs[night + tag] / (3e5**2)) # Barycentric correction vsinismini.append(parfit[4]) bestguess = round(np.nanmean(rvsmini), 5) vsinimini = round(np.nanmean(vsinismini), 5) return night, bestguess, vsinimini
def rv_MPinst(args, inparam, orders, order_use, trk, step2or3, i): # Main function for RV fitting that will be threaded over by multiprocessing nights = inparam.nights night = nights[i] # current looped night order = orders[order_use] xbounds = inparam.xbounddict[order] firstorder = orders[ 0] # First order that will be analyzed, related to file writing print( 'Working on order {:02d}/{:02d} ({}), night {:03d}/{:03d} ({}) PID:{}...' .format( int(order_use) + 1, len(orders), order, i + 1, len(inparam.nights), night, mp.current_process().pid)) #------------------------------------------------------------------------------- # Collect relevant beam and filenum info tagsnight = [] beamsnight = [] for tag in inparam.tagsA[night]: tagsnight.append(tag) beamsnight.append('A') for tag in inparam.tagsB[night]: tagsnight.append(tag) beamsnight.append('B') nightsout = [] wminibox = np.ones(2048) sminibox = np.ones(2048) flminibox_tel = np.ones(2048) flminibox_ste = np.ones(2048) contiminibox = np.ones(2048) flminibox_mod = np.ones(2048) wminibox[:] = np.nan sminibox[:] = np.nan flminibox_tel[:] = np.nan flminibox_ste[:] = np.nan contiminibox[:] = np.nan flminibox_mod[:] = np.nan for t in tagsnight: nightsout.append(night) #------------------------------------------------------------------------------- # Collect initial RV guesses if type(inparam.initguesses) == dict: initguesses = inparam.initguesses[night] elif type(inparam.initguesses) == float: initguesses = inparam.initguesses else: sys.exit( 'ERROR! EXPECING SINGAL NUMBER OR FILE FOR INITGUESSES! QUITTING!') if np.isnan(initguesses) == True: logger.warning( f' --> Previous run of {night} found it inadequate, skipping...') return nightsout, rvsminibox, parfitminibox, vsiniminibox, tagsminibox # start at bucket loc = 1250 +- 100, width = 250 +- 100, depth = 100 +- 5000 but floor at 0 if args.band == 'H': centerloc = 1250 else: centerloc = 1180 #------------------------------------------------------------------------------- ### Initialize parameter array for optimization as well as half-range values for each parameter during the various steps of the optimization. ### Many of the parameters initialized here will be changed throughout the code before optimization and in between optimization steps. pars0 = np.array([ np.nan, # 0: The shift of the stellar template (km/s) [assigned later] 0.3, # 1: The scale factor for the stellar template 0.0, # 2: The shift of the telluric template (km/s) 0.6, # 3: The scale factor for the telluric template inparam.initvsini, # 4: vsini (km/s) np.nan, # 5: The instrumental resolution (FWHM) in pixels np.nan, # 6: Wavelength 0-pt np.nan, # 7: Wavelength linear component np.nan, # 8: Wavelength quadratic component np.nan, # 9: Wavelength cubic component 1.0, #10: Continuum zero point 0., #11: Continuum linear component 0., #12: Continuum quadratic component np.nan, #13: Instrumental resolution linear component np.nan, #14: Instrumental resolution quadratic component centerloc, #15: Blaze dip center location 330, #16: Blaze dip full width 0.05, #17: Blaze dip depth 90, #18: Secondary blaze dip full width 0.05, #19: Blaze dip depth 0.0, #20: Continuum cubic component 0.0, #21: Continuum quartic component 0.0, #22: Continuum pentic component 0.0 ]) #23: Continuum hexic component # This one specific order is small and telluric dominated, start with greater stellar template power to ensure good fits if int(order) == 13: pars0[1] = 0.8 # Iterate over all A/B exposures for t in [0]: tag = tagsnight[t] beam = beamsnight[t] masterbeam = beam # Load synthetic telluric template generated during Step 1 # [:8] here is to ensure program works under Night_Split mode # Use instrumental profile dictionary corresponding to whether IGRINS mounting was loose or not if np.int(night[:8]) < 20180401 or np.int(night[:8]) > 20190531: IPpars = inparam.ips_tightmount_pars[args.band][masterbeam][order] else: IPpars = inparam.ips_loosemount_pars[args.band][masterbeam][order] if beam == 'A': antibeam = 'B' elif beam == 'B': antibeam = 'A' else: sys.exit('uhoh') A0loc = f'../Output/{args.targname}_{args.band}/A0Fits/{night[:8]}A0_{beam}treated_{args.band}.fits' try: hdulist = fits.open(A0loc) except IOError: logger.warning( f' --> No A0-fitted template for night {night}, skipping...') return wminibox, sminibox, flminibox_mod, flminibox_tel, flminibox_ste, contiminibox # Find corresponding table in fits file, given the tables do not go sequentially by order number due to multiprocessing in Step 1 num_orders = 0 for i in range(25): try: hdulist[i].columns[0].name[9:] num_orders += 1 except: continue fits_layer = [ i for i in np.arange(num_orders) + 1 if np.int(hdulist[i].columns[0].name[9:]) == order ][0] tbdata = hdulist[fits_layer].data flag = np.array(tbdata[f'ERRORFLAG{order}'])[0] # Check whether Telfit hit critical error in Step 1 for the chosen order with this night. If so, skip. if flag == 1: logger.warning( f' --> TELFIT ENCOUNTERED CRITICAL ERROR IN ORDER: {order} NIGHT: {night}, skipping...' ) return wminibox, sminibox, flminibox_mod, flminibox_tel, flminibox_ste, contiminibox watm = tbdata['WATM' + str(order)] satm = tbdata['SATM' + str(order)] a0contx = tbdata['X' + str(order)] continuum = tbdata['BLAZE' + str(order)] # Remove extra rows leftover from having columns of unequal length satm = satm[(watm != 0)] watm = watm[(watm != 0)] satm[( satm < 1e-4 )] = 0. # set very low points to zero so that they don't go to NaN when taken to an exponent by template power in fmodel_chi a0contx = a0contx[(continuum != 0)] continuum = continuum[(continuum != 0)] # Retrieve pixel bounds for where within each other significant telluric absorption is present. # If these bounds were not applied, analyzing some orders would give garbage fits. if args.band == 'K': if int(order) in [3, 13, 14]: bound_cut = inparam.bound_cut_dic[args.band][order] else: bound_cut = [150, 150] elif args.band == 'H': if int(order) in [6, 10, 11, 13, 14, 16, 17, 20, 21, 22]: bound_cut = inparam.bound_cut_dic[args.band][order] else: bound_cut = [150, 150] # Load target spectrum x, wave, s, u = init_fitsread(f'{inparam.inpath}/{night}/{beam}/', 'target', 'separate', night, order, tag, args.band, bound_cut) #------------------------------------------------------------------------------- # Execute S/N cut s2n = s / u if np.nanmedian(s2n) < np.float(args.SN_cut): logger.warning( ' --> Bad S/N {:1.3f} < {} for {}{} {}, SKIP'.format( np.nanmedian(s2n), args.SN_cut, night, beam, tag)) continue # Trim obvious outliers above the blaze (i.e. cosmic rays) nzones = 5 x = basicclip_above(x, s, nzones) wave = basicclip_above(wave, s, nzones) u = basicclip_above(u, s, nzones) s = basicclip_above(s, s, nzones) x = basicclip_above(x, s, nzones) wave = basicclip_above(wave, s, nzones) u = basicclip_above(u, s, nzones) s = basicclip_above(s, s, nzones) # Cut spectrum to within wavelength regions defined in input list s_piece = s[(x > xbounds[0]) & (x < xbounds[-1])] u_piece = u[(x > xbounds[0]) & (x < xbounds[-1])] wave_piece = wave[(x > xbounds[0]) & (x < xbounds[-1])] x_piece = x[(x > xbounds[0]) & (x < xbounds[-1])] # Trim stellar template to relevant wavelength range mwave_in, mflux_in = stellarmodel_setup(wave_piece, inparam.mwave0, inparam.mflux0) # Trim telluric template to relevant wavelength range satm_in = satm[(watm > np.min(wave_piece) * 1e4 - 11) & (watm < np.max(wave_piece) * 1e4 + 11)] watm_in = watm[(watm > np.min(wave_piece) * 1e4 - 11) & (watm < np.max(wave_piece) * 1e4 + 11)] # Make sure data is within telluric template range (shouldn't do anything) s_piece = s_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] u_piece = u_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] x_piece = x_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] wave_piece = wave_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] # Normalize continuum from A0 to flux scale of data continuum /= np.nanmedian(continuum) continuum *= np.nanpercentile(s_piece, 99) # -------------------------------------------------------------- par = pars0.copy() # Get initial guess for cubic wavelength solution from reduction pipeline f = np.polyfit(x_piece, wave_piece, 3) par9in = f[0] * 1e4 par8in = f[1] * 1e4 par7in = f[2] * 1e4 par6in = f[3] * 1e4 par[9] = par9in par[8] = par8in par[7] = par7in par[6] = par6in par[0] = initguesses - inparam.bvcs[ night + tag] # Initial RV with barycentric correction par[5] = IPpars[2] par[13] = IPpars[1] par[14] = IPpars[0] # Arrays defining parameter variations during optimization steps # | 0 1 2 3 | | ------ 4 ------ | | 5 | | 6 7 8 9 | |10 11 12| |13 14| |15 16 17 18 19 | |20 21 22 23 | dpars = { 'cont': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e7, 1, 1, 0, 0, 10., 20., 0.2, 50.0, 0.2, 1.0, 1.0, 1.0, 1.0 ]), 'twave': np.array([ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 1e-7, 0, 0, 0, 0, 0, 0., 0., 0.0, 0., 0.0, 0.0, 0.0, 0.0, 0.0 ]), 'ip': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0.0, 0., 0.0, 0.0, 0.0, 0.0, 0.0 ]), 's': np.array([ 5.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0.0, 0., 0.0, 0.0, 0.0, 0.0, 0.0 ]), 'v': np.array([ 0.0, 0.0, 0.0, 0.0, inparam.vsinivary, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0.0, 0., 0.0, 0.0, 0.0, 0.0, 0.0 ]), 'ts': np.array([ 5.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0.0, 0., 0.0, 0.0, 0.0, 0.0, 0.0 ]) } if masterbeam == 'B': dpars['cont'] = np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e7, 1, 1, 0, 0, 0., 0., 0.0, 0., 0.0, 1.0, 1.0, 1.0, 1.0 ]) # Use quadratic blaze correction for order 13; cubic for orders 6, 14, 21; quartic for orders 16 and 22 if args.band == 'H': if np.int(order) in [13]: dpars['cont'][20] = 0. dpars['cont'][21] = 0. dpars['cont'][22] = 0. dpars['cont'][23] = 0. elif np.int(order) in [6, 14, 21]: dpars['cont'][21] = 0. dpars['cont'][22] = 0. dpars['cont'][23] = 0. else: pass else: if np.int(order) in [3]: dpars['cont'][20] = 0. dpars['cont'][21] = 0. dpars['cont'][22] = 0. dpars['cont'][23] = 0. elif np.int(order) in [4, 5]: dpars['cont'][21] = 0. dpars['cont'][22] = 0. dpars['cont'][23] = 0. elif np.int(order) in [6]: dpars['cont'][22] = 0. dpars['cont'][23] = 0. else: pass continuum_in = rebin_jv(a0contx, continuum, x_piece, False) fitobj = fitobjs(s_piece, x_piece, u_piece, continuum_in, watm_in, satm_in, mflux_in, mwave_in, ast.literal_eval(inparam.maskdict[order]), masterbeam, np.array([], dtype=int)) #------------------------------------------------------------------------------- # Initialize an array that puts hard bounds on vsini and the instrumental resolution to make sure they do not diverge to unphysical values optimize = True par_in = par.copy() if masterbeam == 'B': hardbounds = [ par_in[4] - dpars['v'][4], par_in[4] + dpars['v'][4], par_in[5] - dpars['ip'][5], par_in[5] + dpars['ip'][5] ] else: hardbounds = [ par_in[4] - dpars['v'][4], par_in[4] + dpars['v'][4], par_in[5] - dpars['ip'][5], par_in[5] + dpars['ip'][5], par_in[15] - dpars['cont'][15], par_in[15] + dpars['cont'][15], par_in[16] - dpars['cont'][16], par_in[16] + dpars['cont'][16], 0., par_in[17] + dpars['cont'][17], par_in[18] - dpars['cont'][18], par_in[18] + dpars['cont'][18], 0., par_in[19] + dpars['cont'][19] ] if hardbounds[0] < 0.5: hardbounds[0] = 0.5 if hardbounds[2] < 1: hardbounds[2] = 1 # Begin optimization. Fit the blaze, the wavelength solution, the telluric template power and RV, the stellar template power and RV, the # zero point for the instrumental resolution, and the vsini of the star separately, iterating and cycling between each set of parameter fits. cycles = 4 optgroup = [ 'cont', 'twave', 'cont', 'ts', 'cont', 'twave', 's', 'cont', 'twave', 'ip', 'v', 'ip', 'v', 'twave', 's', 'twave', 'ts' ] nk = 1 for nc, cycle in enumerate(np.arange(cycles), start=1): if cycle == 0: parstart = par_in.copy() for optkind in optgroup: parfit_1 = optimizer(parstart, dpars[optkind], hardbounds, fitobj, optimize) parstart = parfit_1.copy() if args.debug == True: outplotter_23( parfit_1, fitobj, '{}_{}_{}_parfit_{}{}'.format(order, night, tag, nk, optkind), trk, inparam, args, step2or3, order) logger.debug(f'{order}_{tag}_{nk}_{optkind}:\n {parfit_1}') nk += 1 ## After first cycle, use best fit model to identify CRs/hot pixels if nc == 1: parfit = parfit_1.copy() fit, chi = fmod(parfit, fitobj) # Everywhere where data protrudes high above model, check whether slope surrounding protrusion is /\ and mask if sufficiently steep residual = fitobj.s / fit MAD = np.median(np.abs(np.median(residual) - residual)) CRmask = np.array( np.where(residual > np.median(residual) + 2 * MAD)[0]) CRmaskF = [] CRmask = list(CRmask) for hit in [0, len(fitobj.x) - 1]: if hit in CRmask: CRmaskF.append(hit) CRmask.remove(hit) CRmask = np.array(CRmask, dtype=np.int) CRmaskF = np.array(CRmaskF, dtype=np.int) for group in mit.consecutive_groups(CRmask): group = np.array(list(group)) if len(group) == 1: gL = group - 1 gR = group + 1 else: peaks = detect_peaks(fitobj.s[group]) if len(peaks) < 1: group = np.concatenate( (np.array([group[0] - 1]), group, np.array([group[-1] + 1]))) peaks = detect_peaks(fitobj.s[group]) if len(peaks) < 1: continue if len(peaks) > 1: continue gL = group[:peaks[0]] gR = group[peaks[0] + 1:] slopeL = (fitobj.s[gL + 1] - fitobj.s[gL]) / (fitobj.x[gL + 1] - fitobj.x[gL]) slopeR = (fitobj.s[gR] - fitobj.s[gR - 1]) / ( fitobj.x[gR] - fitobj.x[gR - 1]) try: if (np.min(slopeL) > 300) and ( np.max(slopeR) < -300) and len(group) < 6: CRmaskF = np.concatenate((CRmaskF, group)) except ValueError: if (slopeL > 300) and (slopeR < -300): CRmaskF = np.concatenate((CRmaskF, group)) fitobj = fitobjs(s_piece, x_piece, u_piece, continuum_in, watm_in, satm_in, mflux_in, mwave_in, ast.literal_eval(inparam.maskdict[order]), masterbeam, CRmaskF) parfit = parfit_1.copy() #------------------------------------------------------------------------------- # if best fit stellar template power is very low, throw out result if parfit[1] < 0.1: logger.warning(f' --> parfit[1] < 0.1, {night} parfit={parfit}') continue # if best fit stellar or telluric template powers are exactly equal to their starting values, fit failed, throw out result if parfit[1] == par_in[1] or parfit[3] == par_in[3]: logger.warning( f' --> parfit[1] == par_in[1] or parfit[3] == par_in[3], {night}' ) continue # if best fit model dips below zero at any point, we're to close to edge of blaze, fit may be comrpomised, throw out result smod, chisq = fmod(parfit, fitobj) if len(smod[(smod < 0)]) > 0: logger.warning(f' --> len(smod[(smod < 0)]) > 0, {night}') continue #------------------------------------------------------------------------------- # Compute model and divide for residual fullmodel, chisq = fmod(parfit, fitobj) # Set both stellar and telluric template powers to 0 to compute only continuum parcont = parfit.copy() parcont[1] = 0. parcont[3] = 0. contmodel, chisq = fmod(parcont, fitobj) # Set stellar tempalte power to 0 to compute only telluric, and vice versa parS = parfit.copy() parT = parfit.copy() parT[1] = 0. parS[3] = 0. stellmodel, chisq = fmod(parS, fitobj) tellmodel, chisq = fmod(parT, fitobj) # Divide everything by continuum model except residual dataflat = fitobj.s / contmodel modelflat = fullmodel / contmodel stellflat = stellmodel / contmodel tellflat = tellmodel / contmodel w = parfit[6] + parfit[7] * fitobj.x + parfit[8] * ( fitobj.x**2.) + parfit[9] * (fitobj.x**3.) wminibox[:len(w)] = w sminibox[:len(w)] = dataflat flminibox_mod[:len(w)] = modelflat flminibox_tel[:len(w)] = tellflat flminibox_ste[:len(w)] = stellflat contiminibox[:len(w)] = contmodel # residualbox[:len(w)] = residual # Save results in fits file c1 = fits.Column(name='wavelength', array=wminibox, format='D') c2 = fits.Column(name='s', array=sminibox, format='D') c3 = fits.Column(name='model_fl', array=flminibox_mod, format='D') c4 = fits.Column(name='tel_fl', array=flminibox_tel, format='D') c5 = fits.Column(name='ste_fl', array=flminibox_ste, format='D') c6 = fits.Column(name='conti_fl', array=contiminibox, format='D') cols = fits.ColDefs([c1, c2, c3, c4, c5, c6]) hdu_1 = fits.BinTableHDU.from_columns(cols) if order == firstorder: # If first time writing fits file, make up filler primary hdu bleh = np.ones((3, 3)) primary_hdu1 = fits.PrimaryHDU(bleh) hdul = fits.HDUList([primary_hdu1, hdu_1]) hdul.writeto(inparam.outpath + '/' + name + '/RVresultsRawBox_fit_wl_{}_{}_{}_{}.fits'.format( args.targname, args.band, night, tag)) else: hh = fits.open(inparam.outpath + '/' + name + '/RVresultsRawBox_fit_wl_{}_{}_{}_{}.fits'.format( args.targname, args.band, night, tag)) hh.append(hdu_1) hh.writeto(inparam.outpath + '/' + name + '/RVresultsRawBox_fit_wl_{}_{}_{}_{}.fits'.format( args.targname, args.band, night, tag), overwrite=True) return wminibox, sminibox, flminibox_mod, flminibox_tel, flminibox_ste, contiminibox
def rv_MPinst(args, inparam, orders, order_use, trk, step2or3, i): # Main function for RV fitting that will be threaded over by multiprocessing nights = inparam.nights night = nights[i] # current looped night order = order_use xbounds = inparam.xbounddict[order] if args.debug: print('Working on order {:02d}, night {:03d}/{:03d} ({}) PID:{}...'. format(int(order), i + 1, len(inparam.nights), night, mp.current_process().pid)) #------------------------------------------------------------------------------- # Collect initial RV guesses if type(inparam.initguesses) == dict: initguesses = inparam.initguesses[night] elif type(inparam.initguesses) == float: initguesses = inparam.initguesses else: sys.exit( 'ERROR! EXPECING SINGAL NUMBER OR FILE FOR INITGUESSES! QUITTING!') if np.isnan(initguesses) == True: logger.warning( f' --> Previous run of {night} found it inadequate, skipping...') return night, np.nan, np.nan # Collect relevant beam and filenum info tagsnight = [] beamsnight = np.array([]) for tag in inparam.tagsB[night]: tagsnight.append(tag) beamsnight = np.append(beamsnight, 'B') # Only do B exposures, and just use first B nodding masterbeam = 'B' beam = 'B' try: tag = tagsnight[0] except IndexError: logger.warning( f' --> No B nodding(frame) for night {night}, skipping...') return night, np.nan, np.nan #------------------------------------------------------------------------------- ### Initialize parameter array for optimization as well as half-range values for each parameter during the various steps of the optimization. ### Many of the parameters initialized here will be changed throughout the code before optimization and in between optimization steps. pars0 = np.array([ np.nan, # 0: The shift of the stellar template (km/s) [assigned later] 0.3, # 1: The scale factor for the stellar template 0.0, # 2: The shift of the telluric template (km/s) 0.6, # 3: The scale factor for the telluric template inparam.initvsini, # 4: vsini (km/s) np.nan, # 5: The instrumental resolution (FWHM) in pixels np.nan, # 6: Wavelength 0-pt np.nan, # 7: Wavelength linear component np.nan, # 8: Wavelength quadratic component np.nan, # 9: Wavelength cubic component 1.0, #10: Continuum zero point 0., #11: Continuum linear component 0., #12: Continuum quadratic component np.nan, #13: Instrumental resolution linear component np.nan, #14: Instrumental resolution quadratic component 0, #15: Blaze dip center location 0, #16: Blaze dip full width 0, #17: Blaze dip depth 0, #18: Secondary blaze dip full width 0, #19: Blaze dip depth 0.0, #20: Continuum cubic component 0.0, #21: Continuum quartic component 0.0, #22: Continuum pentic component 0.0 ]) #23: Continuum hexic component # This one specific order is small and telluric dominated, start with greater stellar template power to ensure good fits if int(order) == 13: pars0[1] = 0.8 # Load synthetic telluric template generated during Step 1 # [:8] here is to ensure program works under Night_Split mode A0loc = f'./Output/{args.targname}_{args.band}/A0Fits/{night[:8]}A0_{beam}treated_{args.band}.fits' try: hdulist = fits.open(A0loc) except IOError: logger.warning( f' --> No A0-fitted template for night {night}, skipping...') return night, np.nan, np.nan # Find corresponding table in fits file, given the tables do not go sequentially by order number due to multiprocessing in Step 1 num_orders = 0 for i in range(25): try: hdulist[i].columns[0].name[9:] num_orders += 1 except: continue fits_layer = [ i for i in np.arange(num_orders) + 1 if np.int(hdulist[i].columns[0].name[9:]) == order ][0] tbdata = hdulist[fits_layer].data flag = np.array(tbdata[f'ERRORFLAG{order}'])[0] # Check whether Telfit hit critical error in Step 1 for the chosen order with this night. If so, try another order. If all hit the error, skip the night. nexto = 0 ordertry = order while 1 == 1: fits_layer = [ i for i in np.arange(num_orders) + 1 if np.int(hdulist[i].columns[0].name[9:]) == ordertry ][0] tbdata = hdulist[fits_layer].data flag = np.array(tbdata[f'ERRORFLAG{ordertry}'])[0] if flag == 1: # If Telfit hit unknown critical error in Step 1, this order can't be used for this night. Try another. orderbad = ordertry ordertry = orders[nexto] logger.warning( f' --> TELFIT ENCOUNTERED CRITICAL ERROR IN ORDER: {orderbad} NIGHT: {night}, TRYING ORDER {ordertry} INSTEAD...' ) else: # All good, continue order = ordertry break nexto += 1 if nexto == len(orders): logger.warning( f' --> TELFIT ENCOUNTERED CRITICAL ERROR IN ALL ORDERS FOR NIGHT: {night}, skipping...' ) return night, np.nan, np.nan watm = tbdata['WATM' + str(order)] satm = tbdata['SATM' + str(order)] a0contx = tbdata['X' + str(order)] continuum = tbdata['BLAZE' + str(order)] # Remove extra rows leftover from having columns of unequal length satm = satm[(watm != 0)] watm = watm[(watm != 0)] satm[( satm < 1e-4 )] = 0. # set very low points to zero so that they don't go to NaN when taken to an exponent by template power in fmodel_chi a0contx = a0contx[(continuum != 0)] continuum = continuum[(continuum != 0)] # Use instrumental profile dictionary corresponding to whether IGRINS mounting was loose or not if (np.int(night[:8]) < 20180401) or (np.int(night[:8]) > 20190531): IPpars = inparam.ips_tightmount_pars[args.band][masterbeam][order] else: IPpars = inparam.ips_loosemount_pars[args.band][masterbeam][order] # Retrieve pixel bounds for where within each other significant telluric absorption is present. # If these bounds were not applied, analyzing some orders would give garbage fits. if args.band == 'K': if int(order) in [3, 13, 14]: bound_cut = inparam.bound_cut_dic[args.band][order] else: bound_cut = [150, 150] elif args.band == 'H': if int(order) in [6, 10, 11, 13, 14, 16, 17, 20, 21, 22]: bound_cut = inparam.bound_cut_dic[args.band][order] else: bound_cut = [150, 150] # Load target spectrum x, wave, s, u = init_fitsread(f'{inparam.inpath}/', 'target', 'combined' + str(masterbeam), night, order, inparam.tagsB[night][0], args.band, bound_cut) #------------------------------------------------------------------------------- # Execute S/N cut s2n = s / u if np.nanmedian(s2n) < np.float(args.SN_cut): logger.warning(' --> Bad S/N {:1.3f} < {} for {}{} {}... '.format( np.nanmedian(s2n), args.SN_cut, night, beam, tag)) pass # Trim obvious outliers above the blaze (i.e. cosmic rays) nzones = 5 x = basicclip_above(x, s, nzones) wave = basicclip_above(wave, s, nzones) u = basicclip_above(u, s, nzones) s = basicclip_above(s, s, nzones) x = basicclip_above(x, s, nzones) wave = basicclip_above(wave, s, nzones) u = basicclip_above(u, s, nzones) s = basicclip_above(s, s, nzones) # Cut spectrum to within wavelength regions defined in input list s_piece = s[(x > xbounds[0]) & (x < xbounds[-1])] u_piece = u[(x > xbounds[0]) & (x < xbounds[-1])] wave_piece = wave[(x > xbounds[0]) & (x < xbounds[-1])] x_piece = x[(x > xbounds[0]) & (x < xbounds[-1])] # Trim stellar template to relevant wavelength range mwave_in, mflux_in = stellarmodel_setup(wave_piece, inparam.mwave0, inparam.mflux0) # Trim telluric template to relevant wavelength range satm_in = satm[(watm > np.min(wave_piece) * 1e4 - 11) & (watm < np.max(wave_piece) * 1e4 + 11)] watm_in = watm[(watm > np.min(wave_piece) * 1e4 - 11) & (watm < np.max(wave_piece) * 1e4 + 11)] # Make sure data is within telluric template range (shouldn't do anything) s_piece = s_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] u_piece = u_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] x_piece = x_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] wave_piece = wave_piece[(wave_piece * 1e4 > np.min(watm_in) + 5) & (wave_piece * 1e4 < np.max(watm_in) - 5)] # Normalize continuum from A0 to flux scale of data continuum /= np.nanmedian(continuum) continuum *= np.nanpercentile(s_piece, 99) # -------------------------------------------------------------- par = pars0.copy() # Get initial guess for cubic wavelength solution from reduction pipeline f = np.polyfit(x_piece, wave_piece, 3) par9in = f[0] * 1e4 par8in = f[1] * 1e4 par7in = f[2] * 1e4 par6in = f[3] * 1e4 par[9] = par9in par[8] = par8in par[7] = par7in par[6] = par6in par[0] = initguesses - inparam.bvcs[ night + tag] # Initial RV with barycentric correction par[5] = IPpars[2] par[13] = IPpars[1] par[14] = IPpars[0] # Arrays defining parameter variations during optimization steps # Optimization will cycle twice. In the first cycle, the RVs can vary more than in the second. # | 0 1 2 3 | | ------ 4 ------ | | 5 | | 6 7 8 9 | |10 11 12| |13 14| |15 16 17 18 19| |20 21 22 23 | dpars1 = { 'cont': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e7, 1, 1, 0, 0, 0., 0., 0., 0., 0., 1.0, 1.0, 1.0, 1.0 ]), 'twave': np.array([ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 1e-7, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]), 'ip': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]), 's': np.array([ 20.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]), 'v': np.array([ 0.0, 0.0, 0.0, 0.0, inparam.vsinivary, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]) } dpars2 = { 'cont': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e7, 1, 1, 0, 0, 0., 0., 0., 0., 0., 1.0, 1.0, 1.0, 1.0 ]), 'twave': np.array([ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 1e-7, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]), 'ip': np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]), 's': np.array([ 5.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]), 'v': np.array([ 0.0, 0.0, 0.0, 0.0, inparam.vsinivary, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0, 0., 0., 0., 0., 0., 0.0, 0.0, 0.0, 0.0 ]) } # Use quadratic blaze correction for order 13; cubic for orders 6, 14, 21; quartic for orders 16 and 22 if args.band == 'H': if int(order) in [13]: dpars1['cont'][20] = 0. dpars1['cont'][21] = 0. dpars1['cont'][22] = 0. dpars1['cont'][23] = 0. dpars2['cont'][20] = 0. dpars2['cont'][21] = 0. dpars2['cont'][22] = 0. dpars2['cont'][23] = 0. elif int(order) in [6, 14, 21]: dpars1['cont'][21] = 0. dpars1['cont'][22] = 0. dpars1['cont'][23] = 0. dpars2['cont'][21] = 0. dpars2['cont'][22] = 0. dpars2['cont'][23] = 0. else: pass else: if np.int(order) in [3]: dpars1['cont'][20] = 0. dpars1['cont'][21] = 0. dpars1['cont'][22] = 0. dpars1['cont'][23] = 0. dpars2['cont'][20] = 0. dpars2['cont'][21] = 0. dpars2['cont'][22] = 0. dpars2['cont'][23] = 0. elif np.int(order) in [4, 5]: dpars1['cont'][21] = 0. dpars1['cont'][22] = 0. dpars1['cont'][23] = 0. dpars2['cont'][21] = 0. dpars2['cont'][22] = 0. dpars2['cont'][23] = 0. elif np.int(order) in [6]: dpars1['cont'][22] = 0. dpars1['cont'][23] = 0. dpars2['cont'][22] = 0. dpars2['cont'][23] = 0. else: pass continuum_in = rebin_jv(a0contx, continuum, x_piece, False) fitobj = fitobjs(s_piece, x_piece, u_piece, continuum_in, watm_in, satm_in, mflux_in, mwave_in, ast.literal_eval(inparam.maskdict[order]), masterbeam, np.array([], dtype=int)) #------------------------------------------------------------------------------- # Initialize an array that puts hard bounds on vsini and the instrumental resolution to make sure they do not diverge to unphysical values optimize = True par_in = par.copy() hardbounds = [ par_in[4] - dpars1['v'][4], par_in[4] + dpars1['v'][4], par_in[5] - dpars1['ip'][5], par_in[5] + dpars1['ip'][5] ] if hardbounds[0] < 0.5: hardbounds[0] = 0.5 if hardbounds[2] < 1: hardbounds[2] = 1 # Begin optimization. Fit the blaze, the wavelength solution, the telluric template power and RV, the stellar template power and RV, the # zero point for the instrumental resolution, and the vsini of the star separately, iterating and cycling between each set of parameter fits. cycles = 2 optgroup = [ 'cont', 'twave', 'cont', 's', 'cont', 'twave', 's', 'cont', 'twave', 'ip', 'v', 'ip', 'v', 'twave', 's', 'twave', 's' ] nk = 1 for nc, cycle in enumerate(np.arange(cycles), start=1): if cycle == 0: parstart = par_in.copy() dpars = dpars1 else: dpars = dpars2 for optkind in optgroup: parfit_1 = optimizer(parstart, dpars[optkind], hardbounds, fitobj, optimize) parstart = parfit_1.copy() if args.debug == True: outplotter_23( parfit_1, fitobj, '{}_{}_{}_parfit_{}{}'.format(order, night, tag, nk, optkind), trk, inparam, args, step2or3, order) logger.debug(f'{order}_{tag}_{nk}_{optkind}:\n {parfit_1}') nk += 1 parfit = parfit_1.copy() #------------------------------------------------------------------------------- # if best fit stellar template power is very low, throw out result if parfit[1] < 0.1: logger.warning( f' --> Stellar template power is low for {night}! Data likely being misfit! Throwing out result...' ) return night, np.nan, np.nan # if best fit stellar or telluric template powers are exactly equal to their starting values, fit failed, throw out result if parfit[1] == par_in[1] or parfit[3] == par_in[3]: logger.warning( f' --> Stellar or telluric template powers have not budged from starting values for {night}! Fit is broken! Optimizer bounds may be unfeasible, or chi-squared may be NaN? Throwing out result...' ) return night, np.nan, np.nan # if best fit model dips below zero at any point, we're to close to edge of blaze, fit may be comrpomised, throw out result smod, chisq = fmod(parfit, fitobj) if len(smod[(smod < 0)]) > 0: logger.warning( f' --> Best fit model dips below 0 for {night}! May be too close to edge of blaze, throwing out result...' ) return night, np.nan, np.nan #------------------------------------------------------------------------------- if args.plotfigs == True: parfitS = parfit.copy() parfitS[3] = 0 parfitT = parfit.copy() parfitT[1] = 0 outplotter_23(parfitS, fitobj, 'parfitS_{}_{}_{}'.format(order, night, tag), trk, inparam, args, step2or3, order) outplotter_23(parfitT, fitobj, 'parfitT_{}_{}_{}'.format(order, night, tag), trk, inparam, args, step2or3, order) outplotter_23(parfit, fitobj, 'parfit_{}_{}_{}'.format(order, night, tag), trk, inparam, args, step2or3, order) rv0 = parfit[0] rvsmini = rv0 + inparam.bvcs[night + tag] + rv0 * inparam.bvcs[night + tag] / ( 2.99792458e5**2) # Barycentric correction vsinismini = parfit[4] bestguess = np.round(rvsmini, 5) vsinimini = np.round(vsinismini, 5) return night, bestguess, vsinimini