# @Time    : 2019-6-19
# @Author  : wilbur.cheng@SKF
# @FileName: baseMSET.py
# @Software: a package in skf anomaly detection library to realize multivariate state estimation technique
#            this package has included all the functions used in MSET, similarity calculation, state memory matrix D,
#            normal state matrix L, residual calculation, and the basic SPRT (Sequential Probability Ratio Test)
#            function for binary hypothesis testing.
#

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.neighbors import BallTree
import openpyxl
import time
#from scikit-learn.neighbors import BallTree

class MSET():

    matrixD = None
    matrixL = None
    healthyResidual = None

    normalDataBallTree = None



    def __init__(self):
        self.model = None


    def calcSimilarity(self, x, y, m = 'euc'):
        """
        Calcualte the similartity of two feature vector
        :param x: one of input feature list
        :param y: one of input feature list
        :param m: method of the similarity calculation method, default is the Eucilidean distance named 'euc';
                    a city block distance function is used when m set to "cbd'.
        :return: the two feature similarity, float, range (0,1)
        """

        if (len(x) != len(y)):
            return 0

        if (m == 'cbd'):
            dSimilarity = [1/(1+np.abs(p-q)) for p,q in zip(x,y)]
            dSimilarity = np.sum(dSimilarity)/len(x)
        else:
            dSimilarity = [np.power(p-q,2) for p, q in zip(x, y)]
            dSimilarity = 1/(1+np.sqrt(np.sum(dSimilarity)))

        return dSimilarity


    def genDLMatrix(self, trainDataset, dataSize4D=100, dataSize4L=50):
        """
        automatically generate the D and L matrix from training data set, assuming the training data set is all normal
        state data.
        :param trainDataset: 2D array, [count of data， length of feature]
        :param dataSize4D: minimized count of state for matrix D
        :param dataSize4L: minimized count of state for matrix L
        :return: 0 if successful, -1 if fail
        """
        [m,n] = np.shape(trainDataset)

        if m < dataSize4D + dataSize4L:
            print('training dataset is too small to generate the matrix D and L')
            return -1

        self.matrixD = []
        selectIndex4D = []
        # Step 1: add all the state with minimum or maximum value in each feature into Matrix D

        for i in range(0, n):
            feature_i = trainDataset[:,i]
            minIndex = np.argmin(feature_i)
            maxIndex = np.argmax(feature_i)

            self.matrixD.append(trainDataset[minIndex, :].tolist())
            selectIndex4D.append(minIndex)
            self.matrixD.append(trainDataset[maxIndex, :].tolist())
            selectIndex4D.append(maxIndex)

        # Step 2: iteratively add the state with the maximum average distance to the states in selected matrixD
        while(1):

            if (len(selectIndex4D) >= dataSize4D):
                break

            # Get the free state list
            freeStateList = list(set(np.arange(0, len(trainDataset))) - set(selectIndex4D))

            # Calculate the average dist of each state in free to selected state in matrix D
            distList = []
            for i in freeStateList:
                tmpState = trainDataset[i]
                tmpDist = [1-self.calcSimilarity(x, tmpState) for x in self.matrixD]
                distList.append(np.mean(tmpDist))

            # select the free state with largest average distance to states in matrixD, and update matrixD
            selectId = freeStateList[distList.index(np.max(distList))]
            self.matrixD.append(trainDataset[selectId, :].tolist())
            selectIndex4D.append(selectId)
        #format matrixD from list to array
        self.matrixD = np.array(self.matrixD)
        self.normalDataBallTree = BallTree(self.matrixD, leaf_size=4, metric = lambda i,j: 1-self.calcSimilarity(i,j))


        # Step 3. select remaining state for matrix L
        #index4L = list(set(np.arange(0, len(trainDataset))) - set(selectIndex4D))
        #self.matrixL = trainDataset[index4L, :]
        # consider the limited dataset, using all the train data to matrix L
        self.matrixL = trainDataset

        # Calculate the healthy Residual from matrix L
        lamdaRatio = 1e-3
        [m, n] = np.shape(self.matrixD)
        self.DDSimilarity = np.array([[1-self.calcSimilarity(x,y) for x in self.matrixD] for y in self.matrixD] + lamdaRatio*np.eye(n))
        self.DDSimilarity = 1/self.DDSimilarity

        #self.healthyResidual = self.calcResidual(self.matrixL)
        self.healthyResidual = self.calcResidualByLocallyWeightedLR(self.matrixL)


        return 0

    def calcResidualByLocallyWeightedLR(self, newStates):
        """
        find the K-nearest neighbors for each input state, then calculate the estimated state by locally weighted average.
        :param newStates: input states list
        :return: residual R_x
        """
        [m,n] = np.shape(newStates)
        est_X = []
        # print(newStates)
        for x in newStates:
            (dist, iList) = self.normalDataBallTree.query([x], 20, return_distance=True)
            weight = 1/(dist[0]+1e-1)
            weight = weight/sum(weight)
            eState = np.sum([w*self.matrixD[i] for w,i in zip(weight, iList[0])])
            est_X.append(eState)

        est_X = np.reshape(est_X, (len(est_X),1))
        # print(est_X)
        # print(newStates)
        return est_X - newStates

    def calcStateResidual(self, newsStates):
        stateResidual = self.calcResidualByLocallyWeightedLR(newsStates)
        return stateResidual

    def calcSPRT(self, newsStates, feature_weight, alpha=0.1, beta=0.1, decisionGroup=5):
        """
        anomaly detection based Wald's SPRT algorithm, refer to A.Wald, Sequential Analysis,Wiley, New York, NY, USA, 1947
        :param newsStates: input states list
        :param feature_weight: the important weight for each feature, Normalized and Nonnegative
        :param alpha: prescribed false alarm rate， 0 < alpha < 1
        :param beta: prescribed miss alarm rate, 0 < beta < 1
        :param decisionGroup: length of the test sample when the decision is done

        :return: anomaly flag for each group of states, 1:anomaly, -1:normal, (-1:1): unable to decision
        """

        # Step 1. transfer the raw residual vector to dimension reduced residual using feature weight
        #stateResidual = self.calcResidual(newsStates)
        stateResidual = self.calcResidualByLocallyWeightedLR(newsStates)
        # print(stateResidual)
        weightedStateResidual = [np.dot(x, feature_weight) for x in stateResidual]
        # print(weightedStateResidual)
        weightedHealthyResidual = [np.dot(x, feature_weight) for x in self.healthyResidual]

        '''
        plt.subplot(211)
        plt.plot(weightedHealthyResidual)
        plt.xlabel('Sample index')
        plt.ylabel('Healthy State Residual')
        plt.subplot(212)
        plt.plot(weightedStateResidual)
        plt.xlabel('Sample index')
        plt.ylabel('All State Residual')
        plt.show()
        '''
        # Calculate the distribution of health state residual
        mu0 = np.mean(weightedHealthyResidual)
        sigma0 = np.std(weightedHealthyResidual)


        #sigma1 = np.std(weightedStateResidual)

        lowThres = np.log(beta/(1-alpha))
        highThres = np.log((1-beta)/alpha)

        flag = []
        for i in range(0, len(newsStates)-decisionGroup+1):
            # For each group to calculate the new state residual distribution
            # Then check the  hypothesis testing results
            mu1 = np.mean(weightedStateResidual[i:i+decisionGroup])
            si = np.sum(weightedStateResidual[i:i+decisionGroup])*(mu1-mu0)/sigma0**2 - decisionGroup*(mu1**2-mu0**2)/(2*sigma0**2)

            if (si > highThres):
                si = highThres
            if (si < lowThres):
                si = lowThres

            if (si > 0):
                si = si/highThres
            if (si < 0):
                si = si/lowThres
            si = 100-si*100
            flag.append(si)

        return flag

    def CRITIC_prepare(self, data, flag=1): # 计算权重前的数据预处理
        '''

        :param data: 输入数据，类型是DataFrame
        :param flag: flag=0特征正向归一化，flag=1特征负向归一化
        :return:返回 DataFrame数据
        '''
        data_columns = data.columns.values
        maxnum = np.max(data, axis=0)
        minnum = np.min(data, axis=0)
        if flag == 0:  # 正向指标归一化计算
            Y = (data - minnum) * 1.0 / (maxnum - minnum)
        if flag == 1:  # 负向指标归一化计算
            Y = (maxnum - data) / (maxnum - minnum)

        # 对ln0处理
        Y0 = np.array(Y * 1.0)
        Y0[np.where(Y0 == 0)] = 0.00001
        Y0 = pd.DataFrame(Y0, columns=data_columns)
        return Y0

    def CRITIC(self, data):  # CRITIC客观法计算子系统中各测点的权重
        '''

        :param data: 归一化预处理之后的DataFrame数据
        :return: 返回权重Series以及按指标排序后的得分项
        '''
        n, m = data.shape
        s = np.std(data, axis=0)
        r = np.corrcoef(data, rowvar=False)
        a = np.sum(1 - r, axis=1)
        c = s * a
        w = c / np.sum(c)
        # score = np.round(np.sum(data * w, axis=1), 6)
        # data['score'] = score
        # data.sort_values(by=['score'], ascending=False, inplace=True)
        return w

    def calcHealthy(self, df_data):    # 计算子系统的健康度
        cols = df_data.columns
        df_data[cols] = df_data[cols].apply(pd.to_numeric, errors='coerce')
        df_data = df_data.dropna()  # 删除空行和非数字项

        W_prepare_data = self.CRITIC_prepare(df_data)
        Weights_data = self.CRITIC(W_prepare_data)


        df_data_values = df_data.values
        df_data_values = np.array(df_data_values)
        [m, n] = np.shape(df_data_values)
        # Residual = []
        flag_Spart_data = []
        for i in range(0, n):
            df_data_i = df_data_values[:, i]
            trainDataSet_data = df_data_i[0:len(df_data_i) // 2]
            testDataSet_data = df_data_i[len(df_data_i) // 2 + 1:]
            trainDataSet_data = np.reshape(trainDataSet_data, (len(trainDataSet_data), 1))
            testDataSet_data = np.reshape(testDataSet_data, (len(testDataSet_data), 1))

            self.genDLMatrix(trainDataSet_data, 60, 5)
            # stateResidual = self.calcStateResidual(testDataSet_data)
            flag_data = self.calcSPRT(testDataSet_data, np.array(1), decisionGroup=1)
            # Residual.append(stateResidual)
            flag_Spart_data.append(flag_data)
        # weights = np.array([1/3, 1/3, 1/3])
        W_data = np.array(Weights_data)
        W_data = W_data.reshape(-1, 1)
        flag_Spart_data = np.array(flag_Spart_data)
        flag_Spart_data = flag_Spart_data.T
        Score_data = np.dot(flag_Spart_data, W_data)
        Health_data = np.mean(Score_data)

        return Weights_data, Health_data

    def ahp(self, matrix): #层次分析法ahp(主观)计算风机各子系统的权重
        # 计算特征值和特征向量
        eigenvalue, eigenvector = np.linalg.eig(matrix)
        # 提取最大特征值对应的特征向量，并归一化
        max_eigenvalue_index = np.argmax(eigenvalue)
        max_eigenvalue = eigenvalue[max_eigenvalue_index]
        weight = eigenvector[:, max_eigenvalue_index]
        weight = weight / np.sum(weight)
        weight = weight.real
        return weight, max_eigenvalue

    def consistency_check(self, matrix, weight): #层次分析法ahp的一致性检验
        n = len(matrix)
        # 计算一致性指标 CI
        lambda_max = np.sum(np.dot(matrix, weight) / weight) / n
        CI = (lambda_max - n) / (n - 1)

        # 随机一致性指标 RI 的值，这里我们假设矩阵大小不超过10
        RI_list = [0, 0, 0.58, 0.90, 1.12, 1.24, 1.32, 1.41, 1.45, 1.49, 1.52, 1.54, 1.56, 1.58, 1.59, 1.61, 1.63, 1.64,
                   1.65]
        RI = RI_list[n - 1]

        # 计算一致性比例 CR
        CR = CI / RI
        return CI, CR

if __name__=='__main__':

    start_time = time.time()
    myMSET = MSET()
    # 1、计算各子系统的健康度(子系统包括：发电机组、传动系统(直驱机组无齿轮箱、无数据)、机舱系统、变流器系统、电网环境、辅助系统（无数据))
    # 1.1、发电机组健康度评分
    df_Gen = pd.read_excel(r'F26_SEP_ZHANGYAOXIAN.xlsx',
                           usecols=[10, 11, 12, 17])  # 读取发电机组指标：发电机U相温度10、发电机V相温度11、发电机W相温度12、发电机轴承温度17
    W_Gen,Health_Gen = myMSET.calcHealthy(df_Gen)
    print(W_Gen)
    print('发电机组健康度:', Health_Gen)

    # 1.2、机舱系统健康度评分
    df_Nac = pd.read_excel(r'F26_SEP_ZHANGYAOXIAN.xlsx',
                           usecols=[23,24,41,42]) # 读取机舱系统指标：塔筒前后振动23、塔筒左右振动24、机舱位置41、机舱温度42
    W_Nac, Health_Nac = myMSET.calcHealthy(df_Nac)
    print(W_Nac)
    print('机舱系统健康度:', Health_Nac)

    # 1.3、变流器系统健康度评分
    df_Con = pd.read_excel(r'F26_SEP_ZHANGYAOXIAN.xlsx',
                           usecols=[18,19]) # 读取变流器系统指标：变流器有功功率19、变流器冷却介质温度18
    W_Con, Health_Con = myMSET.calcHealthy(df_Con)
    print(W_Con)
    print('变流器系统健康度:', Health_Con)

    # 1.4、电网环境健康度评分
    df_Grid = pd.read_excel(r'F26_SEP_ZHANGYAOXIAN.xlsx',
                            usecols=[32,38,64,65,66]) # 读取电网环境指标：有功功率38、无功功率32、电网A相电流64、电网B相电流65、电网C相电流66
    W_Grid, Health_Grid = myMSET.calcHealthy(df_Grid)
    print(W_Grid)
    print('电网环境健康度:', Health_Grid)

    # 2、计算各子系统的权重
    # 输入判断矩阵（发电机组、机舱系统、变流器系统、电网环境）
    matrix_subsys = np.array([
        [1, 2, 3, 4],
        [1/2, 1, 2, 3],
        [1/3, 1/2, 1, 2],
        [1/4, 1/3, 1/2, 1],
    ])
    # 计算判断矩阵的权重和最大特征值
    weight_subsys, max_eigenvalue_subsys = myMSET.ahp(matrix_subsys)
    print(weight_subsys)

    # 检查一致性
    CI1, CR1 = myMSET.consistency_check(matrix_subsys, weight_subsys)

    # 3、计算整机的健康度
    Score_subsys = np.array([Health_Gen, Health_Nac, Health_Con, Health_Grid])
    weight_subsys = weight_subsys.reshape(-1, 1)
    Score_WT = np.dot(Score_subsys, weight_subsys)
    print('整机健康度:', Score_WT)

    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time} seconds")



#

    '''
    f = open("speed_vib.txt", "r")
    data1 = f.read()
    f.close()
    data1 = data1.split('\n')
    rpm = [(row.split('\t')[0]).strip() for row in data1]
    vib = [(row.split('\t')[-1]).strip() for row in data1]

    # print(rpm)
    rpm = np.array(rpm).astype(np.float64)
    vib = np.array(vib).astype(np.float64)

    #vib = [(x-np.mean(vib))/np.std(vib) for x in vib]
    #print(vib)
    trainDataSet = [vib[i] for i in range(0,100) if vib[i] < 5]
    trainDataSet = np.reshape(trainDataSet,(len(trainDataSet),1))
    testDataSet = np.reshape(vib, (len(vib),1))
    '''


    # Title = pd.read_csv(r'F1710001001.csv', header=None, nrows=1, usecols=[36,37,38], encoding='gbk')

    '''
    alarmedFlag = np.array([[i, flag[i]] for i, x in enumerate(flag) if x > 0.99])  # Flag中选出大于0.99的点
    plt.close('all')
    plt.subplot(211)
    plt.plot(testDataSet)
    plt.ylabel('Vibration')
    plt.xlabel('Sample index')
    plt.subplot(212)
    plt.plot(flag)
    plt.ylabel('SPRT results')
    plt.xlabel('Sample index')
    plt.scatter(alarmedFlag[:,0], alarmedFlag[:,1], marker='x',c='r')

    plt.show()
    '''






