Ejemplo n.º 1
0
def convert_mask_to_pkl(dp,maskname):
    """This is a continuity function to deal with updating mask fits files to pkl files without
    disturbing anyone's work-flow..."""
    import sys
    from pathlib import Path
    import pickle
    import tayph.util as ut
    from tayph.vartests import typetest,dimtest,lentest
    import pdb
    import numpy as np
    import astropy.io.fits as fits

    inpath_auto = Path(dp)/(maskname+'_auto.fits')
    inpath_man = Path(dp)/(maskname+'_manual.fits')
    outpath_auto = Path(dp)/(maskname+'_auto.pkl')
    outpath_man = Path(dp)/(maskname+'_manual.pkl')
    ut.check_path(inpath_auto,exists=True)
    ut.check_path(inpath_man,exists=True)

    mask_auto = fits.getdata(inpath_auto)
    mask_man = fits.getdata(inpath_man)

    list_of_mask_auto = []
    list_of_mask_man = []

    for i in range(len(mask_auto)):
        list_of_mask_auto.append(mask_auto[i])

    for i in range(len(mask_man)):
        list_of_mask_man.append(mask_man[i])

    with open(outpath_auto, 'wb') as f: pickle.dump(list_of_mask_auto,f)
    with open(outpath_man, 'wb') as f: pickle.dump(list_of_mask_man,f)
Ejemplo n.º 2
0
def dRV(dp):
    """This program calculates the change in radial velocity in km/s for the
    planet in the data sequence provided in dp, the data-path. dp starts in the
    root folder,i.e. it starts with data/projectname/, and it ends with a slash.

    Example: dv=dRV('data/Kelt-9/night1/')
    The output is an array with length N, corresponding to N exposures.
    The change in radial velocity is calculated using the first derivative of the
    formula for RV, multiplied by the exposure time provided in obs_times.
    The answer is provided in units of km/s change within each exposure."""
    from tayph.vartests import typetest
    import numpy as np
    import astropy.units as u
    from astropy.io import ascii
    import pdb
    import tayph.util as ut
    dp=ut.check_path(dp,exists=True)
    obsp=ut.check_path(dp/'obs_times',exists=True)

    d=ascii.read(obsp,comment="#")
    #Texp=d['exptime'].astype('float')
    Texp=d['col3'].data#astype('float')
    vorb=v_orb(dp)
    p=phase(dp)
    P=paramget('P',dp)
    i=paramget('inclination',dp)
    typetest(P,float,'P in dRV()')
    typetest(i,float,'i in dRV()')

    dRV=vorb*np.cos(2.0*np.pi*p)*2.0*np.pi/((P*u.d).to('s').value)*np.sin(np.radians(i))
    return abs(dRV*Texp)
Ejemplo n.º 3
0
def load_columns_from_file(dp,maskname,mode='strict'):
    """
    This loads the list of lists of columns back into memory after having been
    saved by a call of write_columns_to_file() below.
    """
    import tayph.util as ut
    from tayph.vartests import typetest
    import pickle
    import os
    from pathlib import Path
    ut.check_path(dp)
    typetest(maskname,str,'maskname in load_columns_from_file()')
    typetest(mode,str,'mode in load_columns_from_file()')
    outpath=Path(dp)/(maskname+'_columns.pkl')

    if os.path.isfile(outpath) ==  False:
         if mode == 'strict':
             raise Exception('FileNotFoundError in reading columns from file: Column file named %s do not exist at %s.' % (maskname,dp))
         else:
             print('---No previously saved manual mask exists. User will start a new mask.')
             return([])
    else:
        print(f'------Loading previously saved manual mask {str(outpath)}.')
        pickle_in = open(outpath,"rb")
        return(pickle.load(pickle_in))
Ejemplo n.º 4
0
def write_mask_to_file(dp,maskname,list_of_masks_auto,list_of_masks_manual=[]):
    import sys
    from pathlib import Path
    import pickle
    import tayph.util as ut
    from tayph.vartests import typetest,dimtest,lentest
    import pdb
    import numpy as np
    ut.check_path(dp)
    typetest(maskname,str,'maskname in write_mask_to_file()')
    typetest(list_of_masks_auto,list,'list_of_masks_auto in write_mask_to_file()')
    typetest(list_of_masks_manual,list,'list_of_masks_manual in write_mask_to_file()')
    lentest(list_of_masks_auto,len(list_of_masks_manual),'list_of_masks_auto in '
    'write_mask_to_file()')


    for i in range(len(list_of_masks_auto)):
        dimtest(list_of_masks_auto[i],np.shape(list_of_masks_manual[i]),'list_of_masks_auto in '
        'write_mask_to_file()')
    outpath=Path(dp)/maskname
    if len(list_of_masks_auto) == 0 and len(list_of_masks_manual) == 0:
        print('RuntimeError in write_mask_to_file: Both lists of masks are emtpy!')
        sys.exit()

    print(f'---Saving lists of auto and manual masks to {str(outpath)}')

    if len(list_of_masks_auto) > 0:
        with open(str(outpath)+'_auto.pkl', 'wb') as f: pickle.dump(list_of_masks_auto,f)
        # ut.save_stack(str(outpath)+'_auto.fits',list_of_masks_auto)
    if len(list_of_masks_manual) > 0:
        with open(str(outpath)+'_manual.pkl', 'wb') as f: pickle.dump(list_of_masks_manual,f)
Ejemplo n.º 5
0
def get_model(name, library='models/library', root='models'):
    """This program queries a model from a library file, with predefined models
    for use in model injection, cross-correlation and plotting. These models have
    a standard format. They are 2 rows by N points, where N corresponds to the
    number of wavelength samples. The first row contains the wavelengths, the
    second the associated flux values. The library file has two columns that are
    formatted as follows:

    modelname  modelpath
    modelname  modelpath
    modelname  modelpath

    modelpath starts in the models/ subdirectory.
    Set the root variable if a location is required other than the ./models directory.

    Example call:
    wlm,fxm = get_model('WASP-121_TiO',library='models/testlib')
    """

    from tayph.vartests import typetest, dimtest
    from tayph.util import check_path
    from astropy.io import fits
    from pathlib import Path
    import errno
    import os
    typetest(name, str, 'name in mod.get_model()')
    library = check_path(library, exists=True)

    #First open the library file.
    f = open(library, 'r')
    x = f.read().splitlines()  #Read everything into a big string array.
    f.close()
    n_lines = len(x)  #Number of models in the library.
    models = {}  #This will contain the model names.

    for i in range(0, n_lines):
        line = x[i].split()
        value = (line[1])
        models[line[0]] = value

    try:
        modelpath = Path(models[name])
    except KeyError:
        raise KeyError(
            f'Model {name} is not present in library at {str(library)}'
        ) from None
    if str(modelpath)[0] != '/':  #Test if this is an absolute path.
        root = check_path(root, exists=True)
        modelpath = root / modelpath
    try:
        modelarray = fits.getdata(
            modelpath
        )  #Two-dimensional array with wl on the first row and flux on the second row.
    except FileNotFoundError:
        raise FileNotFoundError(
            f'Model file {modelpath} from library {str(library)} does not exist.'
        )
    dimtest(modelarray, [2, 0])
    return (modelarray[0, :], modelarray[1, :])
Ejemplo n.º 6
0
def read_telluric_transmission_from_file(inpath):
    import pickle
    import tayph.util as ut
    print(f'------Reading telluric transmission from {inpath}')
    ut.check_path(inpath, exists=True)
    pickle_in = open(inpath, "rb")
    return (pickle.load(pickle_in)
            )  #This is a tuple that can be unpacked into 2 elements.
Ejemplo n.º 7
0
def write_telluric_transmission_to_file(wls, T, fxc, outpath):
    """This saves a list of wl arrays and a corresponding list of transmission-spectra
    to a pickle file, to be read by the function below."""
    import pickle
    import tayph.util as ut
    ut.check_path(outpath)
    print(f'------Saving teluric transmission to {outpath}')
    with open(outpath, 'wb') as f:
        pickle.dump((wls, T, fxc), f)
Ejemplo n.º 8
0
def apply_mask_from_file(dp,maskname,list_of_orders):
    import astropy.io.fits as fits
    import numpy as np
    import os.path
    import sys
    import tayph.util as ut
    from tayph.vartests import typetest,dimtest
    from pathlib import Path
    import pickle
    ut.check_path(dp)
    typetest(maskname,str,'maskname in write_mask_to_file()')
    typetest(list_of_orders,list,'list_of_orders in apply_mask_from_file()')

    N = len(list_of_orders)

    inpath_auto = Path(dp)/(maskname+'_auto.pkl')
    inpath_man = Path(dp)/(maskname+'_manual.pkl')

    if os.path.isfile(inpath_auto) ==  False and os.path.isfile(inpath_man) == False:
        raise Exception(f'FileNotFoundError in apply_mask_from_file: Both mask files named '
        f'{maskname} do not exist at {str(dp)}. Rerun with make_maske = True.')

    #At this point either of the mask files is determined to exist.
    #Apply the masks to the orders, by adding. This works because the mask is zero
    #everywhere, apart from the NaNs, and x+0=x, while x+NaN = NaN.


    if os.path.isfile(inpath_auto) ==  True:
        print(f'------Applying sigma_clipped mask from {inpath_auto}')
        # cube_of_masks_auto = fits.getdata(inpath_auto)
        with open(inpath_auto,"rb") as f:
            list_of_masks_auto = pickle.load(f)
        Nm = len(list_of_masks_auto)
        err = f'ERROR in apply_mask_from_file: List_of_orders and list_of_masks_auto do not have '
        f'the same length ({N} vs {Nm}), meaning that the number of orders provided and the number '
        'of orders onto which the masks were created are not the same. This could have happened if '
        'you copy-pased mask_auto from one dataset to another. This is not recommended anyway, as '
        'bad pixels / outliers are expected to be in different locations in different datasets.'
        if Nm != N:
            raise Exception(err)
        #Checks have passed. Add the mask to the list of orders.
        for i in range(N):
            list_of_orders[i]+=list_of_masks_auto[i]

    if os.path.isfile(inpath_man) ==  True:
        print(f'------Applying manually defined mask from {inpath_man}')
        # cube_of_masks_man = fits.getdata(inpath_man)
        with open(inpath_man,"rb") as f:
            list_of_masks_man = pickle.load(f)
        Nm = len(list_of_masks_man)
        err = f'ERROR in apply_mask_from_file: List_of_orders and list_of_masks_auto do not have the same length ({N} vs {Nm}), meaning that the number of orders provided and the number of orders onto which the masks were created are not the same. This could have happened if you copy-pased mask_auto from one dataset to another. This is not recommended anyway, as bad pixels / outliers are expected to be in different locations in different datasets.'
        if Nm != N:
            raise Exception(err)
        for i in range(N):
            list_of_orders[i]+=list_of_masks_man[i]
    return(list_of_orders)
Ejemplo n.º 9
0
def read_shadow(dp, shadowname, rv, ccf):
    """
    This reads  shadow parameters from a pickle file generated by the fit_doppler_
    model class below, and evaluates it on the user-supplied rv and ccf (the latter of which is
    just for shape-purposes and the mask defined during the creation of the shadow model.
    """
    import pickle
    import os.path
    import sys
    from pathlib import Path
    import tayph.util as ut
    from tayph.vartests import typetest, dimtest
    import tayph.system_parameters as sp
    import numpy as np
    typetest(shadowname, str, 'shadowname in read_shadow().')
    typetest(rv, np.ndarray, 'rv in read_shadow()')
    typetest(ccf, np.ndarray, 'ccf in read_shadow()')

    dp = ut.check_path(dp, exists=True)
    inpath = dp / (shadowname + '.pkl')
    ut.check_path(inpath, exists=True)
    RVp = sp.RV(dp)
    pickle_in = open(inpath, "rb")
    d = pickle.load(pickle_in)
    params = d["fit_params"]
    T = d["Transit"]
    p = d["Phases"]
    aRstar = d["aRstar"]
    vsys = d["vsys"]
    i = d["inclination"]
    n_c = d["n_components"]
    maskHW = d["maskHW"]
    offset = d["offset"]  #Offset of second component.
    S = d["S"]
    D = d["D"]
    RVp = sp.RV(dp) + vsys
    mask = mask_ccf_near_RV(rv, ccf, RVp, maskHW)
    return (evaluate_shadow(params,
                            rv,
                            ccf,
                            T,
                            p,
                            aRstar,
                            vsys,
                            i,
                            n_c,
                            offset,
                            S,
                            D,
                            leastsq=False), mask)
Ejemplo n.º 10
0
def retrieve_output_molecfit(path):
    """This collects the actual transmission model created after a pass through
    molecfit is completed"""
    import astropy.io.fits as fits
    import os.path
    import sys
    from pathlib import Path
    import tayph.util as ut
    file = Path(str(Path(path)) + '_out_tac.fits')
    ut.check_path(file, exists=True)

    with fits.open(file) as hdul:
        wl = hdul[1].data['lambda']
        fx = hdul[1].data['flux']
        trans = hdul[1].data['mtrans']
    return (wl, fx, trans)
Ejemplo n.º 11
0
def write_columns_to_file(dp,maskname,list_of_selected_columns):
    """
    This dumps the list of list of columns that are manually selected by the
    user to a pkl file for loading at a later time. This is done to allow the user
    to resume work on a saved mask.
    """
    import pickle
    from pathlib import Path
    import tayph.util as ut
    from tayph.vartests import typetest
    ut.check_path(dp)
    typetest(maskname,str,'maskname in write_columns_to_file()')
    typetest(list_of_selected_columns,list,'list_of_selected_columns in write_columns_to_file()')
    outpath=Path(dp)/(maskname+'_columns.pkl')
    print(f'---Saving list of masked columns to {str(outpath)}')
    with open(outpath, 'wb') as f: pickle.dump(list_of_selected_columns, f)
Ejemplo n.º 12
0
def write_mask_to_file(dp,maskname,list_of_masks_auto,list_of_masks_manual=[]):
    import sys
    from pathlib import Path
    import tayph.util as ut
    from tayph.vartests import typetest,dimtest
    import pdb
    ut.check_path(dp)
    typetest(maskname,str,'maskname in write_mask_to_file()')
    typetest(list_of_masks_auto,list,'list_of_masks_auto in write_mask_to_file()')
    typetest(list_of_masks_manual,list,'list_of_masks_manual in write_mask_to_file()')
    dimtest(list_of_masks_auto,[len(list_of_masks_manual),0,0],'list_of_masks_auto in write_mask_to_file()')
    outpath=Path(dp)/maskname
    if len(list_of_masks_auto) == 0 and len(list_of_masks_manual) == 0:
        print('RuntimeError in write_mask_to_file: Both lists of masks are emtpy!')
        sys.exit()

    print(f'---Saving lists of auto and manual masks to {str(outpath)}')
    if len(list_of_masks_auto) > 0:
        ut.save_stack(str(outpath)+'_auto.fits',list_of_masks_auto)
    if len(list_of_masks_manual) > 0:
        ut.save_stack(str(outpath)+'_manual.fits',list_of_masks_manual)
Ejemplo n.º 13
0
    def __init__(self, fig, ax, rv, ccf, primer, dp, outname):
        """We initialize with a figure object, three axis objects (in a list)
        an rv axis, the CCF, the user-generated prior on the doppler shadow (the two)
        points, and the pointer to the data in order to load physics."""
        import tayph.system_parameters as sp
        import numpy as np
        import pdb
        import scipy.interpolate as interpol
        import tayph.functions as fun
        import tayph.util as ut
        import sys
        #Upon initialization, we pass the keywords onto self.
        nexp = np.shape(ccf)[0]
        nrv = np.shape(ccf)[1]
        self.rv = rv
        self.ccf = ccf
        self.p = sp.phase(dp)
        if len(self.p) != nexp:
            print('ERROR IN FIT_DOPPLER_MODEL __INIT__:')
            print('The height of the CCF does not match nexp.')
            sys.exit()
        transit = sp.transit(dp) - 1.0
        self.T = abs(transit / max(abs(transit)))
        self.ax = ax
        self.aRstar = sp.paramget('aRstar', dp)
        self.vsys = sp.paramget('vsys', dp)
        self.RVp = sp.RV(dp) + self.vsys
        self.inclination = sp.paramget('inclination', dp)
        self.n_components = 1
        self.maskHW = 10.0  #Default masking half-width
        self.offset = 0.0
        self.D = 0  #Initial degree of polynomial fitting.
        self.S = 0
        self.dp = ut.check_path(dp, exists=True)
        self.outpath = self.dp / (outname + '.pkl')
        #Translate the pivot to RV-phase points.
        #Need to interpolate on phase only, because the primer was already defined in RV space.
        p_i = interpol.interp1d(np.arange(nexp, dtype=float), self.p)
        p1 = float(p_i(primer[0][1]))
        p2 = float(p_i(primer[1][1]))
        v1 = primer[0][0]
        v2 = primer[1][0]

        self.primer = [[v1, p1], [v2, p2]]
        self.fit_spinorbit()
        # self.primer = a*self.rv+b
        self.v_star_primer = fun.local_v_star(self.p, self.aRstar,
                                              self.inclination, self.vsini_fit,
                                              self.l_fit) + self.vsys
        # ax[0].plot(v_star_primer,fun.findgen(nexp),'--',color='black',label='Spin-orbit fit to primer')
        self.mask_ccf()
        self.fit_model()
Ejemplo n.º 14
0
def transit(dp):
    """This code uses Ians astro python routines for the approximate Mandel &
    Agol transit lightcurve to produce the predicted transit lightcurve for the
    planet described by the configfile located at dp/config.
    This all assumes a circular orbit.
    ===========
    Derivation:
    ===========
    occultnonlin_small(z,p, cn) is the algorithm of the Mandel&Agol derivation.
    z = d/R_star, where d is the distance of the planet center to the LOS to the
    center of the star.
    sin(alpha) = d/a, with a the orbital distance (semi-major axis).
    so sin(alpha)*a/Rstar = d/a*a/Rstar = d/Rstar = z.
    a/Rstar happens to be a quantity that is well known from the transit light-
    curve. So z = sin(2pi phase)*a/Rstar. But this is in the limit of i = 90.

    From Cegla 2016 it follows that z = sqrt(xp^2 + yp^2). These are given
    as xp = a/Rstar sin(2pi phase) and yp = -a/Rstar * cos(2pi phase) * cos(i).

    The second quantity, p, is Rp/Rstar, also well known from the transit light-
    curve.

    cn is a four-element vector with the nonlinear limb darkening coefficients.
    If a shorter sequence is entered, the later values will be set to zero.
    By default I made it zero; i.e. the injected model does not take into
    account limb-darkening.
    """
    from tayph.vartests import typetest
    import tayph.util as ut
    import tayph.iansastropy as iap
    import numpy as np
    import pdb
    dp=ut.check_path(dp)
    p=phase(dp)
    a_Rstar=paramget('aRstar',dp)
    Rp_Rstar=paramget('RpRstar',dp)
    i=paramget('inclination',dp)
    typetest(a_Rstar,float,'Rp_Rstar')
    typetest(a_Rstar,float,'a_Rstar')
    typetest(i,float,'i')

    xp=np.sin(p*2.0*np.pi)*a_Rstar
    yp=np.cos(p*2.0*np.pi)*np.cos(np.radians(i))*a_Rstar
    z=np.sqrt(xp**2.0 + yp**2.0)
    transit=iap.occultnonlin_small(z,Rp_Rstar,[0.0,0.0])
    return transit
Ejemplo n.º 15
0
def check_dp(dp):
    """
    This is a helper function that checks for the presence of a config file
    and an obs_times file at path dp. It also checks that dp is a Path object and returns
    it as such.
    """
    from tayph.util import check_path
    from pathlib import Path
    check_path(dp)
    if isinstance(dp,str):
        dp = Path(dp)
    p1=dp/'obs_times'
    p2=dp/'config'
    check_path(p1,exists=True)
    check_path(p2,exists=True)
    return(dp)
Ejemplo n.º 16
0
def RV(dp,vorb=None,vsys=False):
    """This program calculates the radial velocity in km/s for the planet in the
    data sequence provided in dp, the data-path. dp starts in the root folder,
    i.e. it starts with data/projectname/, and it ends with a slash.

    Example: v=RV('data/Kelt-9/night1/')
    The output is an array with length N, corresponding to N exposures.
    The radial velocity is provided in km/s."""
    import tayph.util as ut
    import numpy as np
    from tayph.vartests import typetest
    dp=ut.check_path(dp)
    p=phase(dp)
    i=paramget('inclination',dp)
    typetest(i,float,'i')
    if vorb == None:
        vorb=v_orb(dp)
    typetest(vorb,float,'vorb in sp.RV')
    rv=vorb*np.sin(2.0*np.pi*p)*np.sin(np.radians(i))

    if vsys == True:
        vs=paramget('vsys',dp)
        rv+=vs
    return rv#In km/s.
Ejemplo n.º 17
0
def read_harpslike(inpath, filelist, mode, read_s1d=True):
    """
    This reads a folder of HARPS or HARPSN data. Input is a list of filepaths and the mode (HARPS
    or HARPSN).
    """

    if mode == 'HARPS':
        catkeyword = 'HIERARCH ESO DPR CATG'
        bervkeyword = 'HIERARCH ESO DRS BERV'
        thfilekeyword = 'HIERARCH ESO DRS CAL TH FILE'
        Zstartkeyword = 'HIERARCH ESO TEL AIRM START'
        Zendkeyword = 'HIERARCH ESO TEL AIRM END'
    elif mode == 'HARPSN':
        catkeyword = 'OBS-TYPE'
        bervkeyword = 'HIERARCH TNG DRS BERV'
        thfilekeyword = 'HIERARCH TNG DRS CAL TH FILE'
        Zstartkeyword = 'AIRMASS'
        Zendkeyword = 'AIRMASS'  #These are the same because HARPSN doesnt have start and end keywords.
        #Down there, the airmass is averaged, so there is no problem in taking the average of the same number.
    else:
        raise ValueError(
            f"Error in read_harpslike: mode should be set to HARPS or HARPSN ({mode})"
        )

    #The following variables define lists in which all the necessary data will be stored.
    framename = []
    header = []
    s1dhdr = []
    obstype = []
    texp = np.array([])
    date = []
    mjd = np.array([])
    s1dmjd = np.array([])
    npx = np.array([])
    norders = np.array([])
    e2ds = []
    s1d = []
    wave1d = []
    airmass = np.array([])
    berv = np.array([])
    wave = []
    # wavefile_used = []
    for i in range(len(filelist)):
        if filelist[i].endswith('e2ds_A.fits'):
            print(f'------{filelist[i]}', end="\r")
            hdul = fits.open(inpath / filelist[i])
            data = copy.deepcopy(hdul[0].data)
            hdr = hdul[0].header
            hdul.close()
            del hdul[0].data
            if hdr[catkeyword] == 'SCIENCE':
                framename.append(filelist[i])
                header.append(hdr)
                obstype.append(hdr[catkeyword])
                texp = np.append(texp, hdr['EXPTIME'])
                date.append(hdr['DATE-OBS'])
                mjd = np.append(mjd, hdr['MJD-OBS'])
                npx = np.append(npx, hdr['NAXIS1'])
                norders = np.append(norders, hdr['NAXIS2'])
                e2ds.append(data)
                berv = np.append(berv, hdr[bervkeyword])
                airmass = np.append(
                    airmass, 0.5 * (hdr[Zstartkeyword] + hdr[Zendkeyword])
                )  #This is an approximation where we take the mean airmass.
                # if nowave == True:
                # wavefile_used.append(hdr[thfilekeyword])
                #Record which wavefile was used by the pipeline to
                #create the wavelength solution.
                wavedata = ut.read_wave_from_e2ds_header(
                    hdr, mode=mode) / 10.0  #convert to nm.
                wave.append(wavedata)
                # if filelist[i].endswith('wave_A.fits'):
                #     print(filelist[i]+' (wave)')
                #     if nowave == True:
                #         warnings.warn(" in read_e2ds: nowave was set to True but a wave_A file was detected. This wave file is now ignored in favor of the header.",RuntimeWarning)
                #     else:
                #         wavedata=fits.getdata(inpath/filelist[i])
                #         wave.append(wavedata)

                if read_s1d:
                    s1d_path = inpath / Path(
                        str(filelist[i]).replace('e2ds_A.fits', 's1d_A.fits'))
                    ut.check_path(
                        s1d_path,
                        exists=True)  #Crash if the S1D doesn't exist.
                    # if filelist[i].endswith('s1d_A.fits'):
                    hdul = fits.open(s1d_path)
                    data_1d = copy.deepcopy(hdul[0].data)
                    hdr1d = hdul[0].header
                    hdul.close()
                    del hdul
                    # if hdr[catkeyword] == 'SCIENCE':
                    s1d.append(data_1d)
                    if mode == 'HARPSN':  #In the case of HARPS-N we need to convert the units of the
                        #elevation and provide a UTC keyword.
                        hdr1d['TELALT'] = np.degrees(float(hdr1d['EL']))
                        hdr1d['UTC'] = (float(hdr1d['MJD-OBS']) %
                                        1.0) * 86400.0
                    s1dhdr.append(hdr1d)
                    s1dmjd = np.append(s1dmjd, hdr1d['MJD-OBS'])
                    berv1d = hdr1d[bervkeyword]
                    if berv1d != hdr[bervkeyword]:
                        wrn_msg = (
                            'WARNING in read_harpslike(): BERV correction of s1d file is not'
                            f'equal to that of the e2ds file. {berv1d} vs {hdr[bervkeyword]}'
                        )
                        ut.tprint(wrn_msg)
                    gamma = (1.0 -
                             (berv1d * u.km / u.s / const.c).decompose().value
                             )  #Doppler factor BERV.
                    wave1d.append(
                        (hdr1d['CDELT1'] * fun.findgen(len(data_1d)) +
                         hdr1d['CRVAL1']) * gamma)

    #Check that all exposures have the same number of pixels, and clip s1ds if needed.
    # min_npx1d = int(np.min(np.array(npx1d)))
    # if np.sum(np.abs(np.array(npx1d)-npx1d[0])) != 0:
    #     warnings.warn("in read_e2ds when reading HARPS data: Not all s1d files have the same number of pixels. This could have happened if the pipeline has extracted one or two extra pixels in some exposures but not others. The s1d files will be clipped to the smallest length.",RuntimeWarning)
    #     for i in range(len(s1d)):
    #         wave1d[i]=wave1d[i][0:min_npx1d]
    #         s1d[i]=s1d[i][0:min_npx1d]
    #         npx1d[i]=min_npx1d
    output = {
        'wave': wave,
        'e2ds': e2ds,
        'header': header,
        'wave1d': wave1d,
        's1d': s1d,
        's1dhdr': s1dhdr,
        'mjd': mjd,
        'date': date,
        'texp': texp,
        'obstype': obstype,
        'framename': framename,
        'npx': npx,
        'norders': norders,
        'berv': berv,
        'airmass': airmass,
        's1dmjd': s1dmjd
    }
    return (output)
Ejemplo n.º 18
0
def clean_ccf(rv, ccf, ccf_e, dp):
    """
    This routine normalizes the CCF fluxes and subtracts the average out of
    transit CCF, using the transit lightcurve as a mask.


    Parameters
    ----------

    rv : np.ndarray
        The radial velocity axis

    ccf : np.ndarray
        The CCF with second dimension matching the length of rv.

    ccf_e : np.ndarray
        The error on ccf.

    dp : str or path-like
        The datapath of the present dataset, to establish which exposures in ccf
        are in or out of transit.

    Returns
    -------

    ccf_n : np.ndarray
        The transit-lightcurve normalised CCF.

    ccf_ne : np.ndarray
        The error on ccf_n

    ccf_nn : np.ndarray
        The CCF relative to the out-of-transit time-averaged, if sufficient (>25%
        of the time-series) out of transit exposures were available. Otherwise, the
        average over the entire time-series is used.

    ccf_ne : np.array
        The error on ccf_nn.


    """

    import numpy as np
    import tayph.functions as fun
    import tayph.util as ut
    from matplotlib import pyplot as plt
    import pdb
    import math
    import tayph.system_parameters as sp
    import tayph.operations as ops
    import astropy.io.fits as fits
    import sys
    import copy
    from tayph.vartests import typetest, dimtest, nantest

    typetest(rv, np.ndarray, 'rv in clean_ccf()')
    typetest(ccf, np.ndarray, 'ccf in clean_ccf')
    typetest(ccf_e, np.ndarray, 'ccf_e in clean_ccf')
    dp = ut.check_path(dp)
    dimtest(ccf, [0, len(rv)])
    dimtest(ccf_e, [0, len(rv)])
    nantest(rv, 'rv in clean_ccf()')
    nantest(ccf, 'ccf in clean_ccf()')
    nantest(ccf_e, 'ccf_e in clean_ccf()')
    #ADD PARAMGET DV HERE.

    transit = sp.transit(dp)
    # transitblock = fun.rebinreform(transit,len(rv))

    Nrv = int(math.floor(len(rv)))

    baseline_ccf = np.hstack((ccf[:,
                                  0:int(0.25 * Nrv)], ccf[:,
                                                          int(0.75 * Nrv):]))
    baseline_ccf_e = np.hstack(
        (ccf_e[:, 0:int(0.25 * Nrv)], ccf_e[:, int(0.75 * Nrv):]))
    baseline_rv = np.hstack((rv[0:int(0.25 * Nrv)], rv[int(0.75 * Nrv):]))
    meanflux = np.median(
        baseline_ccf, axis=1
    )  #Normalize the baseline flux, but away from the signal of the planet.
    meanflux_e = 1.0 / len(baseline_rv) * np.sqrt(
        np.nansum(baseline_ccf_e**2.0, axis=1))  #1/N times sum of squares.
    #I validated that this is approximately equal to ccf_e/sqrt(N).
    meanblock = fun.rebinreform(meanflux, len(rv))
    meanblock_e = fun.rebinreform(meanflux_e, len(rv))

    ccf_n = ccf / meanblock.T
    ccf_ne = np.abs(ccf_n) * np.sqrt(
        (ccf_e / ccf)**2.0 + (meanblock_e.T / meanblock.T)**
        2.0)  #R=X/Z -> dR = R*sqrt( (dX/X)^2+(dZ/Z)^2 )
    #I validated that this is essentially equal to ccf_e/meanblock.T; as expected because the error on the mean spectrum is small compared to ccf_e.

    if np.sum(transit == 1) == 0:
        print(
            '------WARNING in Cleaning: The data contains only in-transit exposures.'
        )
        print('------The mean ccf is taken over the entire time-series.')
        meanccf = np.nanmean(ccf_n, axis=0)
        meanccf_e = 1.0 / len(transit) * np.sqrt(
            np.nansum(ccf_ne**2.0,
                      axis=0))  #I validated that this is approximately equal
        #to sqrt(N)*ccf_ne, where N is the number of out-of-transit exposures.
    elif np.sum(transit == 1) <= 0.25 * len(transit):
        print(
            '------WARNING in Cleaning: The data contains very few (<25%) out of transit exposures.'
        )
        print('------The mean ccf is taken over the entire time-series.')
        meanccf = np.nanmean(ccf_n, axis=0)
        meanccf_e = 1.0 / len(transit) * np.sqrt(
            np.nansum(ccf_ne**2.0,
                      axis=0))  #I validated that this is approximately equal
        #to sqrt(N)*ccf_ne, where N is the number of out-of-transit exposures.
    if np.min(transit) == 1.0:
        print(
            '------WARNING in Cleaning: The data is not predicted to contain in-transit exposures.'
        )
        print(
            f'------If you expect to be dealing with transit-data, please check the ephemeris '
            f'at {dp}')
        print('------The mean ccf is taken over the entire time-series.')
        meanccf = np.nanmean(ccf_n, axis=0)
        meanccf_e = 1.0 / len(transit) * np.sqrt(
            np.nansum(ccf_ne**2.0,
                      axis=0))  #I validated that this is approximately equal
        #to sqrt(N)*ccf_ne, where N is the number of out-of-transit exposures.
    else:
        meanccf = np.nanmean(ccf_n[transit == 1.0, :], axis=0)
        meanccf_e = 1.0 / np.sum(transit == 1) * np.sqrt(
            np.nansum(ccf_ne[transit == 1.0, :]**2.0,
                      axis=0))  #I validated that this is approximately equal
        #to sqrt(N)*ccf_ne, where N is the number of out-of-transit exposures.

    meanblock2 = fun.rebinreform(meanccf, len(meanflux))
    meanblock2_e = fun.rebinreform(meanccf_e, len(meanflux))

    ccf_nn = ccf_n / meanblock2  #MAY NEED TO DO SUBTRACTION INSTEAD TOGETHER W. NORMALIZATION OF LIGHTCURVE. SEE ABOVE.
    ccf_nne = np.abs(
        ccf_n / meanblock2) * np.sqrt((ccf_ne / ccf_n)**2.0 +
                                      (meanblock2_e / meanblock2)**2.0)
    #I validated that this error is almost equal to ccf_ne/meanccf

    #ONLY WORKS IF LIGHTCURVE MODEL IS ACCURATE, i.e. if Euler observations are available.
    # print("---> WARNING IN CLEANING.CLEAN_CCF(): NEED TO ADD A FUNCTION THAT YOU CAN NORMALIZE BY THE LIGHTCURVE AND SUBTRACT INSTEAD OF DIVISION!")
    return (ccf_n, ccf_ne, ccf_nn - 1.0, ccf_nne)
Ejemplo n.º 19
0
def run_instance(p):
    """This runs the entire cross correlation analysis cascade."""
    import numpy as np
    from astropy.io import fits
    import astropy.constants as const
    import astropy.units as u
    from matplotlib import pyplot as plt
    import os.path
    import scipy.interpolate as interp
    import pylab
    import pdb
    import os.path
    import os
    import sys
    import glob
    import distutils.util
    import pickle
    import copy
    from pathlib import Path

    import tayph.util as ut
    import tayph.operations as ops
    import tayph.functions as fun
    import tayph.system_parameters as sp
    import tayph.tellurics as telcor
    import tayph.masking as masking
    import tayph.models as models
    from tayph.ccf import xcor, clean_ccf, filter_ccf, construct_KpVsys
    from tayph.vartests import typetest, notnegativetest, nantest, postest, typetest_array, dimtest
    import tayph.shadow as shadow
    # from lib import analysis
    # from lib import cleaning
    # from lib import masking as masking
    # from lib import shadow as shadow
    # from lib import molecfit as telcor

    #First parse the parameter dictionary into required variables and test them.
    typetest(p, dict, 'params in run_instance()')

    dp = Path(p['dp'])
    ut.check_path(dp, exists=True)

    modellist = p['modellist']
    templatelist = p['templatelist']
    model_library = p['model_library']
    template_library = p['template_library']
    typetest(modellist, [str, list], 'modellist in run_instance()')
    typetest(templatelist, [str, list], 'templatelist in run_instance()')
    typetest(model_library, str, 'model_library in run_instance()')
    typetest(template_library, str, 'template_library in run_instance()')
    ut.check_path(model_library, exists=True)
    ut.check_path(template_library, exists=True)
    if type(modellist) == str:
        modellist = [modellist]  #Force this to be a list
    if type(templatelist) == str:
        templatelist = [templatelist]  #Force this to be a list
    typetest_array(modellist, str, 'modellist in run_instance()')
    typetest_array(templatelist, str, 'modellist in run_instance()')

    shadowname = p['shadowname']
    maskname = p['maskname']
    typetest(shadowname, str, 'shadowname in run_instance()')
    typetest(maskname, str, 'shadowname in run_instance()')

    RVrange = p['RVrange']
    drv = p['drv']
    f_w = p['f_w']
    resolution = sp.paramget('resolution', dp)
    typetest(RVrange, [int, float], 'RVrange in run_instance()')
    typetest(drv, [int, float], 'drv in run_instance()')
    typetest(f_w, [int, float], 'f_w in run_instance()')
    typetest(resolution, [int, float], 'resolution in run_instance()')
    nantest(RVrange, 'RVrange in run_instance()')
    nantest(drv, 'drv in run_instance()')
    nantest(f_w, 'f_w in run_instance()')
    nantest(resolution, 'resolution in run_instance()')
    postest(RVrange, 'RVrange in run_instance()')
    postest(drv, 'drv in run_instance()')
    postest(resolution, 'resolution in run_instance()')
    notnegativetest(f_w, 'f_w in run_instance()')

    do_colour_correction = p['do_colour_correction']
    do_telluric_correction = p['do_telluric_correction']
    do_xcor = p['do_xcor']
    plot_xcor = p['plot_xcor']
    make_mask = p['make_mask']
    apply_mask = p['apply_mask']
    c_subtract = p['c_subtract']
    do_berv_correction = p['do_berv_correction']
    do_keplerian_correction = p['do_keplerian_correction']
    make_doppler_model = p['make_doppler_model']
    skip_doppler_model = p['skip_doppler_model']
    typetest(do_colour_correction, bool,
             'do_colour_correction in run_instance()')
    typetest(do_telluric_correction, bool,
             'do_telluric_correction in run_instance()')
    typetest(do_xcor, bool, 'do_xcor in run_instance()')
    typetest(plot_xcor, bool, 'plot_xcor in run_instance()')
    typetest(make_mask, bool, 'make_mask in run_instance()')
    typetest(apply_mask, bool, 'apply_mask in run_instance()')
    typetest(c_subtract, bool, 'c_subtract in run_instance()')
    typetest(do_berv_correction, bool, 'do_berv_correction in run_instance()')
    typetest(do_keplerian_correction, bool,
             'do_keplerian_correction in run_instance()')
    typetest(make_doppler_model, bool, 'make_doppler_model in run_instance()')
    typetest(skip_doppler_model, bool, 'skip_doppler_model in run_instance()')

    #We start by defining constants and preparing for generating output.
    c = const.c.value / 1000.0  #in km/s
    colourdeg = 3  #A fitting degree for the colour correction.

    print(
        f'---Passed parameter input tests. Initiating output folder tree in {Path("output")/dp}.'
    )
    libraryname = str(template_library).split('/')[-1]
    if str(dp).split('/')[0] == 'data':
        dataname = str(dp).replace('data/', '')
        print(
            f'------Data is located in data/ folder. Assuming output name for this dataset as {dataname}'
        )
    else:
        dataname = dp
        print(
            f'------Data is NOT located in data/ folder. Assuming output name for this dataset as {dataname}'
        )

    list_of_wls = []  #This will store all the data.
    list_of_orders = []  #All of it needs to be loaded into your memory.
    list_of_sigmas = []

    trigger2 = 0  #These triggers are used to limit the generation of output in the forloop.
    trigger3 = 0
    n_negative_total = 0  #This will hold the total number of pixels that were set to NaN because they were zero when reading in the data.
    air = sp.paramget('air', dp)  #Read bool from str in config file.
    typetest(air, bool, 'air in run_instance()')

    filelist_orders = [str(i) for i in Path(dp).glob('order_*.fits')]
    if len(filelist_orders) == 0:
        raise Exception(
            f'Runtime error: No orders_*.fits files were found in {dp}.')
    try:
        order_numbers = [
            int(i.split('order_')[1].split('.')[0]) for i in filelist_orders
        ]
    except:
        raise Exception(
            'Runtime error: Failed casting fits filename numerals to ints. Are the filenames of the spectral orders correctly formatted?'
        )
    order_numbers.sort()  #This is the ordered list of numerical order IDs.
    n_orders = len(order_numbers)
    if n_orders == 0:
        raise Exception(
            f'Runtime error: n_orders may not have ended up as zero. ({n_orders})'
        )

#Loading the data from the datafolder.
    if do_xcor == True or plot_xcor == True or make_mask == True:
        print(f'---Loading orders from {dp}.')

        # for i in range(startorder,endorder+1):
        for i in order_numbers:
            wavepath = dp / f'wave_{i}.fits'
            orderpath = dp / f'order_{i}.fits'
            sigmapath = dp / f'sigma_{i}.fits'
            ut.check_path(wavepath, exists=True)
            ut.check_path(orderpath, exists=True)
            ut.check_path(sigmapath, exists=False)
            wave_axis = fits.getdata(wavepath)
            dimtest(wave_axis, [0], 'wavelength grid in run_instance()')
            n_px = len(wave_axis)  #Pixel width of the spectral order.
            if air == False:
                if i == np.min(order_numbers):
                    print("------Assuming wavelengths are in vaccuum.")
                list_of_wls.append(1.0 * wave_axis)
            else:
                if i == np.min(order_numbers):
                    print("------Applying airtovac correction.")
                list_of_wls.append(ops.airtovac(wave_axis))

            order_i = fits.getdata(orderpath)
            if i == np.min(order_numbers):
                dimtest(
                    order_i, [0, n_px], f'order {i} in run_instance()'
                )  #For the first order, check that it is 2D and that is has a width equal to n_px.
                n_exp = np.shape(
                    order_i
                )[0]  #then fix n_exp. All other orders should have the same n_exp.
                print(f'------{n_exp} exposures recognised.')
            else:
                dimtest(order_i, [n_exp, n_px], f'order {i} in run_instance()')

            #Now test for negatives, set them to NaN and track them.
            n_negative = len(order_i[order_i <= 0])
            if trigger3 == 0 and n_negative > 0:
                print("------Setting negative values to NaN.")
                trigger3 = -1
            n_negative_total += n_negative
            order_i[order_i <= 0] = np.nan
            postest(order_i, f'order {i} in run_instance().'
                    )  #make sure whatever comes out here is strictly positive.
            list_of_orders.append(order_i)

            try:  #Try to get a sigma file. If it doesn't exist, we raise a warning. If it does, we test its dimensions and append it.
                sigma_i = fits.getdata(sigmapath)
                dimtest(sigma_i, [n_exp, n_px],
                        f'order {i} in run_instance().')
                list_of_sigmas.append(sigma_i)
            except FileNotFoundError:
                if trigger2 == 0:
                    print(
                        '------WARNING: Sigma (flux error) files not provided. Assuming sigma = sqrt(flux). This is standard practise for HARPS data, but e.g. ESPRESSO has a pipeline that computes standard errors on each pixel for you.'
                    )
                    trigger2 = -1
                list_of_sigmas.append(np.sqrt(order_i))
        print(
            f"------{n_negative_total} negative values set to NaN ({np.round(100.0*n_negative_total/n_exp/n_px/len(order_numbers),2)}% of total spectral pixels in dataset.)"
        )

    if len(list_of_orders) != n_orders:
        raise Exception(
            'Runtime error: n_orders is not equal to the length of list_of_orders. Something went wrong when reading them in?'
        )

    print('---Finished loading dataset to memory.')

    #Apply telluric correction file or not.
    # plt.plot(list_of_wls[60],list_of_orders[60][10],color='red')
    # plt.plot(list_of_wls[60],list_of_orders[60][10]+list_of_sigmas[60][10],color='red',alpha=0.5)#plot corrected spectra
    # plt.plot(list_of_wls[60],list_of_orders[60][10]/list_of_sigmas[60][10],color='red',alpha=0.5)#plot SNR
    if do_telluric_correction == True and n_orders > 0:
        print('---Applying telluric correction')
        telpath = dp / 'telluric_transmission_spectra.pkl'
        list_of_orders, list_of_sigmas = telcor.apply_telluric_correction(
            telpath, list_of_wls, list_of_orders, list_of_sigmas)

    # plt.plot(list_of_wls[60],list_of_orders[60][10],color='blue')
    # plt.plot(list_of_wls[60],list_of_orders[60][10]+list_of_sigmas[60][10],color='blue',alpha=0.5)#plot corrected spectra

    # plt.plot(list_of_wls[60],list_of_orders[60][10]/list_of_sigmas[60][10],color='blue',alpha=0.5) #plot SNR
    # plt.show()
    # pdb.set_trace()

#Do velocity correction of wl-solution. Explicitly after telluric correction
#but before masking. Because the cross-correlation relies on columns being masked.
#Then if you start to move the CCFs around before removing the time-average,
#each masked column becomes slanted. Bad deal.
    rv_cor = 0
    if do_berv_correction == True:
        rv_cor += sp.berv(dp)
    if do_keplerian_correction == True:
        rv_cor -= sp.RV_star(dp) * (1.0)

    if type(rv_cor) != int and len(list_of_orders) > 0:
        print('---Reinterpolating data to correct velocities')
        list_of_orders_cor = []
        list_of_sigmas_cor = []
        for i in range(len(list_of_wls)):
            order = list_of_orders[i]
            sigma = list_of_sigmas[i]
            order_cor = order * 0.0
            sigma_cor = sigma * 0.0
            for j in range(len(list_of_orders[0])):
                wl_i = interp.interp1d(list_of_wls[i],
                                       order[j],
                                       bounds_error=False)
                si_i = interp.interp1d(list_of_wls[i],
                                       sigma[j],
                                       bounds_error=False)
                wl_cor = list_of_wls[i] * (
                    1.0 - (rv_cor[j] * u.km / u.s / const.c)
                )  #The minus sign was tested on a slow-rotator.
                order_cor[j] = wl_i(wl_cor)
                sigma_cor[j] = si_i(
                    wl_cor
                )  #I checked that this works because it doesn't affect the SNR, apart from wavelength-shifting it.
            list_of_orders_cor.append(order_cor)
            list_of_sigmas_cor.append(sigma_cor)
            ut.statusbar(i, fun.findgen(len(list_of_wls)))
        # plt.plot(list_of_wls[60],list_of_orders[60][10]/list_of_sigmas[60][10],color='blue')
        # plt.plot(list_of_wls[60],list_of_orders_cor[60][10]/list_of_sigmas_cor[60][10],color='red')
        # plt.show()
        # sys.exit()
        list_of_orders = list_of_orders_cor
        list_of_sigmas = list_of_sigmas_cor

    if len(list_of_orders) != n_orders:
        raise RuntimeError(
            'n_orders is no longer equal to the length of list_of_orders, though it was before. Something went wrong during telluric correction or velocity correction.'
        )

#Compute / create a mask and save it to file (or not)
    if make_mask == True and len(list_of_orders) > 0:
        if do_colour_correction == True:
            print(
                '---Constructing mask with intra-order colour correction applied'
            )
            masking.mask_orders(list_of_wls,
                                ops.normalize_orders(list_of_orders,
                                                     list_of_sigmas,
                                                     colourdeg)[0],
                                dp,
                                maskname,
                                40.0,
                                5.0,
                                manual=True)
        else:
            print(
                '---Constructing mask WITHOUT intra-order colour correction applied.'
            )
            print(
                '---Switch on colour correction if you see colour variations in the 2D spectra.'
            )
            masking.mask_orders(list_of_wls,
                                list_of_orders,
                                dp,
                                maskname,
                                40.0,
                                5.0,
                                manual=True)
        if apply_mask == False:
            print(
                '---WARNING in run_instance: Mask was made but is not applied to data (apply_mask == False)'
            )

#Apply the mask that was previously created and saved to file.
    if apply_mask == True:
        print('---Applying mask')
        list_of_orders = masking.apply_mask_from_file(dp, maskname,
                                                      list_of_orders)
        list_of_sigmas = masking.apply_mask_from_file(dp, maskname,
                                                      list_of_sigmas)
#Interpolate over all isolated NaNs and set bad columns to NaN (so that they are ignored in the CCF)
    if do_xcor == True:
        print('---Healing NaNs')
        list_of_orders = masking.interpolate_over_NaNs(
            list_of_orders
        )  #THERE IS AN ISSUE HERE: INTERPOLATION SHOULD ALSO HAPPEN ON THE SIGMAS ARRAY!
        list_of_sigmas = masking.interpolate_over_NaNs(list_of_sigmas)

#Normalize the orders to their average flux in order to effectively apply a broad-band colour correction (colour is typically a function of airmass and seeing).
    if do_colour_correction == True:
        print('---Normalizing orders to common flux level')
        # plt.plot(list_of_wls[60],list_of_orders[60][10]/list_of_sigmas[60][10],color='blue',alpha=0.4)
        list_of_orders_normalised, list_of_sigmas_normalised, meanfluxes = ops.normalize_orders(
            list_of_orders, list_of_sigmas, colourdeg
        )  #I tested that this works because it doesn't alter the SNR.

        meanfluxes_norm = meanfluxes / np.nanmean(meanfluxes)
    else:
        meanfluxes_norm = fun.findgen(len(
            list_of_orders[0])) * 0.0 + 1.0  #All unity.
        # plt.plot(list_of_wls[60],list_of_orders_normalised[60][10]/list_of_sigmas[60][10],color='red',alpha=0.4)
        # plt.show()
        # sys.exit()

    if len(list_of_orders) != n_orders:
        raise RuntimeError(
            'n_orders is no longer equal to the length of list_of_orders, though it was before. Something went wrong during masking or colour correction.'
        )

#Construct the cross-correlation templates in case we will be computing or plotting the CCF.
    if do_xcor == True or plot_xcor == True:

        list_of_wlts = []
        list_of_templates = []
        outpaths = []

        for templatename in templatelist:
            print(f'---Building template {templatename}')
            wlt, T = models.build_template(templatename,
                                           binsize=0.5,
                                           maxfrac=0.01,
                                           resolution=resolution,
                                           template_library=template_library,
                                           c_subtract=c_subtract)
            T *= (-1.0)
            if np.mean(wlt) < 50.0:  #This is likely in microns:
                print(
                    '------WARNING: The loaded template has a mean wavelength less than 50.0, meaning that it is very likely not in nm, but in microns. I have divided by 1,000 now and hope for the best...'
                )
                wlt *= 1000.0
            list_of_wlts.append(wlt)
            list_of_templates.append(T)

            outpath = Path('output') / Path(dataname) / Path(
                libraryname) / Path(templatename)

            if not os.path.exists(outpath):
                print(
                    f"------The output location ({outpath}) didn't exist, I made it now."
                )
                os.makedirs(outpath)
            outpaths.append(outpath)

#Perform the cross-correlation on the entire list of orders.
    for i in range(len(list_of_wlts)):
        templatename = templatelist[i]
        wlt = list_of_wlts[i]
        T = list_of_templates[i]
        outpath = outpaths[i]
        if do_xcor == True:
            print(
                f'---Cross-correlating spectra with template {templatename}.')
            t1 = ut.start()
            rv, ccf, ccf_e, Tsums = xcor(
                list_of_wls,
                list_of_orders_normalised,
                np.flipud(np.flipud(wlt)),
                T,
                drv,
                RVrange,
                list_of_errors=list_of_sigmas_normalised)
            ut.end(t1)
            print(f'------Writing CCFs to {str(outpath)}')
            ut.writefits(outpath / 'ccf.fits', ccf)
            ut.writefits(outpath / 'ccf_e.fits', ccf_e)
            ut.writefits(outpath / 'RV.fits', rv)
            ut.writefits(outpath / 'Tsum.fits', Tsums)
        else:
            print(
                f'---Reading CCFs with template {templatename} from {str(outpath)}.'
            )
            if os.path.isfile(outpath / 'ccf.fits') == False:
                raise FileNotFoundError(
                    f'CCF output not located at {outpath}. Rerun with do_xcor=True to create these files?'
                )
        rv = fits.getdata(outpath / 'rv.fits')
        ccf = fits.getdata(outpath / 'ccf.fits')
        ccf_e = fits.getdata(outpath / 'ccf_e.fits')
        Tsums = fits.getdata(outpath / 'Tsum.fits')

        ccf_cor = ccf * 1.0
        ccf_e_cor = ccf_e * 1.0

        print('---Cleaning CCFs')
        ccf_n, ccf_ne, ccf_nn, ccf_nne = clean_ccf(rv, ccf_cor, ccf_e_cor, dp)

        if make_doppler_model == True and skip_doppler_model == False:
            shadow.construct_doppler_model(rv,
                                           ccf_nn,
                                           dp,
                                           shadowname,
                                           xrange=[-200, 200],
                                           Nxticks=20.0,
                                           Nyticks=10.0)
            make_doppler_model = False  # This sets it to False after it's been run once, for the first template.
        if skip_doppler_model == False:
            print('---Reading doppler shadow model from ' + shadowname)
            doppler_model, dsmask = shadow.read_shadow(
                dp, shadowname, rv, ccf
            )  #This returns both the model evaluated on the rv,ccf grid, as well as the mask that blocks the planet trace.
            ccf_clean, matched_ds_model = shadow.match_shadow(
                rv, ccf_nn, dsmask, dp, doppler_model
            )  #THIS IS AN ADDITIVE CORRECTION, SO CCF_NNE DOES NOT NEED TO BE ALTERED AND IS STILL VALID VOOR CCF_CLEAN
        else:
            print('---Not performing shadow correction')
            ccf_clean = ccf_nn * 1.0
            matched_ds_model = ccf_clean * 0.0

        if f_w > 0.0:
            print('---Performing high-pass filter on the CCF')
            ccf_clean_filtered, wiggles = filter_ccf(
                rv, ccf_clean, v_width=f_w
            )  #THIS IS ALSO AN ADDITIVE CORRECTION, SO CCF_NNE IS STILL VALID.
        else:
            print('---Skipping high-pass filter')
            ccf_clean_filtered = ccf_clean * 1.0
            wiggles = ccf_clean * 0.0  #This filtering is additive so setting to zero is accurate.

        print('---Weighing CCF rows by mean fluxes that were normalised out')
        ccf_clean_weighted = np.transpose(
            np.transpose(ccf_clean_filtered) * meanfluxes_norm
        )  #MULTIPLYING THE AVERAGE FLUXES BACK IN! NEED TO CHECK THAT THIS ALSO GOES PROPERLY WITH THE ERRORS!
        ccf_nne = np.transpose(np.transpose(ccf_nne) * meanfluxes_norm)

        ut.save_stack(outpath / 'cleaning_steps.fits', [
            ccf, ccf_cor, ccf_nn, ccf_clean, matched_ds_model,
            ccf_clean_filtered, wiggles, ccf_clean_weighted
        ])
        ut.writefits(outpath / 'ccf_cleaned.fits', ccf_clean_weighted)
        ut.writefits(outpath / 'ccf_cleaned_error.fits', ccf_nne)

        print('---Constructing KpVsys')
        Kp, KpVsys, KpVsys_e = construct_KpVsys(rv, ccf_clean_weighted,
                                                ccf_nne, dp)
        ut.writefits(outpath / 'KpVsys.fits', KpVsys)
        ut.writefits(outpath / 'KpVsys_e.fits', KpVsys_e)
        ut.writefits(outpath / 'Kp.fits', Kp)

    return
    sys.exit()

    if plot_xcor == True and inject_model == False:
        print('---Plotting KpVsys')
        analysis.plot_KpVsys(rv, Kp, KpVsys, dp)

    #Now repeat it all for the model injection.
    if inject_model == True:
        for modelname in modellist:
            outpath_i = outpath + modelname + '/'
            if do_xcor == True:
                print('---Injecting model ' + modelname)
                list_of_orders_injected = models.inject_model(
                    list_of_wls,
                    list_of_orders,
                    dp,
                    modelname,
                    model_library=model_library
                )  #Start with the unnormalised orders from before.
                #Normalize the orders to their average flux in order to effectively apply
                #a broad-band colour correction (colour is a function of airmass and seeing).
                if do_colour_correction == True:
                    print(
                        '------Normalizing injected orders to common flux level'
                    )
                    list_of_orders_injected, list_of_sigmas_injected, meanfluxes_injected = ops.normalize_orders(
                        list_of_orders_injected, list_of_sigmas, colourdeg)
                    meanfluxes_norm_injected = meanfluxes_injected / np.mean(
                        meanfluxes_injected)
                else:
                    meanfluxes_norm_injected = fun.findgen(
                        len(list_of_orders_injected[0])
                    ) * 0.0 + 1.0  #All unity.

                print('------Cross-correlating injected orders')
                rv_i, ccf_i, ccf_e_i, Tsums_i = analysis.xcor(
                    list_of_wls,
                    list_of_orders_injected,
                    np.flipud(np.flipud(wlt)),
                    T,
                    drv,
                    RVrange,
                    list_of_errors=list_of_sigmas_injected)
                print('------Writing injected CCFs to ' + outpath_i)
                if not os.path.exists(outpath_i):
                    print("---------That path didn't exist, I made it now.")
                    os.makedirs(outpath_i)
                ut.writefits(outpath_i + '/' + 'ccf_i_' + modelname + '.fits',
                             ccf_i)
                ut.writefits(
                    outpath_i + '/' + 'ccf_e_i_' + modelname + '.fits',
                    ccf_e_i)
            else:
                print('---Reading injected CCFs from ' + outpath_i)
                if os.path.isfile(outpath_i + 'ccf_i_' + modelname +
                                  '.fits') == False:
                    print('------ERROR: Injected CCF not located at ' +
                          outpath_i + 'ccf_i_' + modelname + '.fits' +
                          '. Set do_xcor and inject_model to True?')
                    sys.exit()
                if os.path.isfile(outpath_i + 'ccf_e_i_' + modelname +
                                  '.fits') == False:
                    print('------ERROR: Injected CCF error not located at ' +
                          outpath_i + 'ccf_e_i_' + modelname + '.fits' +
                          '. Set do_xcor and inject_model to True?')
                    sys.exit()
                # f.close()
                # f2.close()
                ccf_i = fits.getdata(outpath_i + 'ccf_i_' + modelname +
                                     '.fits')
                ccf_e_i = fits.getdata(outpath_i + 'ccf_e_i_' + modelname +
                                       '.fits')

            print('---Cleaning injected CCFs')
            ccf_n_i, ccf_ne_i, ccf_nn_i, ccf_nne_i = cleaning.clean_ccf(
                rv, ccf_i, ccf_e_i, dp)
            ut.writefits(outpath_i + 'ccf_normalized_i.fits', ccf_nn_i)
            ut.writefits(outpath_i + 'ccf_ne_i.fits', ccf_ne_i)

            # if make_doppler_model == True and skip_doppler_model == False:
            # shadow.construct_doppler_model(rv,ccf_nn,dp,shadowname,xrange=[-200,200],Nxticks=20.0,Nyticks=10.0)
            if skip_doppler_model == False:
                # print('---Reading doppler shadow model from '+shadowname)
                # doppler_model,maskHW = shadow.read_shadow(dp,shadowname,rv,ccf)
                ccf_clean_i, matched_ds_model_i = shadow.match_shadow(
                    rv, ccf_nn_i, dp, doppler_model, maskHW)
            else:
                print(
                    '---Not performing shadow correction on injected spectra either.'
                )
                ccf_clean_i = ccf_nn_i * 1.0
                matched_ds_model_i = ccf_clean_i * 0.0

            if f_w > 0.0:
                ccf_clean_i_filtered, wiggles_i = cleaning.filter_ccf(
                    rv, ccf_clean_i, v_width=f_w)
            else:
                ccf_clean_i_filtered = ccf_clean_i * 1.0

            ut.writefits(outpath_i + 'ccf_cleaned_i.fits',
                         ccf_clean_i_filtered)
            ut.writefits(outpath + 'ccf_cleaned_i_error.fits', ccf_nne)

            print(
                '---Weighing injected CCF rows by mean fluxes that were normalised out'
            )
            ccf_clean_i_filtered = np.transpose(
                np.transpose(ccf_clean_i_filtered) * meanfluxes_norm_injected
            )  #MULTIPLYING THE AVERAGE FLUXES BACK IN! NEED TO CHECK THAT THIS ALSO GOES PROPERLY WITH THE ERRORS!
            ccf_nne_i = np.transpose(
                np.transpose(ccf_nne_i) * meanfluxes_norm_injected)

            print('---Constructing injected KpVsys')
            Kp, KpVsys_i, KpVsys_e_i = analysis.construct_KpVsys(
                rv, ccf_clean_i_filtered, ccf_nne_i, dp)
            ut.writefits(outpath_i + 'KpVsys_i.fits', KpVsys_i)
            # ut.writefits(outpath+'KpVsys_e_i.fits',KpVsys_e_i)
            if plot_xcor == True:
                print('---Plotting KpVsys with ' + modelname + ' injected.')
                analysis.plot_KpVsys(rv, Kp, KpVsys, dp, injected=KpVsys_i)
Ejemplo n.º 20
0
def build_template(templatename,
                   binsize=1.0,
                   maxfrac=0.01,
                   mode='top',
                   resolution=0.0,
                   c_subtract=True,
                   twopass=False,
                   template_library='models/library'):
    """This routine reads a specified model from the library and turns it into a
    cross-correlation template by subtracting the top-envelope (or bottom envelope),
    if c_subtract is set to True."""

    import tayph.util as ut
    from tayph.vartests import typetest, postest, notnegativetest
    import numpy as np
    import tayph.operations as ops
    import astropy.constants as const
    from astropy.io import fits
    from matplotlib import pyplot as plt
    from scipy import interpolate
    from pathlib import Path
    typetest(templatename, str, 'templatename mod.build_template()')
    typetest(binsize, [int, float], 'binsize mod.build_template()')
    typetest(maxfrac, [int, float], 'maxfrac mod.build_template()')
    typetest(
        mode,
        str,
        'mode mod.build_template()',
    )
    typetest(resolution, [int, float], 'resolution in mod.build_template()')
    typetest(twopass, bool, 'twopass in mod.build_template()')

    binsize = float(binsize)
    maxfrac = float(maxfrac)
    resolution = float(resolution)
    postest(binsize, 'binsize mod.build_template()')
    postest(maxfrac, 'maxfrac mod.build_template()')
    notnegativetest(resolution, 'resolution in mod.build_template()')
    template_library = ut.check_path(template_library, exists=True)

    c = const.c.to('km/s').value

    if mode not in ['top', 'bottom']:
        raise Exception(
            f'RuntimeError in build_template: Mode should be set to "top" or "bottom" ({mode}).'
        )
    wlt, fxt = get_model(templatename, library=template_library)

    if wlt[-1] <= wlt[0]:  #Reverse the wl axis if its sorted the wrong way.
        wlt = np.flipud(wlt)
        fxt = np.flipud(fxt)

    if c_subtract == True:
        wle, fxe = ops.envelope(
            wlt, fxt - np.median(fxt), binsize, selfrac=maxfrac,
            mode=mode)  #These are binpoints of the top-envelope.
        #The median of fxm is first removed to decrease numerical errors, because the spectrum may
        #have values that are large (~1.0) while the variations are small (~1e-5).
        e_i = interpolate.interp1d(wle, fxe, fill_value='extrapolate')
        envelope = e_i(wlt)
        T = fxt - np.median(fxt) - envelope
        absT = np.abs(T)
        T[(absT < 1e-4 * np.max(absT)
           )] = 0.0  #This is now continuum-subtracted and binary-like.
        #Any values that are small are taken out.
        #This therefore assumes that the model has lines that are deep compared to the numerical
        #error of envelope subtraction (!).
    else:
        T = fxt * 1.0

    if resolution != 0.0:
        dRV = c / resolution
        print(
            f'------Blurring template to resolution of data ({round(resolution,0)}, {round(dRV,2)} km/s)'
        )
        wlt_cv, T_cv, vstep = ops.constant_velocity_wl_grid(wlt,
                                                            T,
                                                            oversampling=2.0)
        print(f'---------v_step is {np.round(vstep,3)} km/s')
        print(
            f'---------So the resolution blurkernel has an avg width of {np.round(dRV/vstep,3)} px.'
        )
        T_b = ops.smooth(T_cv, dRV / vstep, mode='gaussian')
        wlt = wlt_cv * 1.0
        T = T_b * 1.0
    return (wlt, T)
Ejemplo n.º 21
0
def inject_model(list_of_wls,
                 list_of_orders,
                 dp,
                 modelname,
                 model_library='library/models'):
    """This function takes a list of spectral orders and injects a model with library
    identifier modelname, and system parameters as defined in dp. The model is blurred taking into
    account spectral resolution and rotation broadening (with an LSF as per Brogi et al.) and
    finite-exposure broadening (with a box LSF).

    It returns a copy of the list of orders with the model injected."""

    import tayph.util as ut
    import tayph.system_parameters as sp
    import tayph.models
    import astropy.constants as const
    import numpy as np
    import scipy
    import tayph.operations as ops
    from tayph.vartests import typetest, dimtest
    import pdb
    import copy
    import matplotlib.pyplot as plt

    # dimtest(order,[0,len(wld)])
    dp = ut.check_path(dp)
    typetest(modelname, str, 'modelname in models.inject_model()')
    typetest(model_library, str, 'model_library in models.inject_model()')

    c = const.c.to('km/s').value  #In km/s
    Rd = sp.paramget('resolution', dp)
    planet_radius = sp.paramget('Rp', dp)
    inclination = sp.paramget('inclination', dp)
    P = sp.paramget('P', dp)
    transit = sp.transit(dp)
    n_exp = len(transit)
    vsys = sp.paramget('vsys', dp)
    rv = sp.RV(dp) + vsys
    dRV = sp.dRV(dp)
    phi = sp.phase(dp)
    dimtest(transit, [n_exp])
    dimtest(rv, [n_exp])
    dimtest(phi, [n_exp])
    dimtest(dRV, [n_exp])

    mask = (transit - 1.0) / (np.min(transit - 1.0))

    wlm, fxm = get_model(modelname, library=model_library)
    if wlm[-1] <= wlm[0]:  #Reverse the wl axis if its sorted the wrong way.
        wlm = np.flipud(wlm)
        fxm = np.flipud(fxm)

    #With the model and the revelant parameters in hand, now only select that
    #part of the model that covers the wavelengths of the order provided.
    #A larger wavelength range would take much extra time because the convolution
    #is a slow operation.

    N_orders = len(list_of_wls)
    if N_orders != len(list_of_orders):
        raise RuntimeError(
            f'in models.inject_model: List_of_wls and list_of_orders are not of the '
            f'same length ({N_orders} vs {len(list_of_orders)})')

    if np.min(wlm) > np.min(list_of_wls) - 1.0 or np.max(
            wlm) < np.max(list_of_wls) + 1.0:
        ut.tprint(
            'WARNING in model injection: Data grid falls (partly) outside of model range. '
            'Setting missing area to 1.0. (meaning, no planet absorption.)')

    modelsel = [
        (wlm >= np.min(list_of_wls) - 1.0) & (wlm <= np.max(list_of_wls) + 1.0)
    ]

    wlm = wlm[tuple(modelsel)]
    fxm = fxm[tuple(modelsel)]

    fxm_b = ops.blur_rotate(wlm, fxm, c / Rd, planet_radius, P,
                            inclination)  #Only do this once per dataset.

    oversampling = 2.5
    wlm_cv, fxm_bcv, vstep = ops.constant_velocity_wl_grid(
        wlm, fxm_b, oversampling=oversampling)

    if np.min(dRV) < c / Rd / 10.0:

        dRV_min = c / Rd / 10.0  #If the minimum dRV is less than 10% of the spectral
        #resolution, we introduce a lower limit to when we are going to blur, because the effect
        #becomes insignificant.
    else:
        dRV_min = np.min(dRV)

    if dRV_min / vstep < 3:  #Meaning, if the smoothing is going to be undersampled by this choice
        #in v_step, it means that the oversampling parameter in ops.constant_velocity_wl_grid was
        #not high enough. Then we adjust it. I choose a value of 3 here to be safe, even though
        #ops.smooth below accepts values as low as 2.
        oversampling_new = 3.0 / (
            dRV_min / vstep) * oversampling  #scale up the oversampling factor.
        wlm_cv, fxm_bcv, vstep = ops.constant_velocity_wl_grid(
            wlm, fxm_b, oversampling=oversampling_new)

    list_of_orders_injected = copy.deepcopy(list_of_orders)

    for i in range(n_exp):
        if dRV[i] >= c / Rd / 10.0:
            fxm_b2 = ops.smooth(fxm_bcv, dRV[i] / vstep, mode='box')
        else:
            fxm_b2 = copy.deepcopy(fxm_bcv)
        shift = 1.0 + rv[i] / c
        fxm_i = scipy.interpolate.interp1d(
            wlm_cv * shift, fxm_b2, fill_value=1.0,
            bounds_error=False)  #This is a class that can be called.
        #Fill_value = 1 because if the model does not fully cover the order, it will be padded with 1.0s,
        #assuming that we are dealing with a model that is in transmission.
        for j in range(len(list_of_orders)):
            list_of_orders_injected[j][i] *= (
                1.0 + mask[i] * (fxm_i(list_of_wls[j]) - 1.0))  #This assumes
            #that the model is in transit radii. This can definitely be vectorised!

        #These are for checking that the broadening worked as expected:
        # injection_total[i,:]= scipy.interpolate.interp1d(wlm_cv*shift,fxm_b2)(wld)
        # injection_rot_only[i,:]=scipy.interpolate.interp1d(wlm*shift,fxm_b)(wld)
        # injection_pure[i,:]=scipy.interpolate.interp1d(wlm*shift,fxm)(wld)

    # ut.save_stack('test.fits',[injection_pure,injection_rot_only,injection_total])
    # pdb.set_trace()
    # ut.writefits('test.fits',injection)
    # pdb.set_trace()

    return (list_of_orders_injected)
Ejemplo n.º 22
0
def write_file_to_molecfit(molecfit_folder,
                           name,
                           headers,
                           waves,
                           spectra,
                           ii,
                           plot=False):
    """This is a wrapper for writing a spectrum from a list to molecfit format.
    name is the filename of the fits file that is the output.
    headers is the list of astropy header objects associated with the list of spectra
    in the spectra variable. ii is the number from that list that needs to be written (meaning
    that this routine is expected to be called as part of a loop).

    The user must make sure that the wavelength axes of these spectra are in air, in the observatory
    rest frame (meaning not BERV_corrected). Tayphs read_e2ds() function should have done this
    automatically.
    """
    import astropy.io.fits as fits
    from scipy import stats
    import copy
    import tayph.functions as fun
    import astropy.constants as const
    import astropy.units as u
    import numpy as np
    from tayph.vartests import typetest
    import tayph.util as ut
    import sys
    import matplotlib.pyplot as plt
    typetest(ii, int, 'ii in write_file_to_molecfit()')
    molecfit_folder = ut.check_path(molecfit_folder, exists=True)
    wave = waves[int(ii)]
    spectrum = spectra[int(ii)]
    npx = len(spectrum)

    #Need to un-berv-correct the s1d spectra to go back to the frame of the Earths atmosphere.
    #This is no longer true as of Feb 17, because read_e2ds now uncorrects HARPS, ESPRESSO and
    #UVES spectra by default.
    # if mode == 'HARPS':
    #     berv = headers[ii]['HIERARCH ESO DRS BERV']
    # elif mode == 'HARPSN':
    #     berv = headers[ii]['HIERARCH TNG DRS BERV']
    # elif mode in ['ESPRESSO','UVES-red','UVES-blue']:
    #     berv = headers[ii]['HIERARCH ESO QC BERV']
    # wave = copy.deepcopy(wave*(1.0-(berv*u.km/u.s/const.c).decompose().value))
    spectrum[spectrum <= 0] = np.nan
    err = np.sqrt(spectrum)
    # spectrum[np.isnan(spectrum)]=0
    # err[np.isnan(err)]=0
    if plot:
        plt.plot(wave, spectrum)
        plt.xlabel('Wavelength')
        plt.ylabel('Flux')
        plt.show()
        plt.plot(wave, err)
        plt.xlabel('Wavelength')
        plt.ylabel('Error')
        plt.show()
    #Write out the s1d spectrum in a format that molecfit eats.
    #This is a fits file with an empty primary extension that contains the header of the original s1d file.
    #Plus an extension that contains a binary table with 3 columns.
    #The names of these columns need to be indicated in the molecfit parameter file,
    #as well as the name of the file itself. This is currently hardcoded.
    col1 = fits.Column(name='wavelength', format='1D', array=wave)
    col2 = fits.Column(name='flux', format='1D', array=spectrum)
    col3 = fits.Column(name='err_flux', format='1D', array=err)
    cols = fits.ColDefs([col1, col2, col3])
    tbhdu = fits.BinTableHDU.from_columns(cols)
    prihdr = fits.Header()
    prihdr = copy.deepcopy(headers[ii])
    prihdu = fits.PrimaryHDU(header=prihdr)
    thdulist = fits.HDUList([prihdu, tbhdu])
    thdulist.writeto(molecfit_folder / name, overwrite=True)
    ut.tprint(f'Spectrum {ii} written to {str(molecfit_folder/name)}')
    return (0)
Ejemplo n.º 23
0
def set_molecfit_config(configpath):
    import pkg_resources
    from pathlib import Path
    import os
    import subprocess
    import tayph.system_parameters as sp
    import tayph.util as ut

    #Prepare for making formatted output.
    # terminal_height,terminal_width = subprocess.check_output(['stty', 'size']).split()

    Q1 = (
        'In what folder are parameter files defined and should (intermediate) molecfit output be '
        'written to?')
    Q2 = 'In what folder is the molecfit binary located?'
    Q3 = 'What is your python 3.x alias?'

    # configpath=get_molecfit_config()
    configpath = Path(configpath)
    if configpath.exists():
        ut.tprint(
            f'Molecfit configuration file already exists at {configpath}.')
        print('Overwriting existing values.')
        current_molecfit_input_folder = sp.paramget('molecfit_input_folder',
                                                    configpath,
                                                    full_path=True)
        current_molecfit_prog_folder = sp.paramget('molecfit_prog_folder',
                                                   configpath,
                                                   full_path=True)
        current_python_alias = sp.paramget('python_alias',
                                           configpath,
                                           full_path=True)

        ut.tprint(Q1)
        ut.tprint(
            f'Currently: {current_molecfit_input_folder} (leave empty to keep current '
            'value).')

        new_input_folder_input = str(input())
        if len(new_input_folder_input) == 0:
            new_molecfit_input_folder = ut.check_path(
                current_molecfit_input_folder, exists=True)
        else:
            new_molecfit_input_folder = ut.check_path(new_input_folder_input,
                                                      exists=True)
        print('')
        ut.tprint(Q2)
        ut.tprint(f'Currently: {current_molecfit_prog_folder}')
        new_prog_folder_input = str(input())
        if len(new_prog_folder_input) == 0:
            new_molecfit_prog_folder = ut.check_path(
                current_molecfit_prog_folder, exists=True)
        else:
            new_molecfit_prog_folder = ut.check_path(new_prog_folder_input,
                                                     exists=True)
        print('')
        ut.tprint(Q3)
        ut.tprint(f'Currently: {current_python_alias}')
        new_python_alias_input = str(input())
        if len(new_python_alias_input) == 0:
            new_python_alias = current_python_alias
        else:
            new_python_alias = new_python_alias_input
    else:  #This is actually the default mode of using this, because this function is generally
        #only called when tel.molecfit() is run for the first time and the config file doesn't exist yet.
        ut.tprint(Q1)
        new_molecfit_input_folder = ut.check_path(str(input()), exists=True)
        print('')
        ut.tprint(Q2)
        new_molecfit_prog_folder = ut.check_path(str(input()), exists=True)
        print('')
        ut.tprint(Q3)
        new_python_alias = str(input())

    with open(configpath, "w") as f:
        f.write(f'molecfit_input_folder   {str(new_molecfit_input_folder)}\n')
        f.write(f'molecfit_prog_folder   {str(new_molecfit_prog_folder)}\n')
        f.write(f'python_alias   {str(new_python_alias)}\n')

    ut.tprint(
        f'New molecfit configation file successfully written to {configpath}')
Ejemplo n.º 24
0
def read_espresso(inpath, filelist, read_s1d=True):
    #The following variables define lists in which all the necessary data will be stored.
    framename = []
    header = []
    s1dhdr = []
    obstype = []
    texp = np.array([])
    date = []
    mjd = np.array([])
    s1dmjd = np.array([])
    npx = np.array([])
    norders = np.array([])
    e2ds = []
    s1d = []
    wave1d = []
    airmass = np.array([])
    berv = np.array([])
    wave = []
    catkeyword = 'EXTNAME'
    bervkeyword = 'HIERARCH ESO QC BERV'
    airmass_keyword1 = 'HIERARCH ESO TEL'
    airmass_keyword2 = ' AIRM '
    airmass_keyword3_start = 'START'
    airmass_keyword3_end = 'END'
    for i in range(len(filelist)):
        if filelist[i].endswith('S2D_BLAZE_A.fits'):
            hdul = fits.open(inpath / filelist[i])
            data = copy.deepcopy(hdul[1].data)
            hdr = hdul[0].header
            hdr2 = hdul[1].header
            wavedata = copy.deepcopy(hdul[5].data)
            hdul.close()
            del hdul

            if hdr2[catkeyword] == 'SCIDATA':
                # print('science keyword found')
                print(f'------{filelist[i]}', end="\r")
                framename.append(filelist[i])
                header.append(hdr)
                obstype.append('SCIENCE')
                texp = np.append(texp, hdr['EXPTIME'])
                date.append(hdr['DATE-OBS'])
                mjd = np.append(mjd, hdr['MJD-OBS'])
                npx = np.append(npx, hdr2['NAXIS1'])
                norders = np.append(norders, hdr2['NAXIS2'])
                e2ds.append(data)
                berv = np.append(berv, hdr[bervkeyword])  #in km.s.
                telescope = hdr['TELESCOP'][-1]
                airmass = np.append(
                    airmass,
                    0.5 * (hdr[airmass_keyword1 + telescope + ' AIRM START'] +
                           hdr[airmass_keyword1 + telescope + ' AIRM END']))
                wave.append(
                    wavedata / 10.0
                )  #*(1.0-(hdr[bervkeyword]*u.km/u.s/const.c).decompose().value))
                #Ok.! So unlike HARPS, ESPRESSO wavelengths are actually BERV corrected in the S2Ds.
                #WHY!!!?. WELL SO BE IT. IN ORDER TO HAVE E2DSes THAT ARE ON THE SAME GRID, AS REQUIRED, WE UNDO THE BERV CORRECTION HERE.
                #WHEN COMPARING WAVE[0] WITH WAVE[1], YOU SHOULD SEE THAT THE DIFFERENCE IS NILL.
                #THATS WHY LATER WE CAN JUST USE WAVE[0] AS THE REPRESENTATIVE GRID FOR ALL.
                #BUT THAT IS SILLY. JUST SAVE THE WAVELENGTHS!

                if read_s1d:
                    s1d_path = inpath / Path(
                        str(filelist[i]).replace('_S2D_BLAZE_A.fits',
                                                 '_S1D_A.fits'))
                    #Need the blazed files. Not the S2D_A's by themselves.
                    ut.check_path(
                        s1d_path,
                        exists=True)  #Crash if the S1D doesn't exist.
                    hdul = fits.open(s1d_path)
                    data_table = copy.deepcopy(hdul[1].data)
                    hdr1d = hdul[0].header
                    hdul.close()
                    del hdul
                    s1d.append(data_table.field(2))

                    berv1d = hdr1d[bervkeyword]
                    if berv1d != hdr[bervkeyword]:
                        wrn_msg = (
                            'WARNING in read_espresso(): BERV correction of S1D file is not'
                            f'equal to that of the S2D file. {berv1d} vs {hdr[bervkeyword]}'
                        )
                        ut.tprint(wrn_msg)
                    gamma = (1.0 -
                             (berv1d * u.km / u.s / const.c).decompose().value)
                    wave1d.append(data_table.field(1) *
                                  gamma)  #This is in angstroms.
                    #We need to check to which UT ESPRESSO was connected, so that we can read
                    #the weather information (which is UT-specific) and parse them into the
                    #header using UT-agnostic keywords that are in the ESPRESSO.par file.
                    TELESCOP = hdr1d['TELESCOP'].split('U')[
                        1]  #This is the number of the UT, either 1, 2, 3 or 4.
                    if TELESCOP not in ['1', '2', '3', '4']:
                        raise ValueError(
                            f"in read_e2ds when reading ESPRESSO data. The UT telescope is not recognised. (TELESCOP={hdr['TELESCOP']})"
                        )
                    else:
                        hdr1d['TELALT'] = hdr1d[f'ESO TEL{TELESCOP} ALT']
                        hdr1d['RHUM'] = hdr1d[f'ESO TEL{TELESCOP} AMBI RHUM']
                        hdr1d['PRESSURE'] = (
                            hdr1d[f'ESO TEL{TELESCOP} AMBI PRES START'] +
                            hdr1d[f'ESO TEL{TELESCOP} AMBI PRES END']) / 2.0
                        hdr1d['AMBITEMP'] = hdr1d[
                            f'ESO TEL{TELESCOP} AMBI TEMP']
                        hdr1d['M1TEMP'] = hdr1d[
                            f'ESO TEL{TELESCOP} TH M1 TEMP']
                    s1dhdr.append(hdr1d)
                    s1dmjd = np.append(s1dmjd, hdr1d['MJD-OBS'])
    if read_s1d:
        output = {
            'wave': wave,
            'e2ds': e2ds,
            'header': header,
            'wave1d': wave1d,
            's1d': s1d,
            's1dhdr': s1dhdr,
            'mjd': mjd,
            'date': date,
            'texp': texp,
            'obstype': obstype,
            'framename': framename,
            'npx': npx,
            'norders': norders,
            'berv': berv,
            'airmass': airmass,
            's1dmjd': s1dmjd
        }
    else:
        output = {
            'wave': wave,
            'e2ds': e2ds,
            'header': header,
            'mjd': mjd,
            'date': date,
            'texp': texp,
            'obstype': obstype,
            'framename': framename,
            'npx': npx,
            'norders': norders,
            'berv': berv,
            'airmass': airmass
        }
    return (output)
Ejemplo n.º 25
0
def paramget(keyword,dp,full_path=False):
    """This code queries a planet system parameter from a config file located in the folder
    specified by the path dp; or run configuration parameters from a file speciefied by the full
    path dp, if full_path is set to True.

    Parameters
    ----------
    keyword : str
        A keyword present in the cofig file.

    dp : str, Path
        Output filename/path.

    full_path: bool
        If set, dp refers to the actual file, not the location of a folder with a config.dat;
        but the actual file itself.

    Returns
    -------
    value : int, float, bool, str
        The value corresponding to the requested keyword.

    """
    from tayph.vartests import typetest
    from tayph.util import check_path
    import pathlib
    import distutils.util
    import pdb


    #This is probably the only case where I don't need obs_times and config to exist together...
    dp=check_path(dp)
    typetest(keyword,str,'keyword in paramget()')

    if isinstance(dp,str) == True:
        dp=pathlib.Path(dp)
    try:
        if full_path == False:
            dp = dp/'config'
        f = open(dp, 'r')
    except FileNotFoundError:
        raise FileNotFoundError('parameter file does not exist at %s' % str(dp)) from None
    x = f.read().splitlines()
    f.close()
    n_lines=len(x)
    keywords={}
    for i in range(0,n_lines):
        line=x[i].split()
        if len(line) > 1:
            try:
                value=float(line[1])
            except ValueError:
                try:
                    value=bool(distutils.util.strtobool(line[1]))
                except ValueError:
                    value=(line[1])
            keywords[line[0]] = value
    try:
        return(keywords[keyword])
    except KeyError:
        # print(keywords)
        raise Exception('Keyword %s is not present in parameter file at %s' % (keyword,dp)) from None
Ejemplo n.º 26
0
def write_file_to_molecfit(molecfit_file_root,
                           name,
                           headers,
                           spectra,
                           ii,
                           mode='HARPS',
                           wave=[]):
    """This is a wrapper for writing a spectrum from a list to molecfit format.
    name is the filename of the fits file that is the output.
    headers is the list of astropy header objects associated with the list of spectra
    in the spectra variable. ii is the number from that list that needs to be written.

    The wave keyword is set for when the s1d headers do not contain wavelength information like HARPS does.
    (for instance, ESPRESSO). The wave keyword needs to be set in this case, to the wavelength array as extracted from FITS files or smth.
    If you do that for HARPS and set the wave keyword, this code will still grab it from the header, and overwrite it. So dont bother.
    """
    import astropy.io.fits as fits
    from scipy import stats
    import copy
    import tayph.functions as fun
    import astropy.constants as const
    import astropy.units as u
    import numpy as np
    from tayph.vartests import typetest
    import tayph.util as ut
    import sys
    typetest(ii, int, 'ii write_file_to_molecfit')
    molecfit_file_root = ut.check_path(molecfit_file_root, exists=True)
    spectrum = spectra[int(ii)]
    npx = len(spectrum)

    if mode == 'HARPS':
        berv = headers[ii][
            'HIERARCH ESO DRS BERV']  #Need to un-correct the s1d spectra to go back to the frame of the Earths atmosphere.
        wave = (headers[ii]['CDELT1'] * fun.findgen(len(spectra[ii])) +
                headers[ii]['CRVAL1']) * (
                    1.0 - (berv * u.km / u.s / const.c).decompose().value)
    elif mode == 'HARPSN':
        berv = headers[ii][
            'HIERARCH TNG DRS BERV']  #Need to un-correct the s1d spectra to go back to the frame of the Earths atmosphere.
        wave = (headers[ii]['CDELT1'] * fun.findgen(len(spectra[ii])) +
                headers[ii]['CRVAL1']) * (
                    1.0 - (berv * u.km / u.s / const.c).decompose().value)
    elif mode in ['ESPRESSO', 'UVES-red', 'UVES-blue']:
        if len(wave) == 0:
            raise ValueError(
                'in write_file_to_molecfit(): When mode in [ESPRESSO,UVES-red,UVES-blue], the 1D wave axis needs to be provided.'
            )
        #WAVE VARIABLE NEEDS TO BE PASSED NOW.
        berv = headers[ii][
            'HIERARCH ESO QC BERV']  #Need to un-correct the s1d spectra to go back to the frame of the Earths atmosphere.
        wave = copy.deepcopy(
            wave * (1.0 - (berv * u.km / u.s / const.c).decompose().value))

    err = np.sqrt(spectrum)

    #Write out the s1d spectrum in a format that molecfit eats.
    #This is a fits file with an empty primary extension that contains the header of the original s1d file.
    #Plus an extension that contains a binary table with 3 columns.
    #The names of these columns need to be indicated in the molecfit parameter file,
    #as well as the name of the file itself. This is currently hardcoded.
    col1 = fits.Column(name='wavelength', format='1D', array=wave)
    col2 = fits.Column(name='flux', format='1D', array=spectrum)
    col3 = fits.Column(name='err_flux', format='1D', array=err)
    cols = fits.ColDefs([col1, col2, col3])
    tbhdu = fits.BinTableHDU.from_columns(cols)
    prihdr = fits.Header()
    prihdr = copy.deepcopy(headers[ii])
    prihdu = fits.PrimaryHDU(header=prihdr)
    thdulist = fits.HDUList([prihdu, tbhdu])
    thdulist.writeto(molecfit_file_root / name, overwrite=True)
    print(f'Spectrum {ii} written')
    return (0)
Ejemplo n.º 27
0
def read_e2ds(inpath,
              outname,
              config,
              nowave=False,
              molecfit=False,
              mode='HARPS',
              ignore_exp=[]):
    """This is the workhorse for reading in a time-series of archival 2D echelle
    spectra and formatting these into the order-wise FITS format that Tayph uses.

    The user should point this script to a folder (located at inpath) that contains
    their pipeline-reduced echelle spectra. The script expects a certain data
    format, depending on the instrument in question. It is designed to accept
    pipeline products of the HARPS, HARPS-N, ESPRESSO and UVES instruments. In the
    case of HARPS, HARPS-N and ESPRESSO these may be downloaded from the archive.
    UVES is a bit special, because the 2D echelle spectra are not a standard
    pipeline output. Typical use cases are explained further below.

    This script formats the time series of 2D echelle spectra into 2D FITS images,
    where each FITS file is the time-series of a single echelle order. If the
    spectrograph has N orders, an order spans npx pixels, and M exposures
    were taken during the time-series, there will be N FITS files, each measuring
    M rows by npx columns. This script will read the headers of the pipeline
    reduced data files to determine the date/time of each, the exposure time, the
    barycentric correction (without applying it) and the airmass, and writes
    these to an ASCII table along with the FITS files containing the spectral
    orders.

    A crucial functionality of this script is that it also acts as a wrapper
    for the Molecfit telluric correction software. If installed properly, the
    user can call this script with the molecfit keyword to let Molecfit loop
    over the entire timeseries. To enable this functionality, the script
    reads the full-width, 1D spectra that are output by the instrument pipelines
    as well. Molecfit is applied to this time-series of 1D spectra, creating a
    time-series of models of the telluric absorption spectrum that is saved along
    with the 2D fits files. Tayph later interpolates these models onto the 2D
    spectra. Molecfit is called once in GUI-mode, allowing the user to select the
    relevant fitting regions and parameters, after which it is repeated
    automatically for the entire time series.

    Without Molecfit, this script finishes in a matter of seconds. However with
    molecfit enabled, it can take many hours (so if I wish to telluric-correct
    my data, I run this script overnight).

    The processing of HARPS, HARPS-N and ESPRESSO data is executed in an almost
    identical manner, because the pipeline-reduced products are almost identical.
    To run on either of these instruments, the user simply downloads all pipeline
    products of a given time-series, and extracts these in the same folder (meaning
    ccfs, e2ds/s2d, s1d, blaze, wave files, etc.) This happens to be the standard
    format when downloading pipeline-reduced data from the archive.

    For UVES, the functionality is much more constricted because the pipeline
    reduced data in the ESO archive is generally not of sufficient stability to
    enable precise time-resolved spectroscopy. I designed this function therefore
    to run on the pipeline-products produced by the Reflex (GUI) software. For this,
    a user should download the raw UVES data of their time series, letting ESO's
    calselector tool find the associated calibration files. This can easily be
    many GBs worth of data for a given observing program. The user should then
    reduce these data with the Reflex software. Reflex creates resampled, stitched
    1D spectra as its primary output. However, we will elect to use the intermediate
    pipeline products, which include the 2D extracted orders, located in Reflex's
    working directory after the reduction process is completed.

    A further complication of UVES data is that it can be used with different
    dichroics and 'arms', leading to spectral coverage on the blue, redu and/or redl
    chips. The user should take care that their time series contains only one of these
    types at any time. If they are mixed, this script will throw an exception.



    Set the nowave keyword to True if the dataset is HARPS or HARPSN, but it has
    no wave files associated with it. This may happen if you downloaded ESO
    Advanced Data Products, which include reduced science e2ds's but not reduced
    wave e2ds's. The wavelength solution is still encoded in the fits header however,
    so we take it from there, instead.

    Set the ignore_exp keyword to a list of exposures (start counting at 0) that
    need to be ignored when reading, e.g. because they are bad for some reason.
    If you have set molecfit to True, this becomes an expensive parameter to
    play with in terms of computing time, so its better to figure out which
    exposures you'd wish to ignore first (by doing most of your analysis),
    before actually running Molecfit, which is icing on the cake in many use-
    cases in the optical.

    The config parameter points to a configuration file (usually your generic
    run definition file) that is only used to point the Molecfit wrapper to the
    Molecfit installation on your system. If you are not using molecfit, you may
    pass an empty string here.

    """

    import os
    import pdb
    from astropy.io import fits
    import astropy.constants as const
    import numpy as np
    import matplotlib.pyplot as plt
    import sys
    import tayph.util as ut
    from tayph.vartests import typetest, dimtest
    import tayph.tellurics as mol
    import tayph.system_parameters as sp
    import tayph.functions as fun
    import copy
    import scipy.interpolate as interp
    import pickle
    from pathlib import Path
    import warnings
    import glob

    # molecfit = False
    #First check the input:
    inpath = ut.check_path(inpath, exists=True)
    typetest(outname, str, 'outname in read_HARPS_e2ds()')
    typetest(nowave, bool, 'nowave switch in read_HARPS_e2ds()')
    typetest(molecfit, bool, 'molecfit switch in read_HARPS_e2ds()')
    typetest(ignore_exp, list, 'ignore_exp in read_HARPS_e2ds()')
    typetest(mode, str, 'mode in read_HARPS_e2ds()')
    if molecfit:
        config = ut.check_path(config, exists=True)

    if mode not in [
            'HARPS', 'HARPSN', 'HARPS-N', 'ESPRESSO', 'UVES-red', 'UVES-blue'
    ]:
        raise ValueError(
            "in read_HARPS_e2ds: mode needs to be set to HARPS, HARPSN, UVES-red, UVES-blue or ESPRESSO."
        )

    filelist = os.listdir(
        inpath
    )  #If mode == UVES, these are folders. Else, they are fits files.
    N = len(filelist)

    if len(filelist) == 0:
        raise FileNotFoundError(
            f" in read_e2ds: input folder {str(inpath)} is empty.")

    #The following variables define lists in which all the necessary data will be stored.
    framename = []
    header = []
    s1dhdr = []
    type = []
    texp = np.array([])
    date = []
    mjd = np.array([])
    s1dmjd = np.array([])
    npx = np.array([])
    norders = np.array([])
    e2ds = []
    s1d = []
    wave1d = []
    airmass = np.array([])
    berv = np.array([])
    wave = []
    blaze = []
    wavefile_used = []
    outpath = Path('data/' + outname)
    if os.path.exists(outpath) != True:
        os.makedirs(outpath)

    e2ds_count = 0
    sci_count = 0
    wave_count = 0
    blaze_count = 0
    s1d_count = 0

    if mode == 'HARPS-N': mode = 'HARPSN'

    #MODE SWITCHING HERE:
    if mode in ['HARPS', 'UVES-red', 'UVES-blue']:
        catkeyword = 'HIERARCH ESO DPR CATG'
        bervkeyword = 'HIERARCH ESO DRS BERV'
        thfilekeyword = 'HIERARCH ESO DRS CAL TH FILE'
        Zstartkeyword = 'HIERARCH ESO TEL AIRM START'
        Zendkeyword = 'HIERARCH ESO TEL AIRM END'
    if mode == 'HARPSN':
        catkeyword = 'OBS-TYPE'
        bervkeyword = 'HIERARCH TNG DRS BERV'
        thfilekeyword = 'HIERARCH TNG DRS CAL TH FILE'
        Zstartkeyword = 'AIRMASS'
        Zendkeyword = 'AIRMASS'  #These are the same because HARPSN doesnt have start and end keywords.
        #Down there, the airmass is averaged, so there is no problem in taking the average of the same number.

    #Here is the actual parsing of the list of files that were read above. The
    #behaviour is different depending on whether this is HARPS, UVES or ESPRESSO
    #data, so it switches with a big if-statement in which there is a forloop
    #over the filelist in each case. The result is lists or np.arrays containing
    #the 2D spectra, the 1D spectra, their 2D and 1D wavelength solutions, the
    #headers, the MJDs, the BERVs and the airmasses, as well as (optionally) CCFs
    #and blaze files, though these are not really used.

    print(f'Read_e2ds is attempting to read a {mode} datafolder.')
    if mode == 'UVES-red' or mode == 'UVES-blue':  #IF we are UVES-like
        for i in range(N):
            print(filelist[i])
            if (inpath / Path(filelist[i])).is_dir():
                tmp_products = [
                    i for i in (
                        inpath /
                        Path(filelist[i])).glob('resampled_science_*.fits')
                ]
                tmp_products1d = [
                    i for i in (inpath /
                                Path(filelist[i])).glob('red_science_*.fits')
                ]
                if mode == 'UVES-red' and len(tmp_products) != 2:
                    raise ValueError(
                        f"in read_e2ds: When mode=UVES-red there should be 2 resampled_science files (redl and redu), but {len(tmp_products)} were detected."
                    )
                if mode == 'UVES-blue' and len(tmp_products) != 1:
                    raise ValueError(
                        f"in read_e2ds: When mode=UVES-rblue there should be 1 resampled_science files (blue), but {len(tmp_products)} were detected."
                    )
                if mode == 'UVES-red' and len(tmp_products1d) != 2:
                    raise ValueError(
                        f"in read_e2ds: When mode=UVES-red there should be 2 red_science files (redl and redu), but {len(tmp_products1d)} were detected."
                    )
                if mode == 'UVES-blue' and len(tmp_products1d) != 1:
                    raise ValueError(
                        f"in read_e2ds: When mode=UVES-rblue there should be 1 red_science files (blue), but {len(tmp_products1d)} were detected."
                    )

                data_combined = [
                ]  #This will store the two chips (redu and redl) in case of UVES_red, or simply the blue chip if otherwise.
                wave_combined = []
                wave1d_combined = []
                data1d_combined = []
                norders_tmp = 0
                for tmp_product in tmp_products:
                    hdul = fits.open(tmp_product)
                    data = copy.deepcopy(hdul[0].data)
                    hdr = hdul[0].header
                    hdul.close()
                    del hdul[0].data
                    if not hdr[
                            'HIERARCH ESO PRO SCIENCE']:  #Only add if it's actually a science product:#I force the user to supply only science exposures in the input  folder. No BS allowed... UVES is hard enough as it is.
                        raise ValueError(
                            f' in read_e2ds: UVES file {tmp_product} is not classified as a SCIENCE file, but should be. Remove it from the folder?'
                        )
                    wavedata = ut.read_wave_from_e2ds_header(hdr, mode='UVES')
                    data_combined.append(data)
                    wave_combined.append(wavedata)
                    norders_tmp += np.shape(data)[0]

                for tmp_product in tmp_products1d:
                    hdul = fits.open(tmp_product)
                    data_1d = copy.deepcopy(hdul[0].data)
                    hdr1d = hdul[0].header
                    hdul.close()
                    del hdul[0].data
                    if not hdr1d[
                            'HIERARCH ESO PRO SCIENCE']:  #Only add if it's actually a science product:#I force the user to supply only science exposures in the input  folder. No BS allowed... UVES is hard enough as it is.
                        raise ValueError(
                            f' in read_e2ds: UVES file {tmp_product} is not classified as a SCIENCE file, but should be. Remove it from the folder?'
                        )

                    npx1d = hdr1d['NAXIS1']
                    wavedata = fun.findgen(
                        npx1d) * hdr1d['CDELT1'] + hdr1d['CRVAL1']
                    data1d_combined.append(data_1d)
                    wave1d_combined.append(wavedata)

                if len(data_combined) < 1 or len(
                        data_combined
                ) > 2:  #Double-checking that length here...
                    raise ValueError(
                        f'in read_e2ds(): Expected 1 or 2 chips, but {len(data_combined)} files were somehow read.'
                    )
                #The chips generally don't give the same size. Therefore I will pad the smaller one with NaNs to make it fit:
                if len(data_combined) != len(data1d_combined):
                    raise ValueError(
                        f'in read_e2ds(): The number of chips in the 1d and 2d spectra is not the same {len(data1d_combined)} vs {len(data_combined)}.'
                    )

                if len(data_combined) == 2:
                    chip1 = data_combined[0]
                    chip2 = data_combined[1]
                    wave1 = wave_combined[0]
                    wave2 = wave_combined[1]
                    npx_1 = np.shape(chip1)[1]
                    npx_2 = np.shape(chip2)[1]
                    no_1 = np.shape(chip1)[0]
                    no_2 = np.shape(chip2)[0]
                    npx_max = np.max([npx_1, npx_2])
                    npx_min = np.min([npx_1, npx_2])
                    diff = npx_max - npx_min
                    #Pad the smaller one with NaNs to match the wider one:
                    if npx_1 < npx_2:
                        chip1 = np.hstack(
                            [chip1, np.zeros((no_1, diff)) * np.nan])
                        wave1 = np.hstack(
                            [wave1, np.zeros((no_1, diff)) * np.nan])
                    else:
                        chip2 = np.hstack(
                            [chip2, np.zeros((no_2, diff)) * np.nan])
                        wave2 = np.hstack(
                            [wave2, np.zeros((no_2, diff)) * np.nan])

                    #So now they can be stacked:
                    e2ds_stacked = np.vstack((chip1, chip2))
                    npx = np.append(npx, np.shape(e2ds_stacked)[1])
                    e2ds.append(e2ds_stacked)
                    wave.append(np.vstack((wave1, wave2)))

                    chip1_1d = data1d_combined[0]
                    chip2_1d = data1d_combined[1]
                    wave1_1d = wave1d_combined[0]
                    wave2_1d = wave1d_combined[1]

                    if np.nanmean(wave1_1d) < np.nanmean(wave2_1d):
                        combined_data_1d = np.concatenate((chip1_1d, chip2_1d))
                        combined_wave_1d = np.concatenate((wave1_1d, wave2_1d))
                    else:
                        combined_data_1d = np.concatenate((chip2_1d, chip1_1d))
                        combined_wave_1d = np.concatenate((wave2_1d, wave1_1d))
                    wave1d.append(combined_wave_1d)
                    s1d.append(combined_data_1d)
                else:
                    e2ds.append(data_combined[0])
                    wave.append(wave_combined[0])
                    npx = np.append(npx, np.shape(data_combined[0])[1])
                    wave1d.append(wave1d_combined[0])
                    s1d.append(data1d_combined[0])
                #Only using the keyword from the second header in case of redl,redu.

                s1dmjd = np.append(s1dmjd, hdr1d['MJD-OBS'])
                framename.append(hdr['ARCFILE'])
                header.append(hdr)
                type.append('SCIENCE')
                texp = np.append(texp, hdr['EXPTIME'])
                date.append(hdr['DATE-OBS'])
                mjd = np.append(mjd, hdr['MJD-OBS'])
                norders = np.append(norders, norders_tmp)
                airmass = np.append(
                    airmass, 0.5 * (hdr[Zstartkeyword] + hdr[Zendkeyword])
                )  #This is an approximation where we take the mean airmass.
                berv_i = sp.calculateberv(hdr['MJD-OBS'],
                                          hdr['HIERARCH ESO TEL GEOLAT'],
                                          hdr['HIERARCH ESO TEL GEOLON'],
                                          hdr['HIERARCH ESO TEL GEOELEV'],
                                          hdr['RA'], hdr['DEC'])
                berv = np.append(berv, berv_i)
                hdr1d[
                    'HIERARCH ESO QC BERV'] = berv_i  #Append the berv here using the ESPRESSO berv keyword, so that it can be used in molecfit later.
                s1dhdr.append(hdr1d)
                sci_count += 1
                s1d_count += 1
                e2ds_count += 1

    elif mode == 'ESPRESSO':
        catkeyword = 'EXTNAME'
        bervkeyword = 'HIERARCH ESO QC BERV'
        airmass_keyword1 = 'HIERARCH ESO TEL'
        airmass_keyword2 = ' AIRM '
        airmass_keyword3_start = 'START'
        airmass_keyword3_end = 'END'

        for i in range(N):
            if filelist[i].endswith('S2D_A.fits'):
                e2ds_count += 1
                print(filelist[i])
                hdul = fits.open(inpath / filelist[i])
                data = copy.deepcopy(hdul[1].data)
                hdr = hdul[0].header
                hdr2 = hdul[1].header
                wavedata = copy.deepcopy(hdul[5].data)
                hdul.close()
                del hdul[1].data

                if hdr2[catkeyword] == 'SCIDATA':
                    print('science keyword found')
                    framename.append(filelist[i])
                    header.append(hdr)
                    type.append('SCIENCE')
                    texp = np.append(texp, hdr['EXPTIME'])
                    date.append(hdr['DATE-OBS'])
                    mjd = np.append(mjd, hdr['MJD-OBS'])
                    npx = np.append(npx, hdr2['NAXIS1'])
                    norders = np.append(norders, hdr2['NAXIS2'])
                    e2ds.append(data)
                    sci_count += 1
                    berv = np.append(berv, hdr[bervkeyword] * 1000.0)
                    telescope = hdr['TELESCOP'][-1]
                    airmass = np.append(
                        airmass, 0.5 *
                        (hdr[airmass_keyword1 + telescope + ' AIRM START'] +
                         hdr[airmass_keyword1 + telescope + ' AIRM END']))
                    wave.append(wavedata * (1.0 -
                                            (hdr[bervkeyword] * u.km / u.s /
                                             const.c).decompose().value))
                    #Ok.! So unlike HARPS, ESPRESSO wavelengths are BERV corrected in the S2Ds.
                    #WHY!!!?. WELL SO BE IT. IN ORDER TO HAVE E2DSes THAT ARE ON THE SAME GRID, AS REQUIRED, WE UNDO THE BERV CORRECTION HERE.
                    #WHEN COMPARING WAVE[0] WITH WAVE[1], YOU SHOULD SEE THAT THE DIFFERENCE IS NILL.
                    #THATS WHY LATER WE JUST USE WAVE[0] AS THE REPRESENTATIVE GRID FOR ALL.

            if filelist[i].endswith('CCF_A.fits'):
                #ccf,hdr=fits.getdata(inpath+filelist[i],header=True)
                hdul = fits.open(inpath / filelist[i])
                ccf = copy.deepcopy(hdul[1].data)
                hdr = hdul[0].header
                hdr2 = hdul[1].header
                hdul.close()
                del hdul[1].data

                if hdr2[catkeyword] == 'SCIDATA':
                    print('CCF ADDED')
                    #ccftotal+=ccf
                    ccfs.append(ccf)
                    ccfmjd = np.append(ccfmjd, hdr['MJD-OBS'])
                    nrv = np.append(nrv, hdr2['NAXIS1'])
                    ccf_count += 1

            if filelist[i].endswith('S1D_A.fits'):
                hdul = fits.open(inpath / filelist[i])
                data_table = copy.deepcopy(hdul[1].data)
                hdr = hdul[0].header
                hdr2 = hdul[1].header
                hdul.close()
                del hdul[1].data
                if hdr['HIERARCH ESO PRO SCIENCE'] == True:
                    s1d.append(data_table.field(2))
                    wave1d.append(data_table.field(1))
                    s1dhdr.append(hdr)
                    s1dmjd = np.append(s1dmjd, hdr['MJD-OBS'])
                    s1d_count += 1

    else:  #IF we are HARPS-like:
        for i in range(N):
            if filelist[i].endswith('e2ds_A.fits'):
                e2ds_count += 1
                print(filelist[i])

                hdul = fits.open(inpath / filelist[i])
                data = copy.deepcopy(hdul[0].data)
                hdr = hdul[0].header
                hdul.close()
                del hdul[0].data
                if hdr[catkeyword] == 'SCIENCE':
                    framename.append(filelist[i])
                    header.append(hdr)
                    type.append(hdr[catkeyword])
                    texp = np.append(texp, hdr['EXPTIME'])
                    date.append(hdr['DATE-OBS'])
                    mjd = np.append(mjd, hdr['MJD-OBS'])
                    npx = np.append(npx, hdr['NAXIS1'])
                    norders = np.append(norders, hdr['NAXIS2'])
                    e2ds.append(data)
                    sci_count += 1
                    berv = np.append(berv, hdr[bervkeyword])
                    airmass = np.append(
                        airmass, 0.5 * (hdr[Zstartkeyword] + hdr[Zendkeyword])
                    )  #This is an approximation where we take the mean airmass.
                    if nowave == True:
                        #Record which wavefile was used by the pipeline to
                        #create the wavelength solution.
                        wavefile_used.append(hdr[thfilekeyword])
                        wavedata = ut.read_wave_from_e2ds_header(hdr,
                                                                 mode=mode)
                        wave.append(wavedata)
            # else:
            # berv=np.append(berv,np.nan)
            # airmass=np.append(airmass,np.nan)
            if filelist[i].endswith('wave_A.fits'):
                print(filelist[i] + ' (wave)')
                if nowave == True:
                    warnings.warn(
                        " in read_e2ds: nowave was set to True but a wave_A file was detected. This wave file is now ignored in favor of the header.",
                        RuntimeWarning)
                else:
                    wavedata = fits.getdata(inpath / filelist[i])
                    wave.append(wavedata)
                    wave_count += 1
            if filelist[i].endswith('blaze_A.fits'):
                print(filelist[i] + ' (blaze)')
                blazedata = fits.getdata(inpath / filelist[i])
                blaze.append(blazedata)
                blaze_count += 1
            if filelist[i].endswith('s1d_A.fits'):
                hdul = fits.open(inpath / filelist[i])
                data_1d = copy.deepcopy(hdul[0].data)
                hdr = hdul[0].header
                hdul.close()
                del hdul[0].data
                if hdr[catkeyword] == 'SCIENCE':
                    s1d.append(data_1d)
                    s1dhdr.append(hdr)
                    s1dmjd = np.append(s1dmjd, hdr['MJD-OBS'])
                    s1d_count += 1
    #Now we catch some errors:
    #-The above should have read a certain number of e2ds files.
    #-A certain number of these should be SCIENCE frames.
    #-There should be at least one WAVE file.
    #-All exposures should have the same number of spectral orders.
    #-All orders should have the same number of pixels (this is true for HARPS).
    #-The wave frame should have the same dimensions as the order frames.
    #-If nowave is set, test that all frames used the same wave_A calibrator.
    #-The blaze file needs to have the same shape as the e2ds files.
    #-The number of s1d files should be the same as the number of e2ds files.

    if e2ds_count == 0:
        raise FileNotFoundError(
            f"in read_e2ds: The input folder {str(inpath)} does not contain files ending in e2ds.fits."
        )
    if sci_count == 0:
        print('')
        print('')
        print('')
        print("These are the files and their types:")
        for i in range(len(type)):
            print('   ' + framename[i] + '  %s' % type[i])
        raise ValueError(
            "in read_e2ds: The input folder (%2) contains e2ds files, but none of them are classified as SCIENCE frames with the HIERARCH ESO DPR CATG/OBS-TYPE keyword or HIERARCH ESO PRO SCIENCE keyword. The list of frames is printed above."
        )
    if np.max(np.abs(norders - norders[0])) == 0:
        norders = int(norders[0])
    else:
        print('')
        print('')
        print('')
        print("These are the files and their number of orders:")
        for i in range(len(type)):
            print('   ' + framename[i] + '  %s' % norders[i])
        raise ValueError(
            "in read_e2ds: Not all files have the same number of orders. The list of frames is printed above."
        )

    if np.max(np.abs(npx - npx[0])) == 0:
        npx = int(npx[0])
    else:
        print('')
        print('')
        print('')
        print("These are the files and their number of pixels:")
        for i in range(len(type)):
            print('   ' + framename[i] + '  %s' % npx[i])
        raise ValueError(
            "in read_HARPS_e2ds: Not all files have the same number of pixels. The list of frames is printed above."
        )
    if wave_count >= 1:
        wave = wave[0]  #SELECT ONLY THE FIRST WAVE FRAME. The rest is ignored.
    else:
        if nowave == False and mode not in ['UVES-red', 'UVES-blue']:
            print('')
            print('')
            print('')
            print("ERROR in read_e2ds: No wave_A.fits file was detected.")
            print("These are the files in the folder:")
            for i in range(N):
                print(filelist[i])
            print(
                "This may have happened if you downloaded the HARPS data from the"
            )
            print(
                "ADP query form, which doesn't include wave_A files (as far as I"
            )
            print(
                "have seen). Set the /nowave keyword in your call to read_HARPS_e2ds"
            )
            print("if you indeed do not expect a wave_A file to be present.")
            raise FileNotFoundError(
                "No wave_A.fits file was detected. More details are printed above."
            )

    if nowave == True and mode not in [
            'UVES-red', 'UVES-blue', 'ESPRESSO'
    ]:  #This here is peculiar to HARPS/HARPSN.
        if all(x == wavefile_used[0] for x in wavefile_used):
            print(
                "Nowave is set, and simple wavelength calibration extraction")
            print(
                "works, as all files in the dataset used the same wave_A file."
            )
            wave = wave[0]
        else:
            print('')
            print('')
            print('')
            print("These are the filenames and their wave_A file used:")
            for i in range(N - 1):
                print('   ' + framename[i] + '  %s' % wavefile_used[0])
            warnings.warn(
                "in read_e2ds: Nowave is set, but not all files in the dataset used the same wave_A file when the pipeline was run. This script will continue using only the first wavelength solution. Theoretically, this may affect the quality of the data if the solution is wrong (in which case interpolation would be needed), but due to the stability of HARPS this is probably not an issue worth interpolating for.",
                RuntimeWarning)
            wave = wave[0]

    if mode == 'ESPRESSO':
        dimtest(wave1d, np.shape(s1d), 'wave and s1d in read_e2ds()')
        dimtest(wave, np.shape(e2ds), 'wave and e2ds in read_e2ds()')
        diff = wave - wave[0]
        if np.abs(np.nanmax(diff)) > (np.nanmin(wave[0]) / 1e6):
            warnings.warn(
                " in read_e2ds: The wavelength solution over the time series is not constant. I continue by interpolating all the data onto the wavelength solution of the first frame. This will break if the wavelength solutions of your time-series are vastly different, in which case the output will be garbage.",
                RuntimeWarning)
            for ii in range(len(e2ds)):
                if ii > 0:
                    for jj in range(len(e2ds[ii])):
                        e2ds[ii][jj] = interp.interp1d(
                            wave[ii][jj],
                            e2ds[ii][jj],
                            fill_value='extrapolate')(wave[0][jj])
        wave = wave[0]

        diff1d = wave1d - wave1d[0]
        if np.abs(np.nanmax(diff1d)) > (
                np.nanmin(wave1d) / 1e6
        ):  #if this is true, all the wavelength solutions are the same. Great!
            warnings.warn(
                " in read_e2ds: The wavelength solution over the time series of the 1D spectra is not constant. I continue by interpolating all the 1D spectra onto the wavelength solution of the first frame. This will break if the wavelength solutions of your time-series are vastly different, in which case the output will be garbage.",
                RuntimeWarning)
            for ii in range(len(s1d)):
                if ii > 0:
                    s1d[ii] = interp.interp1d(wave1d[ii],
                                              s1d[ii],
                                              fill_value='extrapolate')(
                                                  wave1d[0])
        wave1d = wave1d[0]

    #We are going to test whether the wavelength solution is the same for the
    #entire time series in the case of UVES. In normal use cases this should be true.
    if mode in ['UVES-red', 'UVES-blue']:
        dimtest(wave1d, np.shape(s1d), 'wave and s1d in read_e2ds()')
        dimtest(wave, np.shape(e2ds), 'wave and e2ds in read_e2ds()')

        diff = wave - wave[0]
        if np.abs(
                np.nanmax(diff)
        ) == 0:  #if this is true, all the wavelength solutions are the same. Great!
            wave = wave[0]
        else:
            warnings.warn(
                " in read_e2ds: The wavelength solution over the time series is not constant. I continue by interpolating all the data onto the wavelength solution of the first frame. This will break if the wavelength solutions of your time-series are vastly different, in which case the output will be garbage.",
                RuntimeWarning)
            for ii in range(len(e2ds)):
                if ii > 0:
                    for jj in range(len(e2ds[ii])):
                        e2ds[ii][jj] = interp.interp1d(
                            wave[ii][jj],
                            e2ds[ii][jj],
                            fill_value='extrapolate')(wave[0][jj])
            wave = wave[0]

        diff1d = wave1d - wave1d[0]
        if np.abs(
                np.nanmax(diff1d)
        ) == 0:  #if this is true, all the wavelength solutions are the same. Great!
            wave1d = wave1d[0]
        else:
            warnings.warn(
                " in read_e2ds: The wavelength solution over the time series of the 1D spectra is not constant. I continue by interpolating all the 1D spectra onto the wavelength solution of the first frame. This will break if the wavelength solutions of your time-series are vastly different, in which case the output will be garbage.",
                RuntimeWarning)
            for ii in range(len(s1d)):
                if ii > 0:
                    s1d[ii] = interp.interp1d(wave1d[ii],
                                              s1d[ii],
                                              fill_value='extrapolate')(
                                                  wave1d[0])
            wave1d = wave1d[0]

    if blaze_count >= 1:  #This will only be triggered for HARPS/HARPSN/ESPRESSO
        blaze = blaze[
            0]  #SELECT ONLY THE FIRST blaze FRAME. The rest is ignored.
    if np.shape(wave) != np.shape(
            e2ds[0]):  #If UVES/ESPRESSO, this was already checked implicitly.
        raise ValueError(
            f"in read_e2ds: A wave file was detected but its shape ({np.shape(wave)[0]},{np.shape(wave)[1]}) does not match that of the orders ({np.shape(e2ds[0])[0]},{np.shape(e2ds[0])[1]})"
        )
    if np.shape(blaze) != np.shape(e2ds[0]) and blaze_count > 0:
        raise ValueError(
            f"in read_e2ds: A blaze file was detected but its shape ({np.shape(blaze)[0]},{np.shape(wave)[1]}) does not match that of the orders ({np.shape(e2ds[0])[0]},{np.shape(e2ds[0])[1]})"
        )
    if len(s1dhdr) != len(
            e2ds
    ) and molecfit == True:  #Only a problem if we actually run Molecfit.
        raise ValueError(
            f'in read_e2ds: The number of s1d SCIENCE files and e2ds SCIENCE files is not the same. ({len(s1dhdr)} vs {len(e2ds)})'
        )

    #Ok, so now we should have ended up with a number of lists that contain all
    #the relevant science data and associated information.
    #We determine how to sort the resulting lists in time:
    sorting = np.argsort(mjd)
    s1dsorting = np.argsort(s1dmjd)

    if len(ignore_exp) > 0:
        sorting = [x for i, x in enumerate(sorting) if i not in ignore_exp]
        s1dsorting = [
            x for i, x in enumerate(s1dsorting) if i not in ignore_exp
        ]

    if mode == 'HARPSN':  #In the case of HARPS-N we need to convert the units of the elevation and provide a UTC keyword.
        for i in range(len(header)):
            s1dhdr[i]['TELALT'] = np.degrees(float(s1dhdr[i]['EL']))
            s1dhdr[i]['UTC'] = (float(s1dhdr[i]['MJD-OBS']) % 1.0) * 86400.0

    #Sort the s1d files for application of molecfit.
    if molecfit == True:
        if len(sorting) != len(s1dsorting):
            raise ValueError(
                "in read_HARPS_e2ds: Sorted science frames and sorted s1d frames are not of the same length. Telluric correction can't proceed."
            )

        s1dhdr_sorted = []
        s1d_sorted = []
        for i in range(len(s1dsorting)):
            s1dhdr_sorted.append(s1dhdr[s1dsorting[i]])
            s1d_sorted.append(s1d[s1dsorting[i]])

        print("")
        print("")
        print("")
        print(
            'Molecfit will be executed onto the files with dates in this order:'
        )
        for x in s1dhdr_sorted:
            print(x['DATE-OBS'])
        print("")
        print("")
        print("")

        if mode in ['ESPRESSO', 'UVES-red', 'UVES-blue']:
            list_of_wls, list_of_trans = mol.do_molecfit(s1dhdr_sorted,
                                                         s1d_sorted,
                                                         config,
                                                         load_previous=False,
                                                         mode=mode,
                                                         wave=wave1d)
        else:
            list_of_wls, list_of_trans = mol.do_molecfit(s1dhdr_sorted,
                                                         s1d_sorted,
                                                         config,
                                                         load_previous=False,
                                                         mode=mode)

        if len(list_of_trans) != len(sorting):
            raise ValueError(
                "in read_e2ds(): Molecfit did not produce the same number of spectra as there are in the e2ds spectra."
            )
        mol.write_telluric_transmission_to_file(
            list_of_wls, list_of_trans,
            outpath / 'telluric_transmission_spectra.pkl')

    #Now we loop over all exposures and collect the i-th order from each exposure,
    #put these into a new matrix and save them to FITS images:
    f = open(outpath / 'obs_times', 'w', newline='\n')
    headerline = 'MJD' + '\t' + 'DATE' + '\t' + 'EXPTIME' + '\t' + 'MEAN AIRMASS' + '\t' + 'BERV (km/s)' + '\t' + 'FILE NAME'
    for i in range(norders):
        order = np.zeros((len(sorting), npx))
        wave_axis = wave[i, :] / 10.0  #Convert to nm.
        print('CONSTRUCTING ORDER %s' % i)
        c = 0  #To count the number of science frames that have passed. The counter
        # c is not equal to j because the list of files contains not only SCIENCE
        # frames.

        for j in range(len(sorting)):  #Loop over exposures
            if i == 0:
                print('---' + type[sorting[j]] + '  ' + date[sorting[j]])
            if type[sorting[j]] == 'SCIENCE':  #This check may be redundant.
                exposure = e2ds[sorting[j]]
                order[c, :] = exposure[i, :]
                #T_i = interp.interp1d(list_of_wls[j],list_of_trans[j])#This should be time-sorted, just as the e2ds files.
                #Do a manual check here that the MJDs are identical.
                #Also, determiine what to do with airtovac.
                #tel_order[c,:] = T_i[wave_axis]
                #Now I also need to write it to file.
                if i == 0:  #Only do it the first time, not for every order.
                    line = str(mjd[sorting[j]]) + '\t' + date[sorting[
                        j]] + '\t' + str(texp[sorting[j]]) + '\t' + str(
                            np.round(airmass[sorting[j]], 3)) + '\t' + str(
                                np.round(
                                    berv[sorting[j]],
                                    5)) + '\t' + framename[sorting[j]] + '\n'
                    f.write(line)
                c += 1

        fits.writeto(outpath / ('order_' + str(i) + '.fits'),
                     order,
                     overwrite=True)
        fits.writeto(outpath / ('wave_' + str(i) + '.fits'),
                     wave_axis,
                     overwrite=True)
    f.close()
    print(f'Time-table written to {outpath/"obs_times"}.')
Ejemplo n.º 28
0
def do_molecfit(headers,
                spectra,
                configfile,
                wave=[],
                mode='HARPS',
                load_previous=False):
    """This is the main wrapper for molecfit that pipes a list of s1d spectra and
    executes it. It first launces the molecfit gui on the middle spectrum of the
    sequence, and then loops through the entire list, returning the transmission
    spectra of the Earths atmosphere in the same order as the list provided.
    These can then be used to correct the s1d spectra or the e2ds spectra.
    Note that the s1d spectra are assumed to be in the barycentric frame in vaccuum,
    but that the output transmission spectrum is in the observers frame, and e2ds files
    are in air wavelengths by default.

    If you have run do_molecfit before, and want to reuse the output of the previous run
    for whatever reason, set the load_previous keyword to True. This will reload the
    list of transmission spectra created last time, if available.
    """

    import pdb
    import numpy as np
    import matplotlib.pyplot as plt
    import sys
    import os.path
    import pickle
    import copy
    from pathlib import Path
    import tayph.util as ut
    import tayph.system_parameters as sp
    configfile = ut.check_path(configfile, exists=True)

    molecfit_input_folder = sp.paramget('molecfit_input_folder',
                                        configfile,
                                        full_path=True)
    molecfit_prog_folder = sp.paramget('molecfit_prog_folder',
                                       configfile,
                                       full_path=True)
    temp_specname = copy.deepcopy(
        mode)  #The name of the temporary file used (without extension).
    #The spectrum will be named like this.fits There should be a this.par file as well,
    #that contains a line pointing molecfit to this.fits:
    parname = temp_specname + '.par'

    #====== ||  START OF PROGRAM   ||======#
    N = len(headers)
    if N != len(spectra):
        raise RuntimeError(
            f' in prep_for_molecfit: Length of list of headers is not equal to length of list of spectra ({N},{len(spectra)})'
        )

    #Test that the input root and molecfit roots exist; that the molecfit root contains the molecfit executables.
    #that the input root contains the desired parfile and later fitsfile.
    molecfit_input_root = Path(molecfit_input_folder)
    molecfit_prog_root = Path(molecfit_prog_folder)

    ut.check_path(molecfit_input_root, exists=True)
    ut.check_path(molecfit_prog_root, exists=True)
    ut.check_path(
        molecfit_input_root / parname, exists=True
    )  #Test that the molecfit parameter file for this mode exists.
    ut.check_path(molecfit_prog_root / 'molecfit', exists=True)
    ut.check_path(molecfit_prog_root / 'molecfit_gui', exists=True)

    pickle_outpath = molecfit_input_root / 'previous_run_of_do_molecfit.pkl'

    if load_previous == True:
        if os.path.isfile(pickle_outpath) == False:
            print(
                'WARNING in do_molecfit(): Previously saved run was asked for but is not available.'
            )
            print(
                'The program will proceed to re-fit. That run will then be saved.'
            )
            load_previous = False
        else:
            pickle_in = open(pickle_outpath, "rb")
            list_of_wls, list_of_fxc, list_of_trans = pickle.load(pickle_in)

    if load_previous == False:
        list_of_wls = []
        list_of_fxc = []
        list_of_trans = []

        middle_i = int(
            round(0.5 * N)
        )  #We initialize molecfit on the middle spectrum of the time series.
        write_file_to_molecfit(molecfit_input_root,
                               temp_specname + '.fits',
                               headers,
                               spectra,
                               middle_i,
                               mode=mode,
                               wave=wave)
        print(molecfit_input_root)
        print(temp_specname + '.fits')
        execute_molecfit(molecfit_prog_root,
                         molecfit_input_root / parname,
                         gui=True)
        wl, fx, trans = retrieve_output_molecfit(molecfit_input_root /
                                                 temp_specname)
        remove_output_molecfit(molecfit_input_root, temp_specname)
        for i in range(N):  #range(len(spectra)):
            print('Fitting spectrum %s from %s' % (i + 1, len(spectra)))
            t1 = ut.start()
            write_file_to_molecfit(molecfit_input_root,
                                   temp_specname + '.fits',
                                   headers,
                                   spectra,
                                   i,
                                   mode=mode,
                                   wave=wave)
            execute_molecfit(molecfit_prog_root,
                             molecfit_input_root / parname,
                             gui=False)
            wl, fx, trans = retrieve_output_molecfit(molecfit_input_root /
                                                     temp_specname)
            remove_output_molecfit(molecfit_input_root, temp_specname)
            list_of_wls.append(wl * 1000.0)  #Convert to nm.
            list_of_fxc.append(fx / trans)
            list_of_trans.append(trans)
            ut.end(t1)

        pickle_outpath = molecfit_input_root / 'previous_run_of_do_molecfit.pkl'
        with open(pickle_outpath, 'wb') as f:
            pickle.dump((list_of_wls, list_of_fxc, list_of_trans), f)

    to_do_manually = check_fit_gui(list_of_wls, list_of_fxc, list_of_trans)
    if len(to_do_manually) > 0:
        print('The following spectra were selected to redo manually:')
        print(to_do_manually)
        #CHECK THAT THIS FUNCIONALITY WORKS:
        for i in to_do_manually:
            write_file_to_molecfit(molecfit_input_root,
                                   temp_specname + '.fits',
                                   headers,
                                   spectra,
                                   int(i),
                                   mode=mode,
                                   wave=wave)
            execute_molecfit(molecfit_prog_root,
                             molecfit_input_root / parname,
                             gui=True)
            wl, fx, trans = retrieve_output_molecfit(molecfit_input_root /
                                                     temp_specname)
            list_of_wls[int(i)] = wl * 1000.0  #Convert to nm.
            list_of_fxc[int(i)] = fxc
            list_of_trans[int(i)] = trans
    return (list_of_wls, list_of_trans)
Ejemplo n.º 29
0
def mask_orders(list_of_wls,list_of_orders,dp,maskname,w,c_thresh,manual=False):
    """
    This code takes the list of orders and masks out bad pixels.
    It combines two steps, a simple sigma clipping step and a manual step, where
    the user can interactively identify bad pixels in each order. The sigma
    clipping is done on a threshold of c_thresh, using a rolling standard dev.
    with a width of w pixels. Manual masking is a big routine needed to support
    a nice GUI to do that.

    If c_thresh is set to zero, sigma clipping is skipped. If manual=False, the
    manual selection of masking regions (which is manual labour) is turned off.
    If both are turned off, the list_of_orders is returned unchanged.

    If either or both are active, the routine will output 1 or 2 FITS files that
    contain a stack (cube) of the masks for each order. The first file is the mask
    that was computed automatically, the second is the mask that was constructed
    manually. This is done so that the manual mask can be transplanted onto another
    dataset, or saved under a different file-name, to limit repetition of work.

    At the end of the routine, the two masks are merged into a single list, and
    applied to the list of orders.
    """
    import tayph.operations as ops
    import numpy as np
    import tayph.functions as fun
    import tayph.plotting as plotting
    import sys
    import matplotlib.pyplot as plt
    import tayph.util as ut
    import warnings
    from tayph.vartests import typetest,dimtest,postest
    ut.check_path(dp)
    typetest(maskname,str,'maskname in mask_orders()')
    typetest(w,[int,float],'w in mask_orders()')
    typetest(c_thresh,[int,float],'c_thresh in mask_orders()')
    postest(w,'w in mask_orders()')
    postest(c_thresh,'c_thresh in mask_orders()')
    typetest(list_of_wls,list,'list_of_wls in mask_orders()')
    typetest(list_of_orders,list,'list_of_orders in mask_orders()')
    typetest(manual,bool,'manual keyword in mask_orders()')
    dimtest(list_of_wls,[0,0],'list_of_wls in mask_orders()')
    dimtest(list_of_orders,[len(list_of_wls),0,0],'list_of_orders in mask_orders()')

    if c_thresh <= 0 and manual == False:
        print('---WARNING in mask_orders: c_thresh is set to zero and manual masking is turned off.')
        print('---Returning orders unmasked.')
        return(list_of_orders)

    N = len(list_of_orders)
    void = fun.findgen(N)

    list_of_orders = ops.normalize_orders(list_of_orders,list_of_orders)[0]#first normalize. Dont want outliers to
    #affect the colour correction later on, so colour correction cant be done before masking, meaning
    #that this needs to be done twice; as colour correction is also needed for proper maskng. The second variable is
    #a dummy to replace the expected list_of_sigmas input.
    N_NaN = 0
    list_of_masked_orders = []

    for i in range(N):
        list_of_masked_orders.append(list_of_orders[i])

    list_of_masks = []

    if c_thresh > 0:#Check that c_thresh is positive. If not, skip sigma clipping.
        print('------Sigma-clipping mask')
        for i in range(N):
            order = list_of_orders[i]
            N_exp = np.shape(order)[0]
            N_px = np.shape(order)[1]
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", category=RuntimeWarning)
                meanspec = np.nanmean(order,axis = 0)
            meanblock = fun.rebinreform(meanspec,N_exp)
            res = order / meanblock - 1.0
            sigma = fun.running_MAD_2D(res,w)
            with np.errstate(invalid='ignore'):#https://stackoverflow.com/questions/25345843/inequality-comparison-of-numpy-array-with-nan-to-a-scalar
                sel = np.abs(res) >= c_thresh*sigma
                N_NaN += np.sum(sel)#This is interesting because True values count as 1, and False as zero.
                order[sel] = np.nan
            list_of_masks.append(order*0.0)
            ut.statusbar(i,void)

        print(f'%s outliers identified and set to NaN ({N_NaN}/{round(N_NaN/np.size(list_of_masks)*100.0,3)}).')
    else:
        print('------Skipping sigma-clipping (c_thres <= 0)')
        #Do nothing to list_of_masks. It is now an empty list.
        #We now automatically proceed to manual masking, because at this point
        #it has already been established that it must have been turned on.


    list_of_masks_manual = []
    if manual == True:


        previous_list_of_masked_columns = load_columns_from_file(dp,maskname,mode='relaxed')
        list_of_masked_columns = manual_masking(list_of_wls,list_of_orders,list_of_masks,saved = previous_list_of_masked_columns)
        print('------Successfully concluded manual mask.')
        write_columns_to_file(dp,maskname,list_of_masked_columns)

        print('------Building manual mask from selected columns')
        for i in range(N):
            order = list_of_orders[i]
            N_exp = np.shape(order)[0]
            N_px = np.shape(order)[1]
            list_of_masks_manual.append(np.zeros((N_exp,N_px)))
            for j in list_of_masked_columns[i]:
                list_of_masks_manual[i][:,int(j)] = np.nan

    #We write 1 or 2 mask files here. The list of manual masks
    #and list_of_masks (auto) are either filled, or either is an emtpy list if
    #c_thresh was set to zero or manual was set to False (because they were defined
    #as empty lists initially, and then not filled with anything).
    write_mask_to_file(dp,maskname,list_of_masks,list_of_masks_manual)
    return(0)
Ejemplo n.º 30
0
def get_model(name,library='models/library',root='models',is_binary=False):
    """This program queries a model from a library file, with predefined models
    for use in model injection, cross-correlation and plotting. These models have
    a standard format. They are 2 rows by N points, where N corresponds to the
    number of wavelength samples. The first row contains the wavelengths, the
    second the associated flux values. The library file has two columns that are
    formatted as follows:

    modelname  modelpath
    modelname  modelpath
    modelname  modelpath

    modelpath starts in the models/ subdirectory.
    Set the root variable if a location is required other than the ./models directory.

    Example call:
    wlm,fxm = get_model('WASP-121_TiO',library='models/testlib')

    Set the is_binary keyword to ask whether the file is a binary .dat file or not, and return a
    single boolean. Used to switch between cross-correlation and template construction modes.
    """

    from tayph.vartests import typetest,dimtest
    from tayph.util import check_path
    from astropy.io import fits
    from pathlib import Path
    import errno
    import os
    import numpy as np
    typetest(name,str,'name in mod.get_model()')
    library=check_path(library,exists=True)



    #First open the library file.
    f = open(library, 'r')
    x = f.read().splitlines()#Read everything into a big string array.
    f.close()
    n_lines=len(x)#Number of models in the library.
    models={}#This will contain the model names.

    for i in range(0,n_lines):
        line=x[i].split()
        value=(line[1])
        models[line[0]] = value

    try:
        modelpath=Path(models[name])
    except KeyError:
        raise KeyError(f'Model {name} is not present in library at {str(library)}') from None
    if str(modelpath)[0]!='/':#Test if this is an absolute path.
        root=check_path(root,exists=True)
        modelpath=root/modelpath
    if modelpath.exists():
        if modelpath.suffix == '.fits':
            if is_binary: return(False)
            modelarray=fits.getdata(modelpath)#Two-dimensional array with wl on the first row and
            #spectral flux on the second row.
        elif modelpath.suffix == '.dat':
            if is_binary: return(True)
            modelarray = np.loadtxt(modelpath).T#Two-dimensional array with wavelength positions of
            #spectral lines on the first column and weights on the second column (transposed).
        else:
            raise RunTimeError(f'Model file {modelpath} from library {str(library)} is required to '
            'have extension .fits or .dat.')
    else:
        raise FileNotFoundError(f'Model file {modelpath} from library {str(library)} does not '
        'exist.')
    dimtest(modelarray,[2,0])
    return(modelarray[0,:],modelarray[1,:])