Module stasp.Coherence

Expand source code
def cross_spec(data,m,percent_limit=90,noise=None,return_noise_wo_sub=False):
    """
    Compute the cross spectrum.
    
    **Parameters**:

    `data`: Either lc (1) or ps (2).    
        (1) class: list of two 'Lightcurve'-objects; the light curves to be used in the coherence computation. <br>
        (2) class: list of two 'PowerSpectrum'-objects; the power spectras to be used in the coherence computation.
        
    `m`: int    
        Number of time bins per segment.
        
    `percent_limit`: float, optional, default: 90    
            Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `noise`: {'Poisson','Gaussian'}, optional, default: None.    
        For a light curve with 'Poisson'/'Gaussian' errors.
        
    `return_noise_wo_sub`: boolean, optional, default: False    
        If True, noise is returned but not subtracted from powspec.
        If False, returned and subtracted.
    
    **Returns**:

    `ps_v`: class: list of two 'PowerSpectrum'-objects    
        The power spectra used in the coherence computation.
    
    `C_v`: np.ndarray    
        The cross-spectrum (see e.g. section 3 of Epitropakis, A. (2017)) after averaging over K segments.
    """
    
    print('Computing the cross spectrum...\n')

    if isinstance(data[0], lightcurve) and isinstance(data[1], lightcurve):
    
        # Segment-wise: 
        ps_v = [PowerSpectrum(lc, m=m, normalization=None, noise=noise, percent_limit=percent_limit,                               timer_on=False, return_noise_wo_sub=return_noise_wo_sub) for lc in data]

    if isinstance(data[0], PowerSpectrum) and isinstance(data[1], PowerSpectrum):
        ps_v = data
        
    S_v = np.array([ps_v[0].fft_rate_v,ps_v[1].fft_rate_v])
    C_v = np.array([np.conjugate(S1)*S2 for S1,S2 in zip(S_v[0],S_v[1])])

    print('Cross spectrum computed.')    
    
    return ps_v, C_v

def coherence_noiseless(data,m,percent_limit=90,C_v=None):
    """
    Compute the coherence function when noise is not present in the signals.
    
    **Parameters**:

    `data`: Either lc (1) or ps (2).    
        (1) class: list of two 'Lightcurve'-objects; the light curves to be used in the coherence computation. <br>
        (2) class: list of two 'PowerSpectrum'-objects; the power spectras to be used in the coherence computation.
        
    `m`: int    
        Number of time bins per segment.
    
    `percent_limit`: float, optional, default: 90    
            Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `C_v`: np.ndarray    
        If the cross spectrum has already been found, it can be used. In this case, we require data to be a list of PowerSpectrum-object.
    
    **Returns**:

    `xf`: np.ndarray    
        The frequency-vector (with the current binning).
        
    `gamma2`: np.ndarray    
        The coherence function.
     
    `delta_gamma2`: np.ndarray    
        One sigma uncertainty in the coherence function. 
    """

    start = timeit.default_timer()
    print('Computing the noiseless coherence...')
    
    if isinstance(data[0], lightcurve) and isinstance(data[1], lightcurve):
        # Need to subtract if one lightcurve lies within the other
        lc_v = subtract_overlapping_energybands(data)
    
    # Find the Cross Spectrum
    if not isinstance(C_v, np.ndarray):
        ps_v, C_v = cross_spec(data, m=m, percent_limit=percent_limit)
        S_v = np.array([ps_v[0].fft_rate_v,ps_v[1].fft_rate_v])
        xf = ps_v[0].xf
    
    # Have already found the Cross Spectrum
    elif isinstance(C_v, np.ndarray):
        print('Cross spectra already found.')
        if isinstance(data[0], PowerSpectrum) and isinstance(data[1], PowerSpectrum):
            xf = data[0].xf
            S_v = np.array([data[0].fft_rate_v,data[1].fft_rate_v])
        else:
            print('If you provide me with the cross spectra C_v, the input data need to be a list of PowerSpectrum-objects.')
    
    # The Periodograms w/o normalization
    P1_v = [np.abs(x)**2 for x in S_v[0]]
    P2_v = [np.abs(x)**2 for x in S_v[1]]

    # The Coherence and its error
    K = np.size(C_v,axis=0) #number of segments
    gamma2 = abs(np.mean(C_v,axis=0))**2/(np.mean(P1_v,axis=0)*np.mean(P2_v,axis=0))
    delta_gamma2 = np.sqrt(2)*(1-gamma2)/(np.sqrt(abs(gamma2))*np.sqrt(K))
    
    time_taken = timeit.default_timer()-start
    print('Noiseless coherence found (in {:.2f} sec).'.format(time_taken))
    
    return xf, gamma2, delta_gamma2, C_v

# See Appendix A of (Epitropakis,2017)

def compute_coherence_intrinsic(lc_v,m,noise,percent_limit,output=False):
    """
    Compute the intrinsic coherence for a given number of bins per segment (m).
    
    **Parameters**:
 
    `lc_v`: class: list of two 'Lightcurve'-objects    
        The light curves to be used in the coherence computation.
        
    `m`: int    
        Number of bins per segment.
    
    `noise`: {'Poisson','Gaussian'}.    
        For a light curve with 'Poisson'/'Gaussian' errors. 
    
    `percent_limit`: float
            Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `output`: boolean    
        If True, plot the whole light curve as well as the "conditions_for_useful_est_int_coh met"-figures. Also, helpful statements are printed.
    
    **Returns**:

    `xf`: np.ndarray    
        The frequency-vector (which depends on m).
        
    `gamma2`: np.ndarray    
        The coherence function.
    
    `delta_gamma2`: np.ndarray    
        One sigma uncertainty in the coherence function. 
        
    `C_mean`: np.ndarray    
        The cross spectra, can be used for time lag estimations.
    """
    
    # Compute power spectra, cross spectra, and signal powers
    ps_v, C_v = cross_spec(lc_v,m,percent_limit,noise=noise,return_noise_wo_sub=True)
    S_v = np.array([ps_v[0].fft_rate_v,ps_v[1].fft_rate_v])

    # Need to extract freq.vector (same for ps_v[0] and ps_v[1]), number of segments K and mean cross spectra
    xf = ps_v[0].xf 
    K = len(C_v)
    C2_mean = np.array(np.abs(np.mean(C_v,axis=0))**2 )
    
    # Compute average for all power terms over all segments and then calculate n2 once
    P1_mean = np.mean(np.abs(S_v[0])**2,axis=0) #power of signal: |S|^2
    P2_mean = np.mean(np.abs(S_v[1])**2,axis=0)
    N1_mean = np.mean(ps_v[0].Pnoise_v,axis=0) #power of noise
    N2_mean = np.mean(ps_v[1].Pnoise_v,axis=0)
    n2 = (P1_mean*N2_mean + P2_mean*N1_mean - N1_mean*N2_mean)/(K)
    
    # Compute coherence
    gamma2 = comp_gamma2(C2_mean,n2,P1_mean,N1_mean,P2_mean,N2_mean)
    
    # Compute coherence error
    delta_gamma2_int = compute_delta_gamma2_int(gamma2,C2_mean,n2,P1_mean,N1_mean,P2_mean,N2_mean,K)
    
    # Check for what frequencies the intrinsic coherence can be usefully estimaed.
    upper_freq_lim = conditions_for_useful_est_int_coh(xf,gamma2,C2_mean,P1_mean,P2_mean,N1_mean,N2_mean,n2,K,output)
    
    return xf, gamma2, delta_gamma2_int, upper_freq_lim
    
def comp_gamma2(C2,n2,P1,N1,P2,N2):    
    """
    Compute gamma2. See: Epitropakis, A. (2017), Eq. (A1). Compare with Eq. (3) (the noiseless case).
    """
    
    return (C2-n2)/((P1-N1)*(P2-N2))
    
    
def compute_delta_gamma2_int(gamma2,C2,n2,P1,N1,P2,N2,K):
    """
    Compute the error delta_gamma2_int. See: Epitropakis, A. (2017), Eq. (A3)
    """
    
    delta_gamma2 = np.sqrt(2/K)*(1-gamma2)/(np.sqrt(abs(gamma2)))
    term1 = 2*K*(n2/(C2-n2))**2 #2*n2**2*K/(C2-n2)**2
    term2 = (N1/(P1-N1))**2 #N1**2/(P1-N1)**2
    term3 = (N2/(P2-N2))**2 #N2**2/(P2-N2)**2
    term4 = K*(delta_gamma2/gamma2)**2 #K*delta_gamma2**2/gamma2**2
    
    return gamma2/np.sqrt(K)*(term1+term2+term3+term4)**(1/2)
    
    
def coherence_intrinsic(lc_v,m_init,noise='Poisson',return_jointly=True,percent_limit=90,output=False,k_lowlim=8):
    """
    Compute the coherence function (Cf) when noise is present in the signals. The Cf is computed at least
    twice, once for m (number of bins per segment) being very high to cover low frequencies and 
    once for m very small (256 as smallest) to cover high frequencies. 
    
    **Parameters**:

    `lc_v`: class: list of two 'Lightcurve'-objects    
        The light curves to be used in the coherence computation.
        
    `m_init`: int    
        Number of bins per segment to start with. m_init will be lowered during each interation. 
        The higher m, the less number of segments. The fewer segments, the lower frequencies will 
        be considered, as f_min = 1/(m_init*dt).
       
    `noise`: {'Poisson','Gaussian'}, optional, default: 'Poisson'.    
        For a light curve with 'Poisson'/'Gaussian' errors. 
       
    `return_jointly`: boolean    
        If True, the coherence computed for different m:s are merged. If False, the coherence 
        is returned as a list of np.ndarrays, so that the computation for different m:s can be compared.
        
    `percent_limit`: float, optional, default: 90    
        Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `output`: boolean    
        If True, plot the whole light curve as well as the "conditions_for_useful_est_int_coh met"-figures. Also, helpful statements are printed.
    
    `k_lowlim`: int, optional, default: 8    
        The lowest permitted m is 2**k_lowlim. 
    
    **Returns**:

    `xf_v`: np.ndarray (or a list of np.ndarrays if return_jointly is False)    
        The frequency-vector.
        
    `gamma2_v`: np.ndarray (or a list of np.ndarrays if return_jointly is False)    
        The coherence function.
    
    `delta_gamma2_v`: np.ndarray (or a list of np.ndarrays if return_jointly is False)    
        One sigma uncertainty in the coherence function. 
    """
    
    assert isinstance(lc_v[0], lightcurve) and isinstance(lc_v[1], lightcurve), 'The data-input does not contain two light curve objects.'
    assert len(lc_v)==2, 'Coherence should be computed with 2 light curves, not {}'.format(len(lc_v))
    
    print('---------------------------------------------------------------------------------------------------')
    print('                           Computing the intrinsic coherence...')
    print('---------------------------------------------------------------------------------------------------\n')
    start = timeit.default_timer()
    
    # Need to subtract if one lightcurve lies within the other
    lc_v = subtract_overlapping_energybands(lc_v)
    
    # Needed parameters
    m = m_init
    k = int(np.log2(m))
    dt = lc_v[0].dt
    find_again = True
    xf_v, gamma2_v, delta_gamma2_int_v, upper_freq_lim_v = [], [], [], []
    
    # Find intrinsic coherence for current m
    i=0
    while find_again:
        i+=1
        print('Iteration {}) Computing using m = {} bins per segment, i.e. f in [{:.3f},{:.3f}]'.format(i,m,1/(m*dt),1/(2*dt)))
        print('---------------------------------------------------------------------------------------------------')
        xf, gamma2, delta_gamma2_int, upper_freq_lim = compute_coherence_intrinsic(lc_v,m,noise,percent_limit,output=output)
    
        if return_jointly: 
            # Don't use the full freq band found; only use freq up to multi_factor*upper_freq_lim, 
            # i.e. slightly under uppper freq lim.
            if k!=k_lowlim:
                multi_factor = 0.9
                if upper_freq_lim != -1:
                    lim = upper_freq_lim*multi_factor
                else:
                    lim = xf[-1]
            else:
                lim = xf[-1]
                
            gamma2 = gamma2[xf < lim]
            delta_gamma2_int = delta_gamma2_int[xf < lim]
            xf = xf[xf < lim]
        
            # Append to lists
            xf_v = np.append(xf_v,xf)
            gamma2_v = np.append(gamma2_v,gamma2) 
            delta_gamma2_int_v = np.append(delta_gamma2_int_v,delta_gamma2_int) 
            upper_freq_lim_v = np.append(upper_freq_lim_v,upper_freq_lim)
        
        # If not return_jointly; can see what freq-range the different m caught.
        else:
            # Rebin directly; only want to get a feeling for the different m
            num = 50
            lim = xf[-1]
            xf,gamma2,delta_gamma2_int = log_rebin(xf,gamma2,delta_gamma2_int,num=num)
            delta_gamma2_int = error_change(delta_gamma2_int)
            
            # Append to lists
            xf_v.append(xf)
            gamma2_v.append(gamma2) 
            delta_gamma2_int_v.append(delta_gamma2_int) 
            upper_freq_lim_v.append(upper_freq_lim)
    
        print('Intrinsic coherence found for f in [{:.3f},{:.3f}] \n'.format(1/(m*dt),lim))
        
        # Change number of bins / segment until next time
        if k == k_lowlim:
            find_again = False
        else:
            k_temp = np.copy(k)
            multi_factor = 10 #discuss what this is... 
            while 2**(k-1) > multi_factor/(dt*upper_freq_lim):
                k -= 1
                if k == k_lowlim:
                    break 
            if k_temp == k: #to make sure we always make k smaller
                k -= 1
            m = 2**k
    
    time_taken = timeit.default_timer()-start
    print('---------------------------------------------------------------------------------------------------')
    print('             Intrinsic coherence found (in {:.2f} sec). return_jointly = {}'.format(time_taken,return_jointly))
    print('---------------------------------------------------------------------------------------------------')
    
    while True:
        to_clear = input("Do you want to clear the standard out from prints [y/n]? ")
        if to_clear not in ("n", "y"):
            print("Not an appropriate choice.")
        else:
            break
    
    if to_clear == 'y':
        clear_output(True)
        
    while True:
        to_plot = input("Do you want to plot the intrinsic coherence [y/n]? ")
        if to_plot not in ("n", "y"):
            print("Not an appropriate choice.")
        else:
            break
        
    if to_plot == 'y': 
        Ebands = ('{}-{}'.format(lc_v[0].Emin,lc_v[0].Emax),'{}-{}'.format(lc_v[1].Emin,lc_v[1].Emax))
        plot_coherence(xf_v, gamma2_v, delta_gamma2_int_v,Ebands=Ebands)
    
    return xf_v, gamma2_v, delta_gamma2_int_v

def conditions_for_useful_est_int_coh(xf,gamma2,C2,P1,P2,N1,N2,n2,K,output):
    """
    The intrinsic coherence can be usefully estimated when the following conditions_for_useful_est_int_coh are met: 
    1) sqrt(C2) > sqrt(n2)
    2) |S_1|^2/|N_1|^2 > 1/\sqrt{m}
    3) |S_2|^2/|N_2|^2 > 1/\sqrt{m}
    """
    
    upper_freq_lim = []
    
    C = np.sqrt(np.abs(C2))
    n = np.sqrt(np.abs(n2))
    
    if output:
        standard_plot(h=8)
        plt.subplot(3,1,1)

        plt.loglog(xf,C,label=r"$|<C(f)>|^2$")
        plt.loglog(xf,n,label=r"$n^2$")
    
    try:
        # Condition 1
        idx = np.argwhere(np.diff(np.sign(C-n))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        if output:
            plt.plot(upper_freq_lim[-1], n[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()
        plt.title('Conditions to be met for useful estimation of $\gamma_I^2$')

        plt.subplot(3,2,3)
        plt.title(r'High Power (Term 2 of $\delta \gamma^2_{int}$)')
        plt.loglog(xf,(P1-N1),label=r'$|S_1|^2$')
        plt.loglog(xf,N1*np.ones(np.size(xf))/np.sqrt(K),label=r'$N_1/\sqrt{m}$')
    
    try:
        # Condition 2
        temp = N1*np.ones(np.size(xf))/np.sqrt(K)
        idx = np.argwhere(np.diff(np.sign(temp-(P1-N1)))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        
        if output:
            plt.plot(upper_freq_lim[-1], temp[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()

        plt.subplot(3,2,4)
        plt.title(r'High Power (Term 3 of $\delta \gamma^2_{int}$)')
        plt.loglog(xf,(P2-N2),label=r'$|S_2|^2$')
        plt.loglog(xf,N2*np.ones(np.size(xf))/np.sqrt(K),label=r'$N_2/\sqrt{m}$')
        
    try:
        # Condition 3
        temp = N2*np.ones(np.size(xf))/np.sqrt(K)
        idx = np.argwhere(np.diff(np.sign(temp-(P2-N2)))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        if output:
            plt.plot(upper_freq_lim[-1], temp[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()

        plt.subplot(3,1,3)
        plt.title('High Coherence')
        plt.loglog(xf,gamma2,label=r'$\gamma^2_{int}$')
        plt.loglog(xf,n**2/(P1*P2),label=r'$n^2/(P_1P_2)$')
    try:
        idx = np.argwhere(np.diff(np.sign(gamma2-n**2/(P1*P2)))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        if output:
            plt.plot(upper_freq_lim[-1], gamma2[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()
        plt.tight_layout()
        plt.show()
    
    if len(upper_freq_lim) == 0:
        return -1
    else:
        return np.amin(upper_freq_lim)
    
def plot_coherence(xf,gamma2,delta_gamma2_int,Ebands=None,err_lim=1,num=75,save_fig = False):
    """
    Plot the coherence.
    
    **Parameters**:

    `xf`: np.ndarray    
        The frequency-vector (which depends on m).
        
    `gamma2`: np.ndarray    
        The coherence function.
    
    `delta_gamma2`: np.ndarray    
        One sigma uncertainty in the coherence function. 
     
    `Ebands`: tuple of strings, (Eband1,Eband2), optional, default: None    
        The energy bands of the two light curves compared.
    
    `err_lim`: float, optional, default: 1    
        Error limit. If an error element is larger than err_lim, it is set to zero. 
    
    `num`: int, optional, default: 75    
        The number of logarithmic frequency bins to create before plotting.
        
    `save_fig`: boolean, optional, default: False    
         If True, asks for path to location to save plot.
    """
    
    standard_plot()
    ax = plt.gca()

    if Ebands == None:
        Eband1 = input('First energyband in keV [?-?]): ')
        Eband2 = input('Second energyband in keV [?-?]: ')
    else:
        Eband1, Eband2 = Ebands[0], Ebands[1]
    ax.text(0.3,0.1,'({} keV) vs. ({} keV)'.format(Eband1,Eband2),fontsize=14,horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
    
    if len(xf) <= 3:
        for i in range(0,len(xf)):
            ax.errorbar(xf[i],gamma2[i],yerr=delta_gamma2_int[i], fmt = '.',mfc='w',capsize=2, elinewidth=1, markeredgewidth=1,label=r'$N=2^6$')
    else:
        xf, gamma2, error = log_rebin(xf,gamma2,delta_gamma2_int,num=num)
        error = error_change(error,err_lim)
        ax.errorbar(xf,gamma2,yerr=error, fmt = '.k',mfc='w',capsize=2, elinewidth=1, markeredgewidth=1,label=r'$N=2^6$')
    
    ax.set_ylim([0,1.4])
    ax.axhline(1,color='k',linewidth=1,alpha=0.7)
    ax.set_xscale("log")
    plt.xlabel('Frequency [Hz]')

    plt.ylabel('$\gamma_{int}^2$')
    if save_fig:
        path = input('Path to location to save plot [end with .png]: ') 
        plt.savefig(path,bbox_inches='tight')
    plt.show()

Functions

def coherence_intrinsic(lc_v, m_init, noise='Poisson', return_jointly=True, percent_limit=90, output=False, k_lowlim=8)

Compute the coherence function (Cf) when noise is present in the signals. The Cf is computed at least twice, once for m (number of bins per segment) being very high to cover low frequencies and once for m very small (256 as smallest) to cover high frequencies.

Parameters:

lc_v: class: list of two 'Lightcurve'-objects
The light curves to be used in the coherence computation.

m_init: int
Number of bins per segment to start with. m_init will be lowered during each interation. The higher m, the less number of segments. The fewer segments, the lower frequencies will be considered, as f_min = 1/(m_init*dt).

noise: {'Poisson','Gaussian'}, optional, default: 'Poisson'.
For a light curve with 'Poisson'/'Gaussian' errors.

return_jointly: boolean
If True, the coherence computed for different m:s are merged. If False, the coherence is returned as a list of np.ndarrays, so that the computation for different m:s can be compared.

percent_limit: float, optional, default: 90
Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.

output: boolean
If True, plot the whole light curve as well as the "conditions_for_useful_est_int_coh met"-figures. Also, helpful statements are printed.

k_lowlim: int, optional, default: 8
The lowest permitted m is 2**k_lowlim.

Returns:

xf_v: np.ndarray (or a list of np.ndarrays if return_jointly is False)
The frequency-vector.

gamma2_v: np.ndarray (or a list of np.ndarrays if return_jointly is False)
The coherence function.

delta_gamma2_v: np.ndarray (or a list of np.ndarrays if return_jointly is False)
One sigma uncertainty in the coherence function.

Expand source code
def coherence_intrinsic(lc_v,m_init,noise='Poisson',return_jointly=True,percent_limit=90,output=False,k_lowlim=8):
    """
    Compute the coherence function (Cf) when noise is present in the signals. The Cf is computed at least
    twice, once for m (number of bins per segment) being very high to cover low frequencies and 
    once for m very small (256 as smallest) to cover high frequencies. 
    
    **Parameters**:

    `lc_v`: class: list of two 'Lightcurve'-objects    
        The light curves to be used in the coherence computation.
        
    `m_init`: int    
        Number of bins per segment to start with. m_init will be lowered during each interation. 
        The higher m, the less number of segments. The fewer segments, the lower frequencies will 
        be considered, as f_min = 1/(m_init*dt).
       
    `noise`: {'Poisson','Gaussian'}, optional, default: 'Poisson'.    
        For a light curve with 'Poisson'/'Gaussian' errors. 
       
    `return_jointly`: boolean    
        If True, the coherence computed for different m:s are merged. If False, the coherence 
        is returned as a list of np.ndarrays, so that the computation for different m:s can be compared.
        
    `percent_limit`: float, optional, default: 90    
        Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `output`: boolean    
        If True, plot the whole light curve as well as the "conditions_for_useful_est_int_coh met"-figures. Also, helpful statements are printed.
    
    `k_lowlim`: int, optional, default: 8    
        The lowest permitted m is 2**k_lowlim. 
    
    **Returns**:

    `xf_v`: np.ndarray (or a list of np.ndarrays if return_jointly is False)    
        The frequency-vector.
        
    `gamma2_v`: np.ndarray (or a list of np.ndarrays if return_jointly is False)    
        The coherence function.
    
    `delta_gamma2_v`: np.ndarray (or a list of np.ndarrays if return_jointly is False)    
        One sigma uncertainty in the coherence function. 
    """
    
    assert isinstance(lc_v[0], lightcurve) and isinstance(lc_v[1], lightcurve), 'The data-input does not contain two light curve objects.'
    assert len(lc_v)==2, 'Coherence should be computed with 2 light curves, not {}'.format(len(lc_v))
    
    print('---------------------------------------------------------------------------------------------------')
    print('                           Computing the intrinsic coherence...')
    print('---------------------------------------------------------------------------------------------------\n')
    start = timeit.default_timer()
    
    # Need to subtract if one lightcurve lies within the other
    lc_v = subtract_overlapping_energybands(lc_v)
    
    # Needed parameters
    m = m_init
    k = int(np.log2(m))
    dt = lc_v[0].dt
    find_again = True
    xf_v, gamma2_v, delta_gamma2_int_v, upper_freq_lim_v = [], [], [], []
    
    # Find intrinsic coherence for current m
    i=0
    while find_again:
        i+=1
        print('Iteration {}) Computing using m = {} bins per segment, i.e. f in [{:.3f},{:.3f}]'.format(i,m,1/(m*dt),1/(2*dt)))
        print('---------------------------------------------------------------------------------------------------')
        xf, gamma2, delta_gamma2_int, upper_freq_lim = compute_coherence_intrinsic(lc_v,m,noise,percent_limit,output=output)
    
        if return_jointly: 
            # Don't use the full freq band found; only use freq up to multi_factor*upper_freq_lim, 
            # i.e. slightly under uppper freq lim.
            if k!=k_lowlim:
                multi_factor = 0.9
                if upper_freq_lim != -1:
                    lim = upper_freq_lim*multi_factor
                else:
                    lim = xf[-1]
            else:
                lim = xf[-1]
                
            gamma2 = gamma2[xf < lim]
            delta_gamma2_int = delta_gamma2_int[xf < lim]
            xf = xf[xf < lim]
        
            # Append to lists
            xf_v = np.append(xf_v,xf)
            gamma2_v = np.append(gamma2_v,gamma2) 
            delta_gamma2_int_v = np.append(delta_gamma2_int_v,delta_gamma2_int) 
            upper_freq_lim_v = np.append(upper_freq_lim_v,upper_freq_lim)
        
        # If not return_jointly; can see what freq-range the different m caught.
        else:
            # Rebin directly; only want to get a feeling for the different m
            num = 50
            lim = xf[-1]
            xf,gamma2,delta_gamma2_int = log_rebin(xf,gamma2,delta_gamma2_int,num=num)
            delta_gamma2_int = error_change(delta_gamma2_int)
            
            # Append to lists
            xf_v.append(xf)
            gamma2_v.append(gamma2) 
            delta_gamma2_int_v.append(delta_gamma2_int) 
            upper_freq_lim_v.append(upper_freq_lim)
    
        print('Intrinsic coherence found for f in [{:.3f},{:.3f}] \n'.format(1/(m*dt),lim))
        
        # Change number of bins / segment until next time
        if k == k_lowlim:
            find_again = False
        else:
            k_temp = np.copy(k)
            multi_factor = 10 #discuss what this is... 
            while 2**(k-1) > multi_factor/(dt*upper_freq_lim):
                k -= 1
                if k == k_lowlim:
                    break 
            if k_temp == k: #to make sure we always make k smaller
                k -= 1
            m = 2**k
    
    time_taken = timeit.default_timer()-start
    print('---------------------------------------------------------------------------------------------------')
    print('             Intrinsic coherence found (in {:.2f} sec). return_jointly = {}'.format(time_taken,return_jointly))
    print('---------------------------------------------------------------------------------------------------')
    
    while True:
        to_clear = input("Do you want to clear the standard out from prints [y/n]? ")
        if to_clear not in ("n", "y"):
            print("Not an appropriate choice.")
        else:
            break
    
    if to_clear == 'y':
        clear_output(True)
        
    while True:
        to_plot = input("Do you want to plot the intrinsic coherence [y/n]? ")
        if to_plot not in ("n", "y"):
            print("Not an appropriate choice.")
        else:
            break
        
    if to_plot == 'y': 
        Ebands = ('{}-{}'.format(lc_v[0].Emin,lc_v[0].Emax),'{}-{}'.format(lc_v[1].Emin,lc_v[1].Emax))
        plot_coherence(xf_v, gamma2_v, delta_gamma2_int_v,Ebands=Ebands)
    
    return xf_v, gamma2_v, delta_gamma2_int_v
def coherence_noiseless(data, m, percent_limit=90, C_v=None)

Compute the coherence function when noise is not present in the signals.

Parameters:

data: Either lc (1) or ps (2).
(1) class: list of two 'Lightcurve'-objects; the light curves to be used in the coherence computation.
(2) class: list of two 'PowerSpectrum'-objects; the power spectras to be used in the coherence computation.

m: int
Number of time bins per segment.

percent_limit: float, optional, default: 90
Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.

C_v: np.ndarray
If the cross spectrum has already been found, it can be used. In this case, we require data to be a list of PowerSpectrum-object.

Returns:

xf: np.ndarray
The frequency-vector (with the current binning).

gamma2: np.ndarray
The coherence function.

delta_gamma2: np.ndarray
One sigma uncertainty in the coherence function.

Expand source code
def coherence_noiseless(data,m,percent_limit=90,C_v=None):
    """
    Compute the coherence function when noise is not present in the signals.
    
    **Parameters**:

    `data`: Either lc (1) or ps (2).    
        (1) class: list of two 'Lightcurve'-objects; the light curves to be used in the coherence computation. <br>
        (2) class: list of two 'PowerSpectrum'-objects; the power spectras to be used in the coherence computation.
        
    `m`: int    
        Number of time bins per segment.
    
    `percent_limit`: float, optional, default: 90    
            Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `C_v`: np.ndarray    
        If the cross spectrum has already been found, it can be used. In this case, we require data to be a list of PowerSpectrum-object.
    
    **Returns**:

    `xf`: np.ndarray    
        The frequency-vector (with the current binning).
        
    `gamma2`: np.ndarray    
        The coherence function.
     
    `delta_gamma2`: np.ndarray    
        One sigma uncertainty in the coherence function. 
    """

    start = timeit.default_timer()
    print('Computing the noiseless coherence...')
    
    if isinstance(data[0], lightcurve) and isinstance(data[1], lightcurve):
        # Need to subtract if one lightcurve lies within the other
        lc_v = subtract_overlapping_energybands(data)
    
    # Find the Cross Spectrum
    if not isinstance(C_v, np.ndarray):
        ps_v, C_v = cross_spec(data, m=m, percent_limit=percent_limit)
        S_v = np.array([ps_v[0].fft_rate_v,ps_v[1].fft_rate_v])
        xf = ps_v[0].xf
    
    # Have already found the Cross Spectrum
    elif isinstance(C_v, np.ndarray):
        print('Cross spectra already found.')
        if isinstance(data[0], PowerSpectrum) and isinstance(data[1], PowerSpectrum):
            xf = data[0].xf
            S_v = np.array([data[0].fft_rate_v,data[1].fft_rate_v])
        else:
            print('If you provide me with the cross spectra C_v, the input data need to be a list of PowerSpectrum-objects.')
    
    # The Periodograms w/o normalization
    P1_v = [np.abs(x)**2 for x in S_v[0]]
    P2_v = [np.abs(x)**2 for x in S_v[1]]

    # The Coherence and its error
    K = np.size(C_v,axis=0) #number of segments
    gamma2 = abs(np.mean(C_v,axis=0))**2/(np.mean(P1_v,axis=0)*np.mean(P2_v,axis=0))
    delta_gamma2 = np.sqrt(2)*(1-gamma2)/(np.sqrt(abs(gamma2))*np.sqrt(K))
    
    time_taken = timeit.default_timer()-start
    print('Noiseless coherence found (in {:.2f} sec).'.format(time_taken))
    
    return xf, gamma2, delta_gamma2, C_v
def comp_gamma2(C2, n2, P1, N1, P2, N2)

Compute gamma2. See: Epitropakis, A. (2017), Eq. (A1). Compare with Eq. (3) (the noiseless case).

Expand source code
def comp_gamma2(C2,n2,P1,N1,P2,N2):    
    """
    Compute gamma2. See: Epitropakis, A. (2017), Eq. (A1). Compare with Eq. (3) (the noiseless case).
    """
    
    return (C2-n2)/((P1-N1)*(P2-N2))
def compute_coherence_intrinsic(lc_v, m, noise, percent_limit, output=False)

Compute the intrinsic coherence for a given number of bins per segment (m).

Parameters:

lc_v: class: list of two 'Lightcurve'-objects
The light curves to be used in the coherence computation.

m: int
Number of bins per segment.

noise: {'Poisson','Gaussian'}.
For a light curve with 'Poisson'/'Gaussian' errors.

percent_limit: float Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.

output: boolean
If True, plot the whole light curve as well as the "conditions_for_useful_est_int_coh met"-figures. Also, helpful statements are printed.

Returns:

xf: np.ndarray
The frequency-vector (which depends on m).

gamma2: np.ndarray
The coherence function.

delta_gamma2: np.ndarray
One sigma uncertainty in the coherence function.

C_mean: np.ndarray
The cross spectra, can be used for time lag estimations.

Expand source code
def compute_coherence_intrinsic(lc_v,m,noise,percent_limit,output=False):
    """
    Compute the intrinsic coherence for a given number of bins per segment (m).
    
    **Parameters**:
 
    `lc_v`: class: list of two 'Lightcurve'-objects    
        The light curves to be used in the coherence computation.
        
    `m`: int    
        Number of bins per segment.
    
    `noise`: {'Poisson','Gaussian'}.    
        For a light curve with 'Poisson'/'Gaussian' errors. 
    
    `percent_limit`: float
            Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `output`: boolean    
        If True, plot the whole light curve as well as the "conditions_for_useful_est_int_coh met"-figures. Also, helpful statements are printed.
    
    **Returns**:

    `xf`: np.ndarray    
        The frequency-vector (which depends on m).
        
    `gamma2`: np.ndarray    
        The coherence function.
    
    `delta_gamma2`: np.ndarray    
        One sigma uncertainty in the coherence function. 
        
    `C_mean`: np.ndarray    
        The cross spectra, can be used for time lag estimations.
    """
    
    # Compute power spectra, cross spectra, and signal powers
    ps_v, C_v = cross_spec(lc_v,m,percent_limit,noise=noise,return_noise_wo_sub=True)
    S_v = np.array([ps_v[0].fft_rate_v,ps_v[1].fft_rate_v])

    # Need to extract freq.vector (same for ps_v[0] and ps_v[1]), number of segments K and mean cross spectra
    xf = ps_v[0].xf 
    K = len(C_v)
    C2_mean = np.array(np.abs(np.mean(C_v,axis=0))**2 )
    
    # Compute average for all power terms over all segments and then calculate n2 once
    P1_mean = np.mean(np.abs(S_v[0])**2,axis=0) #power of signal: |S|^2
    P2_mean = np.mean(np.abs(S_v[1])**2,axis=0)
    N1_mean = np.mean(ps_v[0].Pnoise_v,axis=0) #power of noise
    N2_mean = np.mean(ps_v[1].Pnoise_v,axis=0)
    n2 = (P1_mean*N2_mean + P2_mean*N1_mean - N1_mean*N2_mean)/(K)
    
    # Compute coherence
    gamma2 = comp_gamma2(C2_mean,n2,P1_mean,N1_mean,P2_mean,N2_mean)
    
    # Compute coherence error
    delta_gamma2_int = compute_delta_gamma2_int(gamma2,C2_mean,n2,P1_mean,N1_mean,P2_mean,N2_mean,K)
    
    # Check for what frequencies the intrinsic coherence can be usefully estimaed.
    upper_freq_lim = conditions_for_useful_est_int_coh(xf,gamma2,C2_mean,P1_mean,P2_mean,N1_mean,N2_mean,n2,K,output)
    
    return xf, gamma2, delta_gamma2_int, upper_freq_lim
def compute_delta_gamma2_int(gamma2, C2, n2, P1, N1, P2, N2, K)

Compute the error delta_gamma2_int. See: Epitropakis, A. (2017), Eq. (A3)

Expand source code
def compute_delta_gamma2_int(gamma2,C2,n2,P1,N1,P2,N2,K):
    """
    Compute the error delta_gamma2_int. See: Epitropakis, A. (2017), Eq. (A3)
    """
    
    delta_gamma2 = np.sqrt(2/K)*(1-gamma2)/(np.sqrt(abs(gamma2)))
    term1 = 2*K*(n2/(C2-n2))**2 #2*n2**2*K/(C2-n2)**2
    term2 = (N1/(P1-N1))**2 #N1**2/(P1-N1)**2
    term3 = (N2/(P2-N2))**2 #N2**2/(P2-N2)**2
    term4 = K*(delta_gamma2/gamma2)**2 #K*delta_gamma2**2/gamma2**2
    
    return gamma2/np.sqrt(K)*(term1+term2+term3+term4)**(1/2)
def conditions_for_useful_est_int_coh(xf, gamma2, C2, P1, P2, N1, N2, n2, K, output)

The intrinsic coherence can be usefully estimated when the following conditions_for_useful_est_int_coh are met: 1) sqrt(C2) > sqrt(n2) 2) |S_1|^2/|N_1|^2 > 1/\sqrt{m} 3) |S_2|^2/|N_2|^2 > 1/\sqrt{m}

Expand source code
def conditions_for_useful_est_int_coh(xf,gamma2,C2,P1,P2,N1,N2,n2,K,output):
    """
    The intrinsic coherence can be usefully estimated when the following conditions_for_useful_est_int_coh are met: 
    1) sqrt(C2) > sqrt(n2)
    2) |S_1|^2/|N_1|^2 > 1/\sqrt{m}
    3) |S_2|^2/|N_2|^2 > 1/\sqrt{m}
    """
    
    upper_freq_lim = []
    
    C = np.sqrt(np.abs(C2))
    n = np.sqrt(np.abs(n2))
    
    if output:
        standard_plot(h=8)
        plt.subplot(3,1,1)

        plt.loglog(xf,C,label=r"$|<C(f)>|^2$")
        plt.loglog(xf,n,label=r"$n^2$")
    
    try:
        # Condition 1
        idx = np.argwhere(np.diff(np.sign(C-n))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        if output:
            plt.plot(upper_freq_lim[-1], n[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()
        plt.title('Conditions to be met for useful estimation of $\gamma_I^2$')

        plt.subplot(3,2,3)
        plt.title(r'High Power (Term 2 of $\delta \gamma^2_{int}$)')
        plt.loglog(xf,(P1-N1),label=r'$|S_1|^2$')
        plt.loglog(xf,N1*np.ones(np.size(xf))/np.sqrt(K),label=r'$N_1/\sqrt{m}$')
    
    try:
        # Condition 2
        temp = N1*np.ones(np.size(xf))/np.sqrt(K)
        idx = np.argwhere(np.diff(np.sign(temp-(P1-N1)))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        
        if output:
            plt.plot(upper_freq_lim[-1], temp[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()

        plt.subplot(3,2,4)
        plt.title(r'High Power (Term 3 of $\delta \gamma^2_{int}$)')
        plt.loglog(xf,(P2-N2),label=r'$|S_2|^2$')
        plt.loglog(xf,N2*np.ones(np.size(xf))/np.sqrt(K),label=r'$N_2/\sqrt{m}$')
        
    try:
        # Condition 3
        temp = N2*np.ones(np.size(xf))/np.sqrt(K)
        idx = np.argwhere(np.diff(np.sign(temp-(P2-N2)))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        if output:
            plt.plot(upper_freq_lim[-1], temp[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()

        plt.subplot(3,1,3)
        plt.title('High Coherence')
        plt.loglog(xf,gamma2,label=r'$\gamma^2_{int}$')
        plt.loglog(xf,n**2/(P1*P2),label=r'$n^2/(P_1P_2)$')
    try:
        idx = np.argwhere(np.diff(np.sign(gamma2-n**2/(P1*P2)))).flatten()
        upper_freq_lim.append(xf[idx[0]])
        if output:
            plt.plot(upper_freq_lim[-1], gamma2[idx[0]], 'ro',label=str(xf[idx[0]]))
    except:
        pass
    
    if output:
        plt.legend()
        plt.tight_layout()
        plt.show()
    
    if len(upper_freq_lim) == 0:
        return -1
    else:
        return np.amin(upper_freq_lim)
def cross_spec(data, m, percent_limit=90, noise=None, return_noise_wo_sub=False)

Compute the cross spectrum.

Parameters:

data: Either lc (1) or ps (2).
(1) class: list of two 'Lightcurve'-objects; the light curves to be used in the coherence computation.
(2) class: list of two 'PowerSpectrum'-objects; the power spectras to be used in the coherence computation.

m: int
Number of time bins per segment.

percent_limit: float, optional, default: 90
Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.

noise: {'Poisson','Gaussian'}, optional, default: None.
For a light curve with 'Poisson'/'Gaussian' errors.

return_noise_wo_sub: boolean, optional, default: False
If True, noise is returned but not subtracted from powspec. If False, returned and subtracted.

Returns:

ps_v: class: list of two 'PowerSpectrum'-objects
The power spectra used in the coherence computation.

C_v: np.ndarray
The cross-spectrum (see e.g. section 3 of Epitropakis, A. (2017)) after averaging over K segments.

Expand source code
def cross_spec(data,m,percent_limit=90,noise=None,return_noise_wo_sub=False):
    """
    Compute the cross spectrum.
    
    **Parameters**:

    `data`: Either lc (1) or ps (2).    
        (1) class: list of two 'Lightcurve'-objects; the light curves to be used in the coherence computation. <br>
        (2) class: list of two 'PowerSpectrum'-objects; the power spectras to be used in the coherence computation.
        
    `m`: int    
        Number of time bins per segment.
        
    `percent_limit`: float, optional, default: 90    
            Lower limit for percent of time bins having to be filled, i.e. without gaps, for that segment to be used.
    
    `noise`: {'Poisson','Gaussian'}, optional, default: None.    
        For a light curve with 'Poisson'/'Gaussian' errors.
        
    `return_noise_wo_sub`: boolean, optional, default: False    
        If True, noise is returned but not subtracted from powspec.
        If False, returned and subtracted.
    
    **Returns**:

    `ps_v`: class: list of two 'PowerSpectrum'-objects    
        The power spectra used in the coherence computation.
    
    `C_v`: np.ndarray    
        The cross-spectrum (see e.g. section 3 of Epitropakis, A. (2017)) after averaging over K segments.
    """
    
    print('Computing the cross spectrum...\n')

    if isinstance(data[0], lightcurve) and isinstance(data[1], lightcurve):
    
        # Segment-wise: 
        ps_v = [PowerSpectrum(lc, m=m, normalization=None, noise=noise, percent_limit=percent_limit,                               timer_on=False, return_noise_wo_sub=return_noise_wo_sub) for lc in data]

    if isinstance(data[0], PowerSpectrum) and isinstance(data[1], PowerSpectrum):
        ps_v = data
        
    S_v = np.array([ps_v[0].fft_rate_v,ps_v[1].fft_rate_v])
    C_v = np.array([np.conjugate(S1)*S2 for S1,S2 in zip(S_v[0],S_v[1])])

    print('Cross spectrum computed.')    
    
    return ps_v, C_v
def plot_coherence(xf, gamma2, delta_gamma2_int, Ebands=None, err_lim=1, num=75, save_fig=False)

Plot the coherence.

Parameters:

xf: np.ndarray
The frequency-vector (which depends on m).

gamma2: np.ndarray
The coherence function.

delta_gamma2: np.ndarray
One sigma uncertainty in the coherence function.

Ebands: tuple of strings, (Eband1,Eband2), optional, default: None
The energy bands of the two light curves compared.

err_lim: float, optional, default: 1
Error limit. If an error element is larger than err_lim, it is set to zero.

num: int, optional, default: 75
The number of logarithmic frequency bins to create before plotting.

save_fig: boolean, optional, default: False
If True, asks for path to location to save plot.

Expand source code
def plot_coherence(xf,gamma2,delta_gamma2_int,Ebands=None,err_lim=1,num=75,save_fig = False):
    """
    Plot the coherence.
    
    **Parameters**:

    `xf`: np.ndarray    
        The frequency-vector (which depends on m).
        
    `gamma2`: np.ndarray    
        The coherence function.
    
    `delta_gamma2`: np.ndarray    
        One sigma uncertainty in the coherence function. 
     
    `Ebands`: tuple of strings, (Eband1,Eband2), optional, default: None    
        The energy bands of the two light curves compared.
    
    `err_lim`: float, optional, default: 1    
        Error limit. If an error element is larger than err_lim, it is set to zero. 
    
    `num`: int, optional, default: 75    
        The number of logarithmic frequency bins to create before plotting.
        
    `save_fig`: boolean, optional, default: False    
         If True, asks for path to location to save plot.
    """
    
    standard_plot()
    ax = plt.gca()

    if Ebands == None:
        Eband1 = input('First energyband in keV [?-?]): ')
        Eband2 = input('Second energyband in keV [?-?]: ')
    else:
        Eband1, Eband2 = Ebands[0], Ebands[1]
    ax.text(0.3,0.1,'({} keV) vs. ({} keV)'.format(Eband1,Eband2),fontsize=14,horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
    
    if len(xf) <= 3:
        for i in range(0,len(xf)):
            ax.errorbar(xf[i],gamma2[i],yerr=delta_gamma2_int[i], fmt = '.',mfc='w',capsize=2, elinewidth=1, markeredgewidth=1,label=r'$N=2^6$')
    else:
        xf, gamma2, error = log_rebin(xf,gamma2,delta_gamma2_int,num=num)
        error = error_change(error,err_lim)
        ax.errorbar(xf,gamma2,yerr=error, fmt = '.k',mfc='w',capsize=2, elinewidth=1, markeredgewidth=1,label=r'$N=2^6$')
    
    ax.set_ylim([0,1.4])
    ax.axhline(1,color='k',linewidth=1,alpha=0.7)
    ax.set_xscale("log")
    plt.xlabel('Frequency [Hz]')

    plt.ylabel('$\gamma_{int}^2$')
    if save_fig:
        path = input('Path to location to save plot [end with .png]: ') 
        plt.savefig(path,bbox_inches='tight')
    plt.show()