# -*- coding: utf-8 -*-
"""
Created on Fri Mar  3 14:03:46 2017

@author: picku

multiScaleEntropy implements the algorithm to calculate multiscale entropy (MSE) for
a complex time series as shown in:
    
Costa, Madalena, Ary L. Goldberger, and C-K. Peng. "Multiscale entropy analysis of 
complex physiologic time series." Physical review letters 89.6 (2002): 068102
                                
The MSE algorithm works by computing sample entropy (SampEn) at multiple scales, making
it advantageous for analyzing features related to structure on scales other than the
shortest one. Sample entropy is a modification of approximate entropy (ApEn) in that 
(1) it does not count self-matching and (2) it does not depend as much on the 
length of the time series. Sample entropy algorithm details can be found in:
    
Pincus, Steven M. "Approximate entropy as a measure of system complexity." 
Proceedings of the National Academy of Sciences, 88(6) (1991): 2297-2301
"""
import numpy as np

#For sleep staging analysis
n = 30
startBeatNum = 10
sBn = np.log(startBeatNum) / np.log(10)
stopBeatNum = 40
stBn = np.log(stopBeatNum) / np.log(10)
scales = np.floor(np.logspace(np.log10(10 ** sBn), np.log10(10 ** stBn), n))

def multiScaleEntropy(timeSeries, scales, r, m):
    '''MultiScaleEntropy - algorithm to calculate multiscale entropy (MSE) for complex time series.
       Introduced by Costa, et al (2002)
    
    Inputs Arguments:         
        - timeSeries: [list] of time series data 
        - scales    : [list] of scales for MSE calculation
        - r         : [float] tolerance (typically 0.2*std(time series))
        - m         : [int] embedding dimension (typically 2)

    Ouputs Arguments:
        - MSE       : [array] of sample entropy at scales --> size [1 length(scales)]
    '''
    N = len(timeSeries)
    MSE = []
    
    for s in scales:        
        if s > N:
            print('Scale must not exceed length of the time series')
        else:
            #"course-graining" process      
            cuts = np.reshape(timeSeries[:(N - int(np.mod(N, s)))], (int(np.floor(N/s)), int(s)))
            coarseGrainedSeries = np.mean(cuts, 1)
            MSE.append(sampEn(coarseGrainedSeries, m, r))       
            
    return MSE        
        
def sampEn(x, m, r): 
    '''sampEn computes sample entropy of time series 'x'. 
        
        Input Arguments:
            - x   : [list] of time-series data 
            - m   : [int] embedding dimension (typically 2)
            - r   : [float] tolerance (typically 0.2*std(x))
            
        Output Arguments:
            - sE  : [float] sample entropy value
    '''
    correlHolder = []
    L = len(x)
    W = [m, m + 1]
   
    for i in W: 
        #create space for extracted windows
        z = np.zeros((L - i + 1 , i))
        #extract windows and store in matrix z       
        j = np.arange(0, L - i + 1)       
        for k in j:
            z[k][:] = x[k:k + i]      
       
        count = 0 
        for l in j:
            for n in j:
                if n != l:
                   #Chebyshev distance
                    D = max(abs(z[l][:] - z[n][:]))
                    if D < r: #distance exceeds tolerance
                        count += 1                       
        
        correlHolder.append(count)
        
    B = correlHolder[0]  
    A = correlHolder[1]
              
    if (A == 0) | (B == 0): #need to do some research on this result***
        return 0
    else:    
        return -1 * np.log(A / B)