Esempio n. 1
0
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)
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 7
0
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