#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from copy import deepcopy
import numpy as np
import pandas as pd
import bindensity as bind
from lmfit import Parameters
from scipy.fft import fft
from scipy.fft import fft2
from ..ANTARESS_general.minim_routines import call_lmfit
from ..ANTARESS_grids.ANTARESS_coord import excl_plrange
from ..ANTARESS_process.ANTARESS_data_align import align_data
from ..ANTARESS_general.utils import dataload_npz,stop,closest,gen_specdopshift,np_where1D,check_data
from ..ANTARESS_general.constant_data import c_light



def detrend_prof(detrend_prof_dic,data_dic,coord_dic,inst,vis,CCF_dic,data_prop,gen_dic,plot_dic):
    r"""**Detrending: main routine.**
    
    Corrects disk-integrated line profiles for abnormal variations
    
     - variations in line contrast, FWHM, and RV should first be fitted within the `plot_dic['prop_DI']` routine to derive their model.
       Model coefficients can then be set in the detrending module.
     - better results are obtained by correcting first for the constrast, before the RV.
       FWHM must be corrected after RVs, so that line profiles are aligned
     - the detrending module is called before the analysis module so that corrected profiles can then be re-analyzed
     - the detrending module requires that aligned profiles have already been calculated, as the RV model has been fitted to profiles corrected for the star motion.
       After correction the routine resets the path of current (and aligned) disk-integrated data to corrected profiles, which will not go through the alignment module again 
     - for modulations the coefficient of degree 0 is not required, as models are normalized to the mean

    Args:
        TBD
    
    Returns:
        TBD
        
    """
    cond_trend = (detrend_prof_dic['corr_trend'] and (inst in detrend_prof_dic['prop']) and (vis in detrend_prof_dic['prop'][inst]))
    cond_PC = (detrend_prof_dic['corr_PC'] and (inst in detrend_prof_dic['PC_model']) and (vis in detrend_prof_dic['PC_model'][inst]))

    #Specific instrumental correction
    if (inst=='EXPRES') and (gen_dic['star_name']=='55Cnc'):cond_custom = True
    else:cond_custom = False

    if cond_trend or cond_PC or cond_custom:
        data_vis=data_dic[inst][vis]
        print('   > Correcting disk-integrated line profiles')
        
        #Calculating aligned data
        if gen_dic['calc_detrend_prof']:
            print('         Calculating data')
            data_prop_vis=data_prop[inst][vis]
            pl_loc = data_vis['transit_pl'][0]
            
            #Correct spectrum
            if ('spec' in data_dic[inst][vis]['type']):
                
                #Single line, processed in RV space
                if detrend_prof_dic['line_trans'] is not None:
                    single_l = True
                    iord_line = detrend_prof_dic['iord_line']
                    nord_eff = 1
                    dim_exp_eff=[nord_eff,data_dic[inst][vis]['nspec']]  
                    
                #Correct full spectrum
                else:
                    single_l = False
                    iord_line = None
                    nord_eff = data_dic[inst]['nord']
                    dim_exp_eff = data_dic[inst][vis]['dim_exp']
                 
            #Correct CCF
            else:
                single_l = True
                iord_line = 0
                nord_eff = data_dic[inst]['nord']
                dim_exp_eff = data_dic[inst][vis]['dim_exp']
                
            #------------------------------------------
            #Trend corrections
            if cond_trend:
                print('           Correcting for trends')
                corr_prop = detrend_prof_dic['prop'][inst][vis]
            
                #Default orders for SNR
                if (inst in detrend_prof_dic['SNRorders']):SNRorders_inst = detrend_prof_dic['SNRorders'][inst]
                else:SNRorders_inst = {'HARPS':[49],'HARPN':[46],'ESPRESSO_MR':[39],'ESPRESSO':[102,103],'CARMENES_VIS':[40],
                                       'NIRPS_HA':[57],'NIRPS_HE':[57],    #H band, 1.63 mic, order not affected by tellurics thus stable for SNR measurement
                                       'EXPRES':[14]}[inst]                #562 nm

                #Initialize corrections
                #    - FWHM corrections can only be applied to single lines
                corr_list = list(corr_prop.keys())
                prop_corr = [corr_loc.split('_')[0] for corr_loc in corr_list]
                var_corr = [corr_loc.split('_')[1] for corr_loc in corr_list]
             
                if np.any([corr_loc=='RV' for corr_loc in prop_corr]):corr_RV=True
                else:corr_RV=False
                if np.any([corr_loc=='ctrst' for corr_loc in prop_corr]):
                    corr_ctrst=True
                    cond_def_cont_all  = np.zeros(data_vis['dim_ord'],dtype=bool)
                else:corr_ctrst=False            
                if np.any([corr_loc=='FWHM' for corr_loc in prop_corr]):
                    if single_l:corr_FWHM=True
                    else:stop('WARNING: FWHM correction cannot be applied to multiple spectral lines')
                else: corr_FWHM=False
                
                #Sanity check
                if corr_FWHM and data_dic['DI']['sysvel'][inst][vis]==0.:
                    print('         WARNING: sysvel = 0 km/s. The correct sysvel must be defined for the FWHM correction to be applied.')
                
                #Defining corrections
                glob_corr_ctrst = np.ones(data_vis['n_in_visit'],dtype=float)
                glob_corr_FWHM = np.ones(data_vis['n_in_visit'],dtype=float)
                glob_corr_RV = np.zeros(data_vis['n_in_visit'],dtype=float)
                for iexp in range(data_vis['n_in_visit']):
    
                    #SNR in chosen spectral orders
                    #    - indexes relative to original orders
                    if np.any(['snr' in corr_loc for corr_loc in var_corr]):
                        SNR_exp_ord = data_prop_vis['SNRs'][iexp] 
                        SNR_exp =np.mean(SNR_exp_ord[SNRorders_inst])
                        if np.any([corr_loc=='snrQ' for corr_loc in var_corr]):SNRQ_exp =np.sqrt(np.sum(SNR_exp_ord[SNRorders_inst]**2.))                 
                        
                    #Orbital phase
                    if np.any([corr_loc=='phase' for corr_loc in var_corr]):
                        cen_ph_exp=coord_dic[inst][vis][pl_loc]['cen_ph'][iexp] 
                 
                    #Airmass and activity indexes
                    env_prop = {}
                    if np.any([corr_loc=='AM' for corr_loc in var_corr]):
                        env_prop['AM']=data_prop_vis['AM'][iexp]     
                    for key in data_vis['act_idx']:
                        if np.any([corr_loc==key for corr_loc in var_corr]):
                            env_prop[key]=data_prop_vis[key][iexp,0]               
                
                    #----------------------------------------------------------------------------------------                    
                    #Polynomial corrections for contrast variation
                    #    - only the modulation around the constant value is corrected for
                    #---------------------------------------------------------------------------------------- 
                    if corr_ctrst:            
                        
                        #Upload latest processed DI data
                        data_exp = dataload_npz(data_vis['proc_DI_data_paths']+str(iexp))
  
                        #Continuum for single line
                        if single_l:
                          
                            #Initializing continuum ranges in the input rest frame to defined pixels over requested ranges 
                            for bd_int in data_dic['DI']['cont_range'][inst][iord_line]:
                                cond_def_cont_all[iexp] |= (data_exp['edge_bins'][0,0:-1]>=bd_int[0]) & (data_exp['edge_bins'][0,1:]<=bd_int[1])    
                            cond_def_cont_all[iexp] &= data_exp['cond_def'][0]
                            
                            #Exclusion of planetary ranges
                            if ('DI_prof' in data_dic['Atm']['no_plrange']) and (iexp in data_dic['Atm'][inst][vis]['iexp_no_plrange']):  
                                cond_def_cont_all[iexp] &= excl_plrange(data_exp['cond_def'][0],data_dic['Atm'][inst][vis]['exclu_range_input'],iexp,data_exp['edge_bins'][0],data_dic[inst][vis]['type'])[0]
    
                        #------------------------
    
                        #With phase   
                        if 'ctrst_phase' in corr_list:glob_corr_ctrst[iexp] = detrend_prof_gen(corr_prop['ctrst_phase'],cen_ph_exp,glob_corr_ctrst[iexp],'modul')
                        
                        #With SNR           
                        if 'ctrst_snr' in corr_list:glob_corr_ctrst[iexp] = detrend_prof_gen(corr_prop['ctrst_snr'],SNR_exp,glob_corr_ctrst[iexp],'modul')
                        elif 'ctrst_snrQ' in corr_list:glob_corr_ctrst[iexp] = detrend_prof_gen(corr_prop['ctrst_snrQ'],SNRQ_exp,glob_corr_ctrst[iexp],'modul')
    
                        #With airmass and indexes
                        for key in env_prop:
                            if 'ctrst_'+key in corr_list:glob_corr_ctrst[iexp] = detrend_prof_gen(corr_prop['ctrst_'+key],env_prop[key],glob_corr_ctrst[iexp],'modul')
                            
                    #----------------------------------------------------------------------------------------                    
                    #Polynomial corrections for FWHM variation
                    #    - only the modulation around the constant value is corrected for
                    #---------------------------------------------------------------------------------------- 
                    if corr_FWHM:                   
    
                        #With phase   
                        if 'FWHM_phase' in corr_list:glob_corr_FWHM[iexp] = detrend_prof_gen(corr_prop['FWHM_phase'],cen_ph_exp,glob_corr_FWHM[iexp],'modul')
                          
                        #With SNR           
                        if 'FWHM_snr' in corr_list:glob_corr_FWHM[iexp] = detrend_prof_gen(corr_prop['FWHM_snr'],SNR_exp,glob_corr_FWHM[iexp],'modul')
                        elif 'FWHM_snrQ' in corr_list:glob_corr_FWHM[iexp] = detrend_prof_gen(corr_prop['FWHM_snrQ'],SNRQ_exp,glob_corr_FWHM[iexp],'modul')
                        
                        #With airmass and indexes
                        for key in env_prop:
                            if 'FWHM_'+key in corr_list:glob_corr_FWHM[iexp] = detrend_prof_gen(corr_prop['FWHM_'+key],env_prop[key],glob_corr_FWHM[iexp],'modul')
    
                    #----------------------------------------------------------------------------------------                 
                    #Polynomial correction for deviation to Keplerian RV curve (km/s)
                    #    - the full polynomial variation is corrected for
                    #----------------------------------------------------------------------------------------       
                    if corr_RV:    
                        
                        #With phase
                        if 'RV_phase' in corr_list:
                            glob_corr_RV[iexp] = detrend_prof_gen(corr_prop['RV_phase'],cen_ph_exp,glob_corr_RV[iexp],'add')
                            
                        #With SNR
                        if 'RV_snr' in corr_list:
                            glob_corr_RV[iexp] = detrend_prof_gen(corr_prop['RV_snr'],SNR_exp,glob_corr_RV[iexp],'add')
    
                        elif 'RV_snrQ' in corr_list:
                            glob_corr_RV[iexp] = detrend_prof_gen(corr_prop['RV_snrQ'],SNRQ_exp,glob_corr_RV[iexp],'add')
    
                        #With airmass and indexes
                        for key in env_prop:
                            if 'RV_'+key in corr_list:glob_corr_RV[iexp] = detrend_prof_gen(corr_prop['RV_'+key],env_prop[key],glob_corr_RV[iexp],'add')
    
                #Normalizing around the mean
                glob_corr_ctrst/=np.mean(glob_corr_ctrst)                    
                glob_corr_FWHM/=np.mean(glob_corr_FWHM)                     
      
                #Continuum for contrast corrections
                if corr_ctrst:
                
                    #Continuum common to all input single line profiles 
                    if single_l:
                        cond_cont_com  = np.all(cond_def_cont_all,axis=0)
                        if np.sum(cond_cont_com)==0.:stop('No pixels in common continuum')
                        cont_func_dic= None
                    
                    #Continuum of stellar spectrum
                    else:
                        cond_cont_com=None
                        cont_func_dic = dataload_npz(gen_dic['save_data_dir']+'Stellar_cont_DI/'+inst+'_'+vis+'/St_cont')['cont_func_dic']

            #------------------------------------------
            #Custom correction
            if cond_custom:
                print('           Applying custom correction')
                cust_corr_RV = np.zeros(data_vis['n_in_visit'],dtype=float)
                corr_data = ((pd.read_csv('/Users/bourrier/Travaux/Exoplanet_systems/Divers/55_cancri/RM/RMR/Data/EXPRES/220728_55CnceTransit_detrend.csv')).values).T
                if vis=='20220131':JD_corr = corr_data[1]-2400000. 
                if vis=='20220406':JD_corr = corr_data[1]-2400000. 
                RV_corr = corr_data[2]*1e-3   
                if vis=='20220406':RV_corr*=-1. 
                for iexp in range(data_vis['n_in_visit']):
                    icorr = closest(JD_corr,coord_dic[inst][vis]['bjd'][iexp])
                    cust_corr_RV[iexp]=RV_corr[icorr]
         
            #------------------------------------------
            #PC corrections
            if cond_PC:
                print('           Correcting for PC')
                
                #PCA results
                pca_results = np.load(detrend_prof_dic['PC_model'][inst][vis]['all'],allow_pickle=True)['data'].item() 

                #Joint intrinsic fit
                if 'in' in detrend_prof_dic['PC_model'][inst][vis]:
                    jointfit_results = np.load(detrend_prof_dic['PC_model'][inst][vis]['in'],allow_pickle=True)['data'].item() 
                    if pca_results['n_pc']!=jointfit_results['n_pc'][inst][vis]:stop('Number of fitted PC must match')
                else:jointfit_results=None
                
                #PC profiles fitted to the residual and intrinsic data
                eig_res_matr = pca_results['eig_res_matr'][0:pca_results['n_pc']]
                
                #PC profiles selected to generate the model
                if (inst in detrend_prof_dic['idx_PC']) and (vis in detrend_prof_dic['idx_PC'][inst]):idx_pc = detrend_prof_dic['idx_PC'][inst][vis]
                else:idx_pc = range(pca_results['n_pc'])
                
            #----------------------------------------------------------------------------------------               

            #Resample aligned profiles on the common visit table if relevant
            if (data_vis['comm_sp_tab']):
                data_com = dataload_npz(data_vis['proc_com_data_paths'])
                cen_bins_resamp, edge_bins_resamp = data_com['cen_bins'],data_com['edge_bins']
            else:cen_bins_resamp, edge_bins_resamp = None,None 

            #Correct each exposure  
            proc_DI_data_paths_new = gen_dic['save_data_dir']+'Detrend_prof/'+inst+'_'+vis+'_'                          
            for iexp in range(data_vis['n_in_visit']):
        
                #Upload latest processed DI data
                data_exp = dataload_npz(data_vis['proc_DI_data_paths']+str(iexp))                  
 
                #Switch spectrum into RV space
                #    - upon radial velocity table associated with chosen transition
                if ('spec' in data_dic[inst][vis]['type']) and (detrend_prof_dic['line_trans'] is not None):
                    data_corr = {'flux' : np.array([data_exp['flux'][iord_line]]),
                                 'cov' : np.array([data_exp['cov'][iord_line]]),
                                 'edge_bins' : np.array([c_light*( (data_exp['edge_bins'][iord_line]/detrend_prof_dic['line_trans']) - 1.)]),
                                 'cen_bins' : np.array([c_light*( (data_exp['cen_bins'][iord_line]/detrend_prof_dic['line_trans']) - 1.)])}
 
                #Correct CCF or full spectrum
                else:data_corr = data_exp
                 
                #---------------------------------  
                #Trend corrections
                if cond_trend:

                    #Correcting for RV variation    
                    if corr_RV:
                        data_corr = align_data(data_corr,data_dic[inst][vis]['type'],nord_eff,dim_exp_eff,gen_dic['resamp_mode'],cen_bins_resamp, edge_bins_resamp,glob_corr_RV[iexp],1./gen_specdopshift(glob_corr_RV[iexp]))

                    #Correcting for contrast variation 
                    if corr_ctrst:                 
                        data_corr = detrend_prof_ctrst(data_corr,single_l,nord_eff,data_dic[inst][vis]['nspec'],glob_corr_ctrst[iexp],cond_cont_com,cont_func_dic,coord_dic[inst][vis]['RV_star_stelCDM'][iexp],data_dic['DI']['sysvel'][inst][vis],
                                                         gen_dic['save_data_dir']+'Scaled_data/'+inst+'_'+vis+'_scaling_'+str(iexp),coord_dic[inst][vis]['t_dur'][iexp])

                    #Correcting for FWHM variation 
                    #    - for CCF and single spectral lines 
                    if corr_FWHM and single_l:                          
                        glob_corr=np.repeat(glob_corr_FWHM[iexp],data_vis['nspec']+1)
                        data_corr = detrend_prof_FWHM(data_corr,glob_corr,coord_dic[inst][vis]['RV_star_solCDM'][iexp],gen_dic['resamp_mode'],comm_sp_tab=data_vis['comm_sp_tab'])

                #---------------------------------  
                #Custom correction
                if cond_custom:
                    data_corr['cen_bins'][0] -= cust_corr_RV[iexp]
                    data_corr['edge_bins'][0] -= cust_corr_RV[iexp]
      
                #---------------------------------      
                #PC correction
                #    - see PCA module for details
                if cond_PC: 
                    i_in = gen_dic[inst][vis]['idx_exp2in'][iexp]
                    
                    #Correct exposure if included in PCA module or joint intrinsic fit
                    if (iexp in pca_results['idx_corr']) or ((jointfit_results is not None) and (i_in in jointfit_results['idx_in_fit'][inst][vis])):
        
                        #Upload flux scaling 
                        data_scaling = dataload_npz(gen_dic['save_data_dir']+'Scaled_data/'+inst+'_'+vis+'_scaling_'+str(iexp))

                        #Switch PCA spectrum into RV space
                        if ('spec' in data_dic[inst][vis]['type']) and (detrend_prof_dic['line_trans'] is not None):
                            bins_edge_PCA = c_light*( (pca_results['edge_bins'][0]/detrend_prof_dic['line_trans']) - 1.)  
                        else:bins_edge_PCA = pca_results['edge_bins'][0]                           

                        #Residual PC model from intrinsic profile fit
                        pc_mod_exp = np.zeros(data_vis['nspec'],dtype=float)
                        if ((jointfit_results is not None) and (i_in in jointfit_results['idx_in_fit'][inst][vis])):
                     
                            #Intrinsic PC model
                            for i_pc in idx_pc:pc_mod_exp+=jointfit_results['p_final']['aPC_idxin'+str(i_in)+'_ord'+str(i_pc)+'__IS'+inst+'_VS'+vis]*eig_res_matr[i_pc]
                            
                            #Scaling to level of residual data 
                            #    - see PCA module, the intrinsic PC model corresponds to :
                            # Pfit = -Pert(w,t,v)*LC_theo(band,t)*Cref(band,v)/(1 - LC_theo(band,t)) 
                            #      which can be scaled to the level of the disk-integrated flux by
                            # globF(v,t)*(1 - LC_theo(band,t))/LC_theo(band,t)
                            LC_theo_exp = 1. - data_scaling['loc_flux_scaling'][iexp](bins_edge_PCA)
                            pc_mod_exp*= (1. - LC_theo_exp)/LC_theo_exp
                      
                        #Residual PC model from PCA analysis
                        elif (iexp in pca_results['idx_corr']):
                            for i_pc in idx_pc:pc_mod_exp+=pca_results['p_final']['aPC_idx'+str(iexp)+'_ord'+str(i_pc)+'__IS'+inst+'_VS'+vis]*eig_res_matr[i_pc]
               
                        #Save residual model for plotting purposes
                        if plot_dic['map_pca_prof']!='':
                            np.savez_compressed(gen_dic['save_data_dir']+'PCA_results/'+inst+'_'+vis+'_model'+str(iexp) ,data = {'flux':np.array([pc_mod_exp]),'edge_bins' :np.array([bins_edge_PCA]),'cond_def':np.ones(data_dic[inst][vis]['dim_exp'],dtype=bool) },allow_pickle=True)
                
                        #Scale to the level of disk-integrated flux density
                        pc_mod_exp*=data_scaling['glob_flux_scaling'][iexp]
  
                        #Temporary exposure table centered in star rest frame
                        edge_bins_shift = data_corr['edge_bins'][0] - coord_dic[inst][vis]['RV_star_solCDM'][iexp] 
                      
                        #PC model interpolated over exposure table
                        pc_mod_exp = bind.resampling(edge_bins_shift,bins_edge_PCA, pc_mod_exp, kind=gen_dic['resamp_mode'])  
                     
                        #Correction
                        data_corr['flux'][0]+=pc_mod_exp
    
                #-----------------------------------------------------
                #Saving corrected data
                #----------------------------------------------------- 
    
                #Switch spectrum back into wavelength space
                #    - wave = wave_ref*(1 + rv/c)
                if ('spec' in data_dic[inst][vis]['type']) and (detrend_prof_dic['line_trans'] is not None):
                    data_exp['flux'][iord_line] = data_corr['flux'][0]
                    data_exp['cov'][iord_line] = data_corr['cov'][0]
                    data_exp['edge_bins'][iord_line] = detrend_prof_dic['line_trans']*gen_specdopshift(data_corr['edge_bins'][0])     
                    data_exp['cen_bins'][iord_line]  = detrend_prof_dic['line_trans']*gen_specdopshift(data_corr['cen_bins'][0])                      
                else:data_exp = data_corr
    
                #Updating defined bins
                data_exp['cond_def'] = ~np.isnan(data_exp['flux'])
    
                #Saving corrected data and updating paths 
                np.savez_compressed(proc_DI_data_paths_new+str(iexp) ,data = data_exp,allow_pickle=True)
            
            ### end of exposure    
            data_vis['proc_DI_data_paths'] = proc_DI_data_paths_new
         
        else: 
            #Updating path to processed data and checking it has been calculated
            data_vis['proc_DI_data_paths'] = gen_dic['save_data_dir']+'Detrend_prof/'+inst+'_'+vis+'_' 
            check_data({'path':data_vis['proc_DI_data_paths']+str(0)})

    return None


def detrend_prof_gen(corr_prop_in,var,corr_in,mode):
    """**Detrending: generic model.**
    
    Calculates polynomial or sinusoidal model, multiplicative or cumulative.

    Args:
        TBD
    
    Returns:
        TBD
    
    """          
    corr_out = deepcopy(corr_in)
    
    #Modulated variation
    if mode=='modul':
        if ('sin' in corr_prop_in):
            loc_corr = (1. + corr_prop_in['sin'][0]*np.sin(2*np.pi*((var-corr_prop_in['sin'][1])/corr_prop_in['sin'][2])))
            corr_out*=loc_corr
        if ('pol' in corr_prop_in) and (len(corr_prop_in['pol'])>0):
            loc_corr = 1.
            for ideg in range(1,len(corr_prop_in['pol'])+1):loc_corr+=corr_prop_in['pol'][ideg-1]*(var**ideg)
            corr_out*=loc_corr

    #Cumulative variation
    elif mode=='add':
        if ('sin' in corr_prop_in):
            loc_corr = corr_prop_in['sin'][0]*np.sin(2*np.pi*((var-corr_prop_in['sin'][1])/corr_prop_in['sin'][2]))
            corr_out+=loc_corr
        if ('pol' in corr_prop_in) and (len(corr_prop_in['pol'])>0):
            loc_corr = 0.
            for ideg in range(1,len(corr_prop_in['pol'])+1):loc_corr+=corr_prop_in['pol'][ideg-1]*(var**ideg)
            corr_out+=loc_corr
        
    return corr_out



def detrend_prof_ctrst(data_corr,single_l,nord_eff,nspec,glob_corr_ctrst_exp,cond_cont_com,cont_func_dic,rv_star_stelCDM,rv_stelCDM_solCDM,scaled_DI_data_paths_exp,t_dur_exp):
    r"""**Detrending: contrast model.**
    
    Corrects line profile for contrast variations.
    
    Line profile is temporarily set to a null continuum so that its contrast can be 'stretched' by the correction factor :math:`\alpha`
    
    .. math::     
       C_\mathrm{corr} &= ( ((F/F_\mathrm{cont})-1)/\alpha  + 1) F_\mathrm{cont}   \\
                       &= (F-F_\mathrm{cont})/\alpha  + F_\mathrm{cont}    \\
                       &= (F/\alpha) + F_\mathrm{cont} (1 - (1/\alpha)) 
                
    We assume that the contrast variation arises from a modification of the measured profile akin to a change in LSF. 
    All profiles (including in-transit) are thus set to a common vertical scale in which the line contrast is equivalent, by normalizing them with the stellar continuum x the profile global flux level.   
    Both continuum and level must have been defined from a first processing of the data.   

    Args:
        TBD
    
    Returns:
        TBD
    
    """  
    
    #Correcting single line profile   
    if single_l:

        #CCF continuum flux
        #    - defined in the same way as when fitting CCFs, to be consistent with the definition of the contrast
        cont_CCF=np.mean(data_corr['flux'][0,cond_cont_com])
                 
        #Correcting for contrast variation
        #    - the covariance is only modified by the correction factor
        corr_val = np.repeat(glob_corr_ctrst_exp,nspec) 
        data_corr['flux'][0],data_corr['cov'][0] = bind.mul_array(data_corr['flux'][0],data_corr['cov'][0],1./corr_val)        
        data_corr['flux'][0] += cont_CCF*(1. - (1./corr_val))
        
    #Correcting full spectrum
    else:    
        
        #Align spectra from solar barycentric (receiver) to star (source) rest frame
        #      see gen_specdopshift() :
        # w_source = w_receiver / (1+ (rv[s/r]/c))
        # w_star =  w_solbar / ((1+ rv[star/starbar]/c))* (1+ rv[starbar/solbar]/c)))
        #    - the profile is only temporarily shifted and does not need resampling
        cen_bins_rest = data_corr['cen_bins']/(gen_specdopshift(rv_star_stelCDM)*gen_specdopshift(rv_stelCDM_solCDM)) 

        #Processing each exposure                   
        glob_flux_scaling = dataload_npz(scaled_DI_data_paths_exp)['glob_flux_scaling']   
        glob_sc = np.repeat(1./(t_dur_exp*glob_flux_scaling),nspec) 
        for iord in range(nord_eff):

            #Set spectrum to common vertical range (set to common flux level and correct spectrum for stellar continuum), stretch/correct/unstretch, reset to original range
            #    - broadband flux scaling is not applied to maintain all profiles to the same flux balance
            comm_sc = glob_sc/cont_func_dic(cen_bins_rest[iord])
            data_corr['flux'][iord],data_corr['cov'][iord] = bind.mul_array(data_corr['flux'][iord],data_corr['cov'][iord],comm_sc/glob_corr_ctrst_exp)
            data_corr['flux'][iord] += (1. - (1./glob_corr_ctrst_exp))
            data_corr['flux'][iord],data_corr['cov'][iord] = bind.mul_array(data_corr['flux'][iord],data_corr['cov'][iord],1./comm_sc)

    return data_corr



def detrend_prof_FWHM(data_corr,FWHM_corr,RV_star_solCDM,resamp_mode,comm_sp_tab=False):
    r"""**Detrending: FWHM model.**
    
    Corrects line profile for FWHM variations.
    
    Performs a stretch of the spectral tables in rv space. 
    Modifying the width of a line profile to get FWHM/corr is equivalent to redefining the line profile on its rv table divided by the FWHM correction.
    Corrected profiles are resampled on the common visit table if relevant (if a common table is used then all original tables point toward this common table). 
    This operation must be performed on velocity tables symmetrical with respect to the line center, ie for lines that have been aligned on the null velocity.  
    This means the RV correction must be performed first, to determine and fix the correct systemic rv. 

    Args:
        TBD
    
    Returns:
        TBD
    
    """ 
    #Temporary spectral table
    #    - stretched by the FWHM correction
    edge_bins_shift = data_corr['edge_bins'][0] - RV_star_solCDM 
    edge_bins_stretch = edge_bins_shift/FWHM_corr
    
    #Resampling on common table
    if comm_sp_tab:
        data_corr['flux'][0],data_corr['cov'][0] = bind.resampling(edge_bins_shift, edge_bins_stretch, data_corr['flux'][0] , cov = data_corr['cov'][0], kind=resamp_mode)   
        data_corr['cond_def'][0] = ~np.isnan(data_corr['flux'][0])
    else:
        data_corr['edge_bins'][0]=edge_bins_stretch+RV_star_solCDM
        data_corr['cen_bins'][0]=0.5*(data_corr['edge_bins'][0][0:-1]+data_corr['edge_bins'][0][1::])
        
    return data_corr





def pc_analysis(gen_dic,data_dic,inst,vis,data_prop,coord_dic):
    r"""**PC: main routine.**
    
    Applies Principal Components Analysis.
    
    We define the perturbed profiles as

    .. math::       
       F(w,t,v) = F_{\star}(w,v) + F_\mathrm{pert}(w,t,v)
          
    Where :math:`F_\mathrm{pert}` is the physical perturbation from the star, assumed to be roughly uniform over the stellar disk

    .. math:: 
       F_\mathrm{pert}(w,t,v) &\sim \sum_{k} I_\mathrm{pert}(w,t,v) LD_{k}(band) S_{k}  \\
                   &= I_\mathrm{pert}(w,t,v) \sum_{k} LD_{k}(band) S_{k}  \\ 
                   &= I_\mathrm{pert}(w,t,v) S_{\star,LD}(band)
                   
    The perturbation can be described as a linear combination of principal components `PC`, as                   

    .. math:: 
       F_\mathrm{pert}(w,t,v) = \sum_{k} a(t,v) PC(k,w)     
    
    We apply the correction to the raw disk-integrated profiles :math:`F(w,t,v) C_\mathrm{ref}(band,v) F_\mathrm{glob}(v,t)` in `detrend_prof()`, so that the correction model is

    .. math::     
       F_\mathrm{corr}^\mathrm{pca}(w,t,v) = F_\mathrm{pert}(w,t,v) C_\mathrm{ref}(band,v) F_\mathrm{glob}(v,t) 
     
    Residual profiles are calculated from the scaled spectra :math:`F_\mathrm{sc}(w,t) = F(w,t,v) C_\mathrm{ref}(w,v)`.  
    Out-of-transit they are defined as

    .. math:: 
       F_\mathrm{res}(w,t,v) &= F_{\star,sc}(w,v) - F_\mathrm{sc}(w,t,v)  \\
                    &= - F_\mathrm{pert}(w,t,v) C_\mathrm{ref}(band,v)              
    
    To which we fit :math:`F_\mathrm{res}(w,t,v) F_\mathrm{glob}(v,t)`     
    
    In-transit, outside of the planetary lines (see `proc_intr_data()`), they are defined as 

    .. math:: 
       F_\mathrm{intr}(w,t,v) &= F_\mathrm{res}(w,t,v)/(1 - LC_\mathrm{theo}(band,t))     \\
                    &= ( F_{\star,sc}(w,v) - F_\mathrm{sc}(w,t,v) )/(1 - LC_\mathrm{theo}(band,t))    \\
                    &= ( \sum_{k}(I_{k}(w,t,v) LD_{k}(band) S_{k})   - ( \sum_{k_\mathrm{unocc}}(I_{k}(w,t,v) LD_{k}(band) S_{k}) + \sum_{k_\mathrm{unocc}}(I_\mathrm{pert}(w,t,v) LD_{k}(band) S_{k}))) C_\mathrm{ref}(band,v)/(1 - LC_\mathrm{theo}(band,t))   \\
                    &= F_\mathrm{intr,pl}(w,t,v) - I_\mathrm{pert}(w,t,v) \sum_{k_\mathrm{unocc}}(LD_{k}(band) S_{k}) C_\mathrm{ref}(band,v)/(1 - LC_\mathrm{theo}(band,t))   \\
                    &= F_\mathrm{intr,pl}(w,t,v) - (F_\mathrm{pert}(w,t,v)/S_{\star,LD}(band)) (S_{\star,LD}(band) - LD(band,t) S_{p}(band,t)) C_\mathrm{ref}(band,v)/(1 - LC_\mathrm{theo}(band,t))   \\
                    &= F_\mathrm{intr,pl}(w,t,v) - F_\mathrm{pert}(w,t,v) (1 - (LD(band,t) S_{p}(band,t)/S_{\star,LD}(band))) C_\mathrm{ref}(band,v)/(1 - LC_\mathrm{theo}(band,t))                \\  
                    &= F_\mathrm{intr,pl}(w,t,v) - F_\mathrm{pert}(w,t,v) LC_\mathrm{theo}(band,t) C_\mathrm{ref}(band,v)/(1 - LC_\mathrm{theo}(band,t))                 
    
    Outside of the planet-occulted stellar lines the continuum of :math:`F_\mathrm{intr,pl}` is constant, and the continuum of :math:`F_\mathrm{pert}` is assumed to be null, so that

    .. math::                
       F_\mathrm{intr}(w,t,v) - F_\mathrm{intr}(cont,t,v) = - F_\mathrm{pert}(w,t,v) LC_\mathrm{theo}(band,t) C_\mathrm{ref}(band,v)/(1 - LC_\mathrm{theo}(band,t))
    
    We thus fit 
    
    .. math::
       (F_\mathrm{intr}(w,t,v) - F_\mathrm{intr}(cont,t,v)) F_\mathrm{glob}(v,t) (1 - LC_\mathrm{theo}(band,t))/LC_\mathrm{theo}(band,t)
             

    Args:
        TBD
    
    Returns:
        TBD
        
    """    
    if (inst in data_dic['PCA']['vis_list']) and ((vis in data_dic['PCA']['vis_list'][inst]) or (data_dic['PCA']['vis_list'][inst]=='all')):
        print('   > Applying PCA to OT residual profiles') 
        
        #Processing data
        if (gen_dic['calc_pca_ana']):
            print('         Calculating data')        
            data_vis=data_dic[inst][vis]
            if (data_vis['type']!='CCF'):stop('Only coded for CCFs')
            gen_vis = gen_dic[inst][vis]

            #Exposure selection
            idx_pca = gen_vis['idx_out']
            if (inst in data_dic['PCA']['idx_pca']) and (vis in data_dic['PCA']['idx_pca'][inst][vis]) and (len(data_dic['PCA']['idx_pca'][inst][vis])>0):
                idx_pca = np.intersect1d(data_dic['PCA']['idx_pca'][inst][vis],idx_pca)
            nexp_pca = len(idx_pca)
       
            #Analysis and fit range 
            #    - currently we assume that all exposures are defined over the same spectral table
            data_com = np.load(data_vis['proc_com_data_paths']+'.npz',allow_pickle=True)['data'].item()
            cond_out_fit_range = False   
            for bd_int in data_dic['PCA']['ana_range'][inst][vis]:
                cond_out_fit_range |= (data_com['edge_bins'][0,0:-1]>=bd_int[0]) & (data_com['edge_bins'][0,1:]<=bd_int[1])   

            #Store residual profiles as a matrix
            n_rep = 100
            full_res_matr = np.zeros([nexp_pca,data_vis['nspec']],dtype=float)*np.nan
            full_noise_matr = np.zeros([nexp_pca,data_vis['nspec'],n_rep],dtype=float)*np.nan
            cond_fit_range_matr = np.zeros([nexp_pca,data_vis['nspec']],dtype=bool)
            iexp2ipca = np.zeros(data_vis['n_in_visit'],dtype=int)-1
            isub_pca_pretr = []
            isub_pca_posttr = []
            for isub,iexp in enumerate(idx_pca):
                iexp2ipca[iexp] = isub
                if iexp in gen_vis['idx_pretr']:isub_pca_pretr+=[isub]
                else:isub_pca_posttr+=[isub]
                    
                #PCA based on out-of-transit data alone
                data_exp = np.load(data_vis['proc_Res_data_paths']+str(iexp)+'.npz',allow_pickle=True)['data'].item()
                cond_fit_range_matr[isub] = cond_out_fit_range & data_exp['cond_def'][0] 
                full_res_matr[isub] = data_exp['flux'][0]

                #Exclude planetary ranges
                if ('PCA_corr' in data_dic['Atm']['no_plrange']) and (iexp in data_dic['Atm'][inst][vis]['iexp_no_plrange']):
                    cond_in_pl = ~(np.ones(data_vis['nvel'],dtype=bool) & excl_plrange(data_exp['cond_def'][0],data_dic['Atm'][inst][vis]['exclu_range_star'],iexp,data_exp['edge_bins'][0],data_dic[inst][vis]['type'])[0])
                    cond_fit_range_matr[isub,cond_in_pl] = False

                #Generate 'noise' matrix 
                #    - we create n_rep realisations using the error array of current exposure
                err_exp = np.sqrt(data_exp['cov'][0][0])
                full_noise_matr[isub] = list(map(np.random.normal, np.zeros(data_vis['nspec']), err_exp, [n_rep] * data_vis['nspec']))

            #Reduce matrixes to analysis range and center each pixel on null level over time (ie the mean value over selected exposures is subtracted)
            cond_def_all = np.all(cond_fit_range_matr,axis=0)
            nspec_res_mat = np.sum(cond_def_all)
            n_pca_pretr = len(isub_pca_pretr)
            n_pca_posttr = len(isub_pca_posttr)
            nexp_pca = {'out':n_pca_pretr+n_pca_posttr}
            res_matr = {'out': full_res_matr[:,cond_def_all]}
            noise_matr = {'out': full_noise_matr[:,cond_def_all]}
            eig_vals = {}
            eig_val_noise = {}
            if n_pca_pretr>0:
                nexp_pca['pre'] =n_pca_pretr 
                res_matr['pre']=full_res_matr[isub_pca_pretr][:,cond_def_all]
                noise_matr['pre']=full_noise_matr[isub_pca_pretr][:,cond_def_all]
            if n_pca_posttr>0:
                nexp_pca['post'] =n_pca_posttr
                res_matr['post']=full_res_matr[isub_pca_posttr][:,cond_def_all]
                noise_matr['post']=full_noise_matr[isub_pca_posttr][:,cond_def_all]
            for key in res_matr:
                res_matr[key]-=np.nanmean(res_matr[key],axis=0)
                noise_matr[key]-=np.nanmean(noise_matr[key],axis=0)

                #Eigen decomposition
                #    - eigenvalues have dimension nexp, and are already sorted in descending order
                #      eigen_vecs have dimension nexp x nexp, where eig_vecs_matr[i,:] is the ith vector corresponding to the ith eigenvalue
                _, eig_vals_sqrt, eig_vecs_matr = np.linalg.svd(res_matr[key])
                eig_vals[key] = eig_vals_sqrt**2.
                eig_vecs_matr = eig_vecs_matr[0:nexp_pca[key]]

                #Store eigenvectors into null matrix with same dimension as original data
                if key=='out':
                    eig_res_matr = np.zeros([nexp_pca[key],data_vis['nspec']],dtype=float)
                    eig_res_matr[:,cond_def_all] = eig_vecs_matr

                #Calculate eigenvalues for each realization and average them
                eig_val_noise[key] = np.zeros(nexp_pca[key],dtype=float)
                for iboot in range(n_rep):
                    _, eig_vals_noise_sqrt, _ = np.linalg.svd(noise_matr[key][:,:,iboot])
                    eig_val_noise[key]+= eig_vals_noise_sqrt**2.
                eig_val_noise[key]/=n_rep
            
            #--------------------------------------------------------------------
            #Fit PC to all exposures
            fixed_args = {'use_cov':False,'suff':'__IS'+inst+'_VS'+vis}
            
            #PC to use
            fixed_args['n_pc'] = data_dic['PCA']['n_pc'][inst][vis]
            eig_res_matr_fit = eig_res_matr[0:fixed_args['n_pc']]
                
            #Exposures to correct
            idx_corr = np.arange(data_vis['n_in_visit'])
            if (inst in data_dic['PCA']['idx_corr']) and (vis in data_dic['PCA']['idx_corr'][inst][vis]) and (len(data_dic['PCA']['idx_corr'][inst][vis])>0):
                idx_corr = np.intersect1d(data_dic['PCA']['idx_corr'][inst][vis],idx_corr)

            #Fit range
            if (inst not in data_dic['PCA']['fit_range']) or (vis not in data_dic['PCA']['fit_range'][inst]):
                fit_range = data_dic['PCA']['ana_range'][inst][vis]
            else:fit_range=data_dic['PCA']['fit_range'][inst][vis]

            #Process selected exposures
            rms_full_res_matr = np.zeros([2,data_vis['n_in_visit']],dtype=float)*np.nan
            corr_res_matr = np.zeros([nexp_pca['out'],nspec_res_mat],dtype=float)
            chi2null_tab = np.zeros(data_vis['n_in_visit'],dtype=float)*np.nan
            chi2_tab = np.zeros(data_vis['n_in_visit'],dtype=float)*np.nan
            BIC_tab = np.zeros(data_vis['n_in_visit'],dtype=float)*np.nan
            nfit_tab = np.zeros(data_vis['n_in_visit'],dtype=float)*np.nan
            p_final = {}   
            for isub,iexp in enumerate(idx_corr): 

                #Initialise fit parameters 
                p_start = Parameters()
                for i_pc in range(fixed_args['n_pc']):p_start.add_many(('aPC_idx'+str(iexp)+'_ord'+str(i_pc)+'__IS'+inst+'_VS'+vis, 0.,  True, None,None,  None))                
                fixed_args['iexp'] = str(iexp)

                #Fitted range and continuum level
                #    - see explanations in function comments
                data_exp = np.load(data_vis['proc_Res_data_paths']+str(iexp)+'.npz',allow_pickle=True)['data'].item() 
                cond_fit_range = False
                for bd_int in fit_range:cond_fit_range |= (data_exp['edge_bins'][0,0:-1]>=bd_int[0]) & (data_exp['edge_bins'][0,1:]<=bd_int[1])  
                cond_fit_range &=  data_exp['cond_def'][0]

                #Fit PCs
                fixed_args['idx_fit'] = np_where1D(cond_fit_range)
                fixed_args['eig_res_matr'] = eig_res_matr_fit[:,cond_fit_range]
                result, merit ,p_best = call_lmfit(p_start,data_exp['cen_bins'][0],data_exp['flux'][0],data_exp['cov'][0],pc_model,verbose=True,fixed_args=fixed_args)
                for key in p_best:p_final[key] = p_best[key].value

                #BIC for null model (=chi2) and best-fit model
                flux_fit = data_exp['flux'][0,cond_fit_range]
                cov_fit = data_exp['cov'][0][:,cond_fit_range] 
                chi2null_tab[iexp] = np.sum( flux_fit**2. / cov_fit[0])
                nfit_tab[iexp] = np.sum(cond_fit_range)
                chi2_tab[iexp] = merit['chi2']
                BIC_tab[iexp] = merit['BIC']

                #Measure RMS pre/post correction on fitted residual profile
                rms_full_res_matr[0,iexp] = np.std(flux_fit)
                rms_full_res_matr[1,iexp] = np.std(flux_fit - merit['fit'])
                
                #Measure RMS pre/post correction on defined residual profile used for the PCA
                if iexp2ipca[iexp]>-1:
                    isub_pca = iexp2ipca[iexp]
                    res_exp = full_res_matr[isub_pca,cond_def_all]
                  
                    fixed_args['eig_res_matr'] = eig_res_matr_fit[:,cond_def_all]
                    corr_res_exp = res_exp - pc_model(p_best,data_com['cen_bins'][0,cond_def_all],args = fixed_args)                  
                
                    #Store corrected residual profile used for the PCA
                    #    - OT profiles are fully defined over 'cond_def_all'
                    corr_res_matr[isub_pca] = corr_res_exp

            #--------------------------------------------------------------------
            #Quality assessment
            #    - apply FFT to defined residual profiles used for the PCA
            #    - we separate pre- and post-transit exposures to avoid introducing correlation, if the noise is not white

            #Max fft of residual profiles
            #    - mean-centering does not change the result   
            fft_pre = np.nan if n_pca_pretr==0. else np.max(np.abs(fft2(res_matr['pre']))**2.)
            fft_post = np.nan if n_pca_posttr==0. else np.max(np.abs(fft2(res_matr['post']))**2.)
            max_fft2_res_matr = [ fft_pre , fft_post ]
                            
            #Max fft of corrected residual profiles used for PCA
            fft_pre = np.nan if n_pca_pretr==0. else np.max(np.abs(fft2(corr_res_matr[isub_pca_pretr]))**2.)
            fft_post = np.nan if n_pca_posttr==0. else np.max(np.abs(fft2(corr_res_matr[isub_pca_posttr]))**2.)
            max_fft2_corr_res_matr = [ fft_pre , fft_post ]
            
            #Distribution of residuals, post-correction
            hist_corr_res_pre = np.histogram(corr_res_matr[isub_pca_pretr],bins=data_dic['PCA']['nbins'],density=True)
            std_corr_res_pre = np.std(corr_res_matr[isub_pca_pretr])
            hist_corr_res_post = np.histogram(corr_res_matr[isub_pca_posttr],bins=data_dic['PCA']['nbins'],density=True)
            std_corr_res_post = np.std(corr_res_matr[isub_pca_posttr])
            hist_corr_res = np.histogram(corr_res_matr,bins=data_dic['PCA']['nbins'],density=True)
            std_corr_res = np.std(corr_res_matr)

            #Loop over velocities
            fft1D_res_matr = np.zeros([2,nspec_res_mat],dtype=float)*np.nan
            fft1D_corr_res_matr = np.zeros([2,nspec_res_mat],dtype=float)*np.nan
            fft1D_boot_res_matr = np.zeros([2,nspec_res_mat],dtype=float)
            if n_pca_pretr>0:boot_res_matr_pretr = np.zeros([n_pca_pretr,nspec_res_mat,data_dic['PCA']['nboot']],dtype=float) 
            if n_pca_posttr>0:boot_res_matr_posttr = np.zeros([n_pca_posttr,nspec_res_mat,data_dic['PCA']['nboot']],dtype=float) 
            for ipix in range(nspec_res_mat):
                
                #Pre-transit data
                if n_pca_pretr>0:
                    
                    #FFT on current velocity column
                    fft1D_res_matr[0,ipix] = np.max(np.abs(fft(res_matr['pre'][:,ipix]))**2.)
                    fft1D_corr_res_matr[0,ipix] = np.max(np.abs(fft(corr_res_matr[isub_pca_pretr,ipix]))**2.)
                    for iboot in range(data_dic['PCA']['nboot']):
                        boot_res_matr_pretr[:,ipix,iboot] = np.random.choice(corr_res_matr[isub_pca_pretr,ipix],n_pca_pretr,replace=False)                        
                        fft1D_boot_res_matr[0,ipix] += np.max(np.abs(fft(boot_res_matr_pretr[:,ipix,iboot]))**2.)
                    fft1D_boot_res_matr[0,ipix]/=n_pca_pretr

                #Post-transit data
                if n_pca_posttr>0:

                    #FFT on current velocity column
                    fft1D_res_matr[1,ipix] = np.max(np.abs(fft(res_matr['post'][:,ipix]))**2.)
                    fft1D_corr_res_matr[1,ipix] = np.max(np.abs(fft(corr_res_matr[isub_pca_posttr,ipix]))**2.)
                    for iboot in range(data_dic['PCA']['nboot']):
                        boot_res_matr_posttr[:,ipix,iboot] = np.random.choice(corr_res_matr[isub_pca_posttr,ipix],n_pca_posttr,replace=False) 
                        fft1D_boot_res_matr[1,ipix] += np.max(np.abs(fft(boot_res_matr_posttr[:,ipix,iboot]))**2.)
                    fft1D_boot_res_matr[1,ipix]/=n_pca_posttr

            #Max fft of corrected residual profiles used for PCA, bootstrapped                        
            max_fft2_boot_res_matr = np.zeros(2,dtype=float)
            for iboot in range(data_dic['PCA']['nboot']):
                if n_pca_pretr>0:max_fft2_boot_res_matr[0] +=  np.max(np.abs(fft2(boot_res_matr_pretr[:,:,iboot]))**2.)
                if n_pca_posttr>0:max_fft2_boot_res_matr[1] +=  np.max(np.abs(fft2(boot_res_matr_posttr[:,:,iboot]))**2.)
            if n_pca_pretr>0:
                max_fft2_boot_res_matr[0]/=n_pca_pretr
            else:max_fft2_boot_res_matr[0] = np.nan
            if n_pca_posttr>0:max_fft2_boot_res_matr[1]/=n_pca_posttr
            else:max_fft2_boot_res_matr[1] = np.nan                

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

            #Save PCA results
            data_save = {'eig_val_res':eig_vals,'eig_val_noise':eig_val_noise,'eig_res_matr':eig_res_matr,'idx_pca':idx_pca,'isub_pca':np_where1D(iexp2ipca>-1),'idx_corr':idx_corr,'cen_bins':data_com['cen_bins'][0],
                         'n_pc':fixed_args['n_pc'],'rms_full_res_matr':rms_full_res_matr,'BIC_tab':BIC_tab,'chi2_tab':chi2_tab,'chi2null_tab':chi2null_tab,'nfit_tab':nfit_tab,
                         'edge_bins' : data_com['edge_bins'],'p_final':p_final,
                         'max_fft2_res_matr':max_fft2_res_matr,'max_fft2_corr_res_matr':max_fft2_corr_res_matr,'max_fft2_boot_res_matr':max_fft2_boot_res_matr,
                         'cen_bins_res_mat':data_com['cen_bins'][0,cond_def_all],'fft1D_res_matr':fft1D_res_matr,'fft1D_corr_res_matr':fft1D_corr_res_matr,'fft1D_boot_res_matr':fft1D_boot_res_matr,
                         'hist_corr_res_pre':hist_corr_res_pre,'hist_corr_res_post':hist_corr_res_post,'hist_corr_res':hist_corr_res,
                         'std_corr_res_pre':std_corr_res_pre,'std_corr_res_post':std_corr_res_post,'std_corr_res':std_corr_res}
            np.savez_compressed(gen_dic['save_data_dir']+'PCA_results/'+inst+'_'+vis,data=data_save,allow_pickle=True)   
    
        else:
            data_paths={'0':gen_dic['save_data_dir']+'PCA_results/'+inst+'_'+vis}
            check_data(data_paths)  

    return None

def pc_model(params,x,args = None):
    """**PC: model.**
    
    Calculates linear combination of Principal Components.

    Args:
        TBD
    
    Returns:
        TBD
    
    """     
    mod = np.zeros(len(x),dtype=float)
    for i_pc in range(args['n_pc']):mod+=params['aPC_idx'+args['iexp']+'_ord'+str(i_pc)+args['suff']]*args['eig_res_matr'][i_pc]
    return mod



   
def corr_length_determination(Res_data_vis,data_vis,scr_search,inst,vis,gen_dic):
    """**Correlation length.**
    
    Determine the spectral correlation length from out-of-transit residual profiles (so that values are spread around zero). 
    See method in Pont et al. 2006 and Bourrier et al. 2015 (tomography).

    Args:
        TBD
    
    Returns:
        TBD
    
    """  
    #Processing out-of-transit data
    Res_data_vis['corr_search']={
        'meas':np.zeros([data_vis['n_out_tr'],gen_dic['scr_srch_max_binwin']],dtype=float),
        'fit':np.zeros([data_vis['n_out_tr'],gen_dic['scr_srch_max_binwin']],dtype=float),
        'sig_corr':np.zeros([data_vis['n_out_tr']],dtype=float),
        'sig_uncorr':np.zeros([data_vis['n_out_tr']],dtype=float)
        }
    for isub,iexp in enumerate(gen_dic[inst][vis]['idx_out']):    
        
        #Upload data
        data_exp = np.load(data_vis['proc_Res_data_paths']+str(iexp)+'.npz',allow_pickle=True)['data'].item()
        n_pix = len(data_exp['flux'][0]) 
        
        #Calculate dispersion on binned points for each window bin size
        for ibin,nperbin in enumerate(gen_dic['scr_srch_nperbins']):
            
            #Number of possible positions for the sliding window, and corresponding positions
            #    - the last value for i_0 is n_win-1
            #                     for i_f is n_pts-1
            n_win=n_pix-nperbin+1.
            i_0_tab=np.arange(n_win)
            i_f_tab=nperbin-1.+np.arange(n_pix-nperbin+1.)                    
    
    #       #Placing sliding window successively with no overlap
    #       n_win=int(n_pix/nperbin)
    #       i_0_tab=np.arange(n_win)*nperbin
    #       i_f_tab=i_0_tab+nperbin-1.
    
            #Table of the mean residuals within a given window
            mean_tabs=np.zeros(n_win)
    
            #Sliding window along the whole time-series
            #    - steps are by one point, smaller than the sampling interval
            for idx,(i_0,i_f) in enumerate(zip(i_0_tab,i_f_tab)): 
      
                #Binning residuals within current window
                mean_tabs[idx]=np.mean(data_exp['flux'][i_0:i_f+1])
                          
            #Standard-deviation of the mean residuals, for current bin size 
            Res_data_vis['corr_search']['meas'][isub,ibin]=mean_tabs.std()
    
        #--------------------------------------------------------------
    
        #Measured values to fit
        xtofit = gen_dic['scr_srch_nperbins']
        ytofit = Res_data_vis['corr_search']['meas'][isub]
        covtofit = np.ones([1,len(xtofit)])
    
        #Guess
        #     -  sig_bin=1./np.power( np.power(sqrt(nbin)/sig_uncorr,4.) + np.power(1./sig_corr,4.) ,1./4.)
        # + for large bin size, uncorrelated noise should dominate: sig_bin ~  sig_uncorr / sqrt(nbin)
        # + for a bin size of one, we use the guess for the uncorrelated noise
        sig_uncorr= ytofit[-1]*np.sqrt(xtofit[-1])
        sig_corr = np.power( (1./np.power(ytofit[0],4.) ) - np.power(np.sqrt(1.)/sig_uncorr,4.) , -1./4. )       
    
        # Initialise fit parameters 
        #             (    Name,    Value,  Vary,   Min,   Max,  Expr)
        p_use = Parameters()
        p_use.add_many(( 'sig_uncorr', sig_uncorr,  True,None,None,  None),
                      (  'sig_corr', sig_corr,  True,None,None,  None))
    
        #Fitting
        result,merit,p_best = call_lmfit(p_use,xtofit,ytofit,covtofit,binned_stddev_fit,verbose=True)

        #Saving fit and its uncorrelated/correlated components 
        Res_data_vis['corr_search']['fit'][isub]=merit['fit']
        Res_data_vis['corr_search']['sig_corr'][isub]=p_best['sig_corr'].value    
        Res_data_vis['corr_search']['sig_uncorr'][isub]=p_best['sig_uncorr'].value 
    
    return None                



def binned_stddev_fit(param,x):
    """**Noise model.**
    
    Calculates white + red noise model as a function of spectral bin size.

    Args:
        TBD
    
    Returns:
        TBD
    
    """  
    #Uncorrelated standard deviation
    sig_uncorr=(param['sig_uncorr'].value)/np.sqrt(x)

    #Correlated standard deviation
    sig_corr=(param['sig_corr'].value)

    #Global standard deviation with bin size
    sig_bin=1./np.power( np.power(1./sig_uncorr,4.) + np.power(1./sig_corr,4.) ,1./4.)

    return sig_bin
