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
Beispiel #3
0
def MPinst(args, inparam, i, order0, order):
    #    nights = inparam.nights
    nights = i[0]
    print('Working on {} band, order {}/{}, night {} ...'.format(
        args.band, order, len(order0), i[0]))
    night = str(nights)

    # 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 [14]:
            bound_cut = inparam.bound_cut_dic[args.band][order]
        else:
            bound_cut = [150, 150]

    elif args.band == 'H':
        if int(order) in [13, 14, 16, 20]:
            bound_cut = inparam.bound_cut_dic[args.band][order]
        else:
            bound_cut = [150, 150]
    ### Load relevant A0 spectrum
    x, a0wavelist, a0fluxlist, u = init_fitsread(
        inparam.inpath, 'A0', 'separate', night, order,
        f'{int(inparam.tags[night]):04d}', args.band, bound_cut)

    #-------------------------------------------------------------------------------
    nzones = 12
    a0wavelist = basicclip_above(a0wavelist, a0fluxlist, nzones)
    a0x = basicclip_above(x, a0fluxlist, nzones)
    a0u = basicclip_above(u, a0fluxlist, nzones)
    a0fluxlist = basicclip_above(a0fluxlist, a0fluxlist, nzones)
    a0wavelist = basicclip_above(a0wavelist, a0fluxlist, nzones)
    a0x = basicclip_above(a0x, a0fluxlist, nzones)
    a0u = basicclip_above(a0u, a0fluxlist, nzones)
    a0fluxlist = basicclip_above(a0fluxlist, a0fluxlist, nzones)

    # Normalize
    a0fluxlist /= np.median(a0fluxlist)

    # Compute rough blaze fn estimate
    continuum = A0cont(a0wavelist, a0fluxlist, night, order)
    a0contwave = a0wavelist.copy()
    a0masterwave = a0wavelist.copy()
    a0masterwave *= 1e4

    # Trim stellar template to relevant wavelength range
    mwave_in, mflux_in = stellarmodel_setup(a0wavelist, inparam.mwave0,
                                            inparam.mflux0)

    # Trim telluric template to relevant wavelength range
    satm_in = inparam.satm[(inparam.watm > min(a0wavelist) * 1e4 - 11)
                           & (inparam.watm < max(a0wavelist) * 1e4 + 11)]
    watm_in = inparam.watm[(inparam.watm > min(a0wavelist) * 1e4 - 11)
                           & (inparam.watm < max(a0wavelist) * 1e4 + 11)]

    # Get initial guess for cubic wavelength solution from reduction pipeline
    f = np.polyfit(a0x, a0wavelist, 3)
    par9in = f[0] * 1e4
    par8in = f[1] * 1e4
    par7in = f[2] * 1e4
    par6in = f[3] * 1e4

    # Determine whether IGRINS mounting was loose or night for the night in question
    if (int(night) < 20180401) or (int(night) > 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.
    parA0 = np.array([
        0.0,  # 0: The shift of the stellar template (km/s)
        0.0,  # 1: The scale factor for the stellar template
        0.0,  # 2: The shift of the telluric  template (km/s)
        1.0,  # 3: The scale factor for the telluric template
        0.0,  # 4: vsini (km/s)
        IPpars[2],  # 5: The instrumental resolution (FWHM) in pixels
        par6in,  # 6: Wavelength 0-pt
        par7in,  # 7: Wavelength linear component
        par8in,  # 8: Wavelength quadratic component
        par9in,  # 9: Wavelength cubic component
        1.0,  #10: Continuum zero point
        0.,  #11: Continuum linear component
        0.,  #12: Continuum quadratic component
        IPpars[1],  #13: Insrumental resolution linear component
        IPpars[0]
    ])  #14: Insrumental resolution quadratic component

    # Make sure data is within telluric template range (shouldn't do anything)
    a0fluxlist = a0fluxlist[(a0wavelist * 1e4 > min(watm_in) + 5)
                            & (a0wavelist * 1e4 < max(watm_in) - 5)]
    a0u = a0u[(a0wavelist * 1e4 > min(watm_in) + 5)
              & (a0wavelist * 1e4 < max(watm_in) - 5)]
    a0x = a0x[(a0wavelist * 1e4 > min(watm_in) + 5)
              & (a0wavelist * 1e4 < max(watm_in) - 5)]
    continuum = continuum[(a0wavelist * 1e4 > min(watm_in) + 5)
                          & (a0wavelist * 1e4 < max(watm_in) - 5)]
    a0wavelist = a0wavelist[(a0wavelist * 1e4 > min(watm_in) + 5)
                            & (a0wavelist * 1e4 < max(watm_in) - 5)]

    # Define main spectrum
    s = a0fluxlist.copy()
    x = a0x.copy()
    u = a0u.copy()

    # Collect all fit variables into one class
    fitobj = fitobjs(s, x, u, continuum, watm_in, satm_in, mflux_in, mwave_in,
                     [])

    # Arrays defining parameter variations during optimization steps
    dpar_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])
    dpar_wave = np.array([
        0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 1e-7, 0, 0, 0, 0,
        0
    ])
    dpar = np.array(
        [0.0, 0.0, 5.0, 3.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0, 1e4, 1, 1, 0, 0])
    dpar_st = np.array(
        [0.0, 0.0, 5.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 1e4, 1, 1, 0, 0])
    dpar_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])

    #-------------------------------------------------------------------------------

    # 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 = parA0.copy()
    hardbounds = [
        par_in[4] - dpar[4], par_in[4] + dpar[4], par_in[5] - dpar[5],
        par_in[5] + dpar[5]
    ]
    if hardbounds[0] < 0:
        hardbounds[0] = 0
    if hardbounds[3] < 0:
        hardbounds[3] = 1

    # Begin optimization.
    # For every pre-Telfit spectral fit, first fit just template strength/rv/continuum, then just wavelength solution, then template/continuum again, then ip,
    # then finally wavelength. Normally would fit for all but wavelength at the end, but there's no need for the pre-Telfit fit, since all we want
    # is a nice wavelength solution to feed into Telfit.
    parfit_1 = optimizer(par_in, dpar_st, hardbounds, fitobj, optimize)
    parfit_2 = optimizer(parfit_1, dpar_wave, hardbounds, fitobj, optimize)
    parfit_3 = optimizer(parfit_2, dpar_st, hardbounds, fitobj, optimize)
    parfit_4 = optimizer(parfit_3, dpar, hardbounds, fitobj, optimize)
    parfit = optimizer(parfit_4, dpar_wave, hardbounds, fitobj, optimize)

    #-------------------------------------------------------------------------------

    # Get best fit wavelength solution
    a0w_out_fit = parfit[6] + parfit[7] * x + parfit[8] * (
        x**2.) + parfit[9] * (x**3.)

    # Trim stellar template to new relevant wavelength range
    mwave_in, mflux_in = stellarmodel_setup(a0w_out_fit / 1e4, inparam.mwave0,
                                            inparam.mflux0)

    # Feed this new wavelength solution into Telfit. Returns high-res synthetic telluric template, parameters of that best fit, and blaze function best fit
    watm1, satm1, telfitparnames, telfitpars, a0contwave, continuum = telfitter(
        a0w_out_fit, a0fluxlist, a0u, inparam, night, order, args)

    #-------------------------------------------------------------------------------
    #-------------------------------------------------------------------------------

    # If Telfit encountered error (details in Telfitter.py), skip night/order combo
    if len(watm1) == 1:
        logger.warning(
            f'TELFIT ENCOUNTERED CRITICAL ERROR IN ORDER: {order} NIGHT: {night}'
        )

        # Write out table to fits header with errorflag = 1
        c0 = fits.Column(name=f'ERRORFLAG{order}',
                         array=np.array([1]),
                         format='K')
        cols = fits.ColDefs([c0])
        hdu_1 = fits.BinTableHDU.from_columns(cols)

        # If first time writing fits file, make up filler primary hdu
        if order == firstorder:
            bleh = np.ones((3, 3))
            primary_hdu = fits.PrimaryHDU(bleh)
            hdul = fits.HDUList([primary_hdu, hdu_1])
            hdul.writeto('{}/{}A0_treated_{}.fits'.format(
                inparam.outpath, night, args.band))
        else:
            hh = fits.open('{}/{}A0_treated_{}.fits'.format(
                inparam.outpath, night, args.band))
            hh.append(hdu_1)
            hh.writeto('{}/{}A0_treated_{}.fits'.format(
                inparam.outpath, night, args.band),
                       overwrite=True)

    else:  # If Telfit exited normally, proceed.

        #  Save best blaze function fit
        a0contwave /= 1e4
        continuum = rebin_jv(a0contwave, continuum, a0wavelist, False)

        # Fit the A0 again using the new synthetic telluric template.
        # This allows for any tweaks to the blaze function fit that may be necessary.
        fitobj = fitobjs(s, x, u, continuum, watm1, satm1, mflux_in, mwave_in,
                         [])

        parfit_1 = optimizer(par_in, dpar_st, hardbounds, fitobj, optimize)
        parfit_2 = optimizer(parfit_1, dpar_wave, hardbounds, fitobj, optimize)
        parfit_3 = optimizer(parfit_2, dpar_st, hardbounds, fitobj, optimize)
        parfit_4 = optimizer(parfit_3, dpar_wave, hardbounds, fitobj, optimize)
        parfit = optimizer(parfit_4, dpar, hardbounds, fitobj, optimize)

        #-------------------------------------------------------------------------------

        a0w_out = parfit[6] + parfit[7] * x + parfit[8] * (
            x**2.) + parfit[9] * (x**3.)
        cont_adj = parfit[10] + parfit[11] * x + parfit[12] * (x**2.)

        c2 = rebin_jv(a0contwave * 1e4, continuum, a0w_out, False)
        c2 /= np.median(c2)
        cont_save = c2 * cont_adj

        # Write out table to fits file with errorflag = 0
        c0 = fits.Column(name='ERRORFLAG' + str(order),
                         array=np.array([0]),
                         format='K')
        cc = fits.Column(name='WAVE_pretel' + str(order),
                         array=a0w_out_fit,
                         format='D')
        c1 = fits.Column(name='WAVE' + str(order), array=a0w_out, format='D')
        c2 = fits.Column(name='BLAZE' + str(order),
                         array=cont_save,
                         format='D')
        c3 = fits.Column(name='X' + str(order), array=a0x, format='D')
        c4 = fits.Column(name='INTENS' + str(order),
                         array=a0fluxlist,
                         format='D')
        c5 = fits.Column(name='SIGMA' + str(order), array=a0u, format='D')
        c6 = fits.Column(name='WATM' + str(order), array=watm1, format='D')
        c7 = fits.Column(name='SATM' + str(order), array=satm1, format='D')
        c8 = fits.Column(name='TELFITPARNAMES' + str(order),
                         array=np.array(telfitparnames),
                         format='8A')
        c9 = fits.Column(name='TELFITPARS' + str(order),
                         array=telfitpars,
                         format='D')
        cols = fits.ColDefs([c0, cc, c1, c2, c3, c4, c5, c6, c7, c8, c9])
        hdu_1 = fits.BinTableHDU.from_columns(cols)

        #        if order == 1: # If first time writing fits file, make up filler primary hdu
        bleh = np.ones((3, 3))
        primary_hdu = fits.PrimaryHDU(bleh)
        hdul = fits.HDUList([primary_hdu, hdu_1])
        # hdul.writeto(inparam.outpath+'/'+night+'A0_treated_{}_order{}.fits'.format(args.band, order),overwrite=True)
        hdul.writeto('{}/{}A0_treated_{}_order{}.fits'.format(
            inparam.outpath, night, args.band, order),
                     overwrite=True)
def MPinst(i, order0, order):
    #    nights = inparam.nights
    nights = i[0]
    print('Working on {} band, order {}/{}, night {} ...'.format(
        args.band, order, len(order0), i[0]))
    night = str(nights)

    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]
    ### Load relevant A0 spectrum
    # x (list of wavelength used position)
    if args.band == 'K':
        if order == 11:
            bound_cut = [200, 100]
        elif order == 12:
            bound_cut = [900, 300]
        elif order == 13:
            bound_cut = [200, 400]
        elif order == 14:
            bound_cut = [150, 300]
        else:
            bound_cut = [150, 100]
    elif args.band == 'H':
        if order == 10:
            bound_cut = [250, 150]  #ok
        elif order == 11:
            bound_cut = [600, 150]
        elif order == 13:
            bound_cut = [200, 600]  #ok
        elif order == 14:
            bound_cut = [700, 100]
        elif order == 16:
            bound_cut = [400, 100]
        elif order == 17:
            bound_cut = [1000, 100]
        elif order == 20:
            bound_cut = [500, 150]
        elif (order == 7) or (order == 8) or (order == 9) or (order == 12) or (
                order == 15) or (order == 18) or (order == 19):
            bound_cut = [500, 500]
        else:
            bound_cut = [150, 100]
    x, a0wavelist, a0fluxlist, u = init_fitsread(
        inparam.inpath, 'A0', 'separate', night, order,
        '{:04d}'.format(int(inparam.tags[night])), args.band, bound_cut)

    nzones = 12
    a0wavelist = basicclip_above(a0wavelist, a0fluxlist, nzones)
    a0x = basicclip_above(x, a0fluxlist, nzones)
    a0u = basicclip_above(u, a0fluxlist, nzones)
    a0fluxlist = basicclip_above(a0fluxlist, a0fluxlist, nzones)

    # do twice?
    a0wavelist = basicclip_above(a0wavelist, a0fluxlist, nzones)
    a0x = basicclip_above(a0x, a0fluxlist, nzones)
    a0u = basicclip_above(a0u, a0fluxlist, nzones)
    a0fluxlist = basicclip_above(a0fluxlist, a0fluxlist, nzones)

    # Normalize
    a0fluxlist /= np.median(a0fluxlist)

    # Compute rough blaze fn estimate
    continuum = A0cont(a0wavelist, a0fluxlist, night, order)
    a0contwave = a0wavelist.copy()
    a0masterwave = a0wavelist.copy()
    a0masterwave *= 1e4

    # Trim stellar template to relevant wavelength range
    mwave_in, mflux_in = stellarmodel_setup(a0wavelist, inparam.mwave0,
                                            inparam.mflux0)

    # Trim telluric template to relevant wavelength range
    satm_in = inparam.satm[(inparam.watm > min(a0wavelist) * 1e4 - 11)
                           & (inparam.watm < max(a0wavelist) * 1e4 + 11)]
    watm_in = inparam.watm[(inparam.watm > min(a0wavelist) * 1e4 - 11)
                           & (inparam.watm < max(a0wavelist) * 1e4 + 11)]

    ### 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 sunspot spectrum (km/s)
        1.0,  # 1: The scale factor for the sunspot spectrum
        0.0,  # 2: The shift of the telluric spectrum (km/s)
        1.0,  # 3: The scale factor for the telluric spectrum
        0.0,  # 4: vsini (km/s)
        IPpars[2],  # 5: The instrumental resolution (FWHM) in pixels
        2.29315012e+04,  # 6: Wavelength 0-pt
        1.75281163e-01,  # 7: Wavelength linear component
        -9.92637874e-06,  # 8: Wavelength quadratic component
        0,  # 9: Wavelength cubic component
        1.0,  #10: Continuum zero point
        0.,  #11: Continuum linear component
        0.,  #12: Continuum quadratic component
        IPpars[1],  #13: IP linear component
        IPpars[0],  #14: IP quadratic component
        0.0
    ])  #15: Differential Rotation Coefficient

    # Save a copy of initial parameter array. Make sure stellar template isn't being used.
    parA0 = pars0.copy()
    parA0[0] = 0.
    parA0[1] = 0.

    # Cut target spectrum to be within telluric template wavelengths (should be unncessary)
    a0fluxlist = a0fluxlist[(a0wavelist * 1e4 > min(watm_in) + 5)
                            & (a0wavelist * 1e4 < max(watm_in) - 5)]
    a0u = a0u[(a0wavelist * 1e4 > min(watm_in) + 5)
              & (a0wavelist * 1e4 < max(watm_in) - 5)]
    a0x = a0x[(a0wavelist * 1e4 > min(watm_in) + 5)
              & (a0wavelist * 1e4 < max(watm_in) - 5)]
    a0wavelist = a0wavelist[(a0wavelist * 1e4 > min(watm_in) + 5)
                            & (a0wavelist * 1e4 < max(watm_in) - 5)]

    ##Set initial wavelength guess. If order not in initwavedict, fit cubic polynomial to wavelengths given in file
    # 6: Wavelength 0-pt
    # 7: Wavelength linear component
    # 8: Wavelength quadratic component
    # 9: Wavelength cubic component

    f = np.polyfit(a0x, a0wavelist, 3)
    parA0[9] = f[0] * 1e4
    parA0[8] = f[1] * 1e4
    parA0[7] = f[2] * 1e4
    parA0[6] = f[3] * 1e4

    # Define main spectrum parameters
    s = a0fluxlist.copy()
    x = a0x.copy()
    u = a0u.copy()

    # Collect all fit variables into one class
    #global fitobj, optimize;
    fitobj = fitobjs(s, x, u, continuum, watm_in, satm_in, mflux_in, mwave_in)

    # Arrays defining parameter variations during optimization steps
    dpar_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, 0])
    dpar_wave = np.array([
        0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 5.00000e-5, 1e-7, 0, 0, 0, 0,
        0, 0
    ])
    dpar = np.array(
        [0.0, 0.0, 5.0, 3.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0, 1e4, 1, 1, 0, 0, 0])
    dpar_st = np.array(
        [0.0, 0.0, 5.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 1e4, 1, 1, 0, 0, 0])
    dpar_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])

    #-------------------------------------------------------------------------------
    # For every pre-Telfit spectral fit, first fit just template strength/rv/continuum, then just wavelength soln, then template/continuum again, then ip,
    # then finally wavelength. Normally would fit for all but wavelength at the end, but there's no need for the pre-Telfit fit, since all we want
    # is the wavelength solution.

    optimize = True
    par_in = parA0.copy()
    hardbounds = [
        par_in[4] - dpar[4], par_in[4] + dpar[4], par_in[5] - dpar[5],
        par_in[5] + dpar[5], par_in[15] - dpar[15], par_in[15] + dpar[15]
    ]
    if hardbounds[0] < 0:
        hardbounds[0] = 0
    if hardbounds[3] < 0:
        hardbounds[3] = 1


#        if args.plotfigs == True:#
#            outplotter(targname,par_in,fitobj,'{}_{}_{}_1'.format(label,night,tag))

    parfit_1 = optimizer(par_in, dpar_st, hardbounds, fitobj, optimize)
    parfit_2 = optimizer(parfit_1, dpar_wave, hardbounds, fitobj, optimize)
    parfit_3 = optimizer(parfit_2, dpar_st, hardbounds, fitobj, optimize)
    parfit_4 = optimizer(parfit_3, dpar, hardbounds, fitobj, optimize)
    parfit = optimizer(parfit_4, dpar_wave, hardbounds, fitobj, optimize)

    # if inparam.plotfigs == True:
    #     outplotter(parfit, fitobj, '{}_{}_1'.format(label,night), 0)
    #-------------------------------------------------------------------------------
    # Get fitted wavelength solution
    a0w_out_fit = parfit[6] + parfit[7] * x + parfit[8] * (
        x**2.) + parfit[9] * (x**3.)

    # Trim stellar template to relevant wavelength range
    mwave_in, mflux_in = stellarmodel_setup(a0w_out_fit / 1e4, inparam.mwave0,
                                            inparam.mflux0)

    # Using this new wavelength solution, get Telfit'd telluric template, parameters of that best fit, and blaze fn best fit
    watm1, satm1, telfitparnames, telfitpars, a0contwave, continuum = telfitter(
        a0w_out_fit, a0fluxlist, a0u, inparam, night, order, args)
    # watm1, satm1 from Telfit, fack one.

    if (args.band == 'H') & (
        (order == 7) | (order == 8) | (order == 9) | (order == 12) |
        (order == 15) | (order == 18) | (order == 19)
    ):  # If Telfit encountered error mentioned in Telfitter.py, skip night/order combo
        # Write out table to fits header with errorflag = 1
        c0 = fits.Column(name='ERRORFLAG' + str(order),
                         array=np.array([1]),
                         format='K')
        cols = fits.ColDefs([c0])
        hdu_1 = fits.BinTableHDU.from_columns(cols)
        #        if order == order0[0]: # If first time writing fits file, make up filler primary hdu
        bleh = np.ones((3, 3))
        primary_hdu = fits.PrimaryHDU(bleh)
        hdul = fits.HDUList([primary_hdu, hdu_1])
        hdul.writeto('{}/{}A0_treated_{}_order{}.fits'.format(
            inparam.outpath, night, args.band, order),
                     overwrite=True)

    elif len(
            watm1
    ) == 1:  # If Telfit encountered error mentioned in Telfitter.py, skip night/order combo

        print('TELFIT ENCOUNTERED CRITICAL ERROR, ORDER ' + str(order) +
              ' NIGHT ' + str(night))

        # Write out table to fits header with errorflag = 1
        c0 = fits.Column(name='ERRORFLAG' + str(order),
                         array=np.array([1]),
                         format='K')
        cols = fits.ColDefs([c0])
        hdu_1 = fits.BinTableHDU.from_columns(cols)
        #        if order == order0[0]: # If first time writing fits file, make up filler primary hdu
        bleh = np.ones((3, 3))
        primary_hdu = fits.PrimaryHDU(bleh)
        hdul = fits.HDUList([primary_hdu, hdu_1])
        hdul.writeto('{}/{}A0_treated_{}_order{}.fits'.format(
            inparam.outpath, night, args.band, order),
                     overwrite=True)

    else:

        a0contwave /= 1e4
        continuum = rebin_jv(a0contwave, continuum, a0wavelist, False)

        # Fit whole A0 again to get even better wave soln to use for a0contwave and tweak blaze fn fit as
        # needed with quadratic adjustment
        fitobj = fitobjs(s, x, u, continuum, watm1, satm1, mflux_in, mwave_in)

        parfit_1 = optimizer(par_in, dpar_st, hardbounds, fitobj, optimize)
        parfit_2 = optimizer(parfit_1, dpar_wave, hardbounds, fitobj, optimize)
        parfit_3 = optimizer(parfit_2, dpar_st, hardbounds, fitobj, optimize)
        parfit_4 = optimizer(parfit_3, dpar_wave, hardbounds, fitobj, optimize)
        parfit = optimizer(parfit_4, dpar, hardbounds, fitobj, optimize)

        if inparam.plotfigs == True:
            fig, axes = plt.subplots(1,
                                     1,
                                     figsize=(5, 3),
                                     facecolor='white',
                                     dpi=300)
            axes.plot(fitobj.x,
                      parfit[5] + parfit[13] * (fitobj.x) + parfit[14] *
                      (fitobj.x**2),
                      '-k',
                      lw=0.5,
                      label='data',
                      alpha=.6)

            axes.tick_params(axis='both',
                             labelsize=4.5,
                             right=True,
                             top=True,
                             direction='in')
            axes.set_ylabel(r'Normalized Flux',
                            size=5,
                            style='normal',
                            family='sans-serif')
            axes.set_xlabel('Wavelength',
                            size=5,
                            style='normal',
                            family='sans-serif')
            axes.legend(fontsize=4, edgecolor='white')
            fig.savefig('{}/figs_{}/IP_{}_{}.png'.format(
                inparam.outpath, args.band, order, night),
                        bbox_inches='tight',
                        format='png',
                        overwrite=True)

            outplotter(parfit, fitobj,
                       'Post_parfit_{}_{}'.format(order, night))

        a0w_out = parfit[6] + parfit[7] * x + parfit[8] * (
            x**2.) + parfit[9] * (x**3.)
        cont_adj = parfit[10] + parfit[11] * x + parfit[12] * (x**2.)

        c2 = rebin_jv(a0contwave * 1e4, continuum, a0w_out, False)
        c2 /= np.median(c2)
        cont_save = c2 * cont_adj

        # Write out table to fits file with errorflag = 0
        c0 = fits.Column(name='ERRORFLAG' + str(order),
                         array=np.array([0]),
                         format='K')
        cc = fits.Column(name='WAVE_pretel' + str(order),
                         array=a0w_out_fit,
                         format='D')
        c1 = fits.Column(name='WAVE' + str(order), array=a0w_out, format='D')
        c2 = fits.Column(name='BLAZE' + str(order),
                         array=cont_save,
                         format='D')
        c3 = fits.Column(name='X' + str(order), array=a0x, format='D')
        c4 = fits.Column(name='INTENS' + str(order),
                         array=a0fluxlist,
                         format='D')
        c5 = fits.Column(name='SIGMA' + str(order), array=a0u, format='D')
        c6 = fits.Column(name='WATM' + str(order), array=watm1, format='D')
        c7 = fits.Column(name='SATM' + str(order), array=satm1, format='D')
        c8 = fits.Column(name='TELFITPARNAMES' + str(order),
                         array=np.array(telfitparnames),
                         format='8A')
        c9 = fits.Column(name='TELFITPARS' + str(order),
                         array=telfitpars,
                         format='D')
        cols = fits.ColDefs([c0, cc, c1, c2, c3, c4, c5, c6, c7, c8, c9])
        hdu_1 = fits.BinTableHDU.from_columns(cols)

        #        if order == 1: # If first time writing fits file, make up filler primary hdu
        bleh = np.ones((3, 3))
        primary_hdu = fits.PrimaryHDU(bleh)
        hdul = fits.HDUList([primary_hdu, hdu_1])
        # hdul.writeto(inparam.outpath+'/'+night+'A0_treated_{}_order{}.fits'.format(args.band, order),overwrite=True)
        hdul.writeto('{}/A0_Fits/{}A0_treated_{}_order{}.fits'.format(
            inparam.outpath, night, args.band, order),
                     overwrite=True)
Beispiel #5
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