"""
Article : Least squares twin support vector machines for pattern classification
Link    : https://sci-hub.tw/https://www.sciencedirect.com/science/article/abs/pii/S0957417408006854
Author  : Saeed Khosravi
"""
import numpy as np
class LSTSVM:
    """
        Least Squares Support Vector Machines
        A = Instances with label +1
        B = Instances with label -1
        C1 = hyperparameter for hyperplane 1
        C2 = hyperparameter for hyperplane 2
        
    """
    def __init__(self, X, y, C1, C2, eps = 1e-4):
        self.A = X[np.ix_(y[:,0] ==   1),:][0,:,:]
        self.B = X[np.ix_(y[:,0] ==  -1),:][0,:,:]
        self.C1 = C1
        self.C2 = C2
        self.eps = eps
        
    def fit(self):
        A = self.A
        B = self.B
        C1 = self.C1
        C2 = self.C2
        eps = self.eps
        m1, n = A.shape
        m2, n = B.shape
        e1 = np.ones((m1, 1))
        e2 = np.ones((m2, 1))
        X = np.concatenate((A, B), axis=0)
        G = np.concatenate((A, e1), axis=1)
        H = np.concatenate((B, e2), axis=1)
        

        if(m1 < m2):
            Y = self.calc_Y_or_Z(H)
           
            #w1, b1
            GYGT = np.dot(np.dot(G, Y), G.T)
            I = np.eye(GYGT.shape[0], GYGT.shape[1])
            w1_b1 = - np.dot(Y - np.dot(np.dot(np.dot(Y, G.T), np.linalg.inv(C1*I + GYGT)), np.dot(G, Y)), 
                             np.dot(H.T, np.ones((H.T.shape[1], 1))))
            w1 = w1_b1[:-1,  :]
            b1 = w1_b1[ -1,  :]
            
            #w2, b2
            w2_b2 = C2 * np.dot(Y - np.dot(np.dot(np.dot(Y, G.T), np.linalg.inv((I/C2)+GYGT)), np.dot(G, Y)), 
                                np.dot(G.T, np.ones((G.T.shape[1], 1))))
            w2 = w2_b2[:-1,  :]
            b2 = w2_b2[ -1,  :]
            
        else:
            Z = self.calc_Y_or_Z(G)
            
            #w1, b1
            HZHT = np.dot(np.dot(H, Z), H.T)
            I = np.eye(HZHT.shape[0], HZHT.shape[1])
            w1_b1 = -C1*np.dot(Z - np.dot(np.dot(np.dot(Z, H.T), np.linalg.inv((I/C1) + HZHT)), np.dot(H, Z)),
                               np.dot(H.T, np.ones((H.T.shape[1], 1))))
            w1 = w1_b1[:-1,  :]
            b1 = w1_b1[ -1,  :]
            
            #w2, b2
            w2_b2 = np.dot(Z - np.dot(np.dot(np.dot(Z, H.T), np.linalg.inv(C2*I + HZHT)), np.dot(H, Z)), 
                           np.dot(G.T, np.ones((G.T.shape[1], 1))))
            w2 = w2_b2[:-1,  :]
            b2 = w2_b2[ -1,  :]

        self.w1 = w1
        self.w2 = w2
        self.b1 = b1
        self.b2 = b2
    
    def predict(self, x_test, y_test):
        distance1 = np.abs(np.dot(x_test, self.w1) + self.b1)
        distance2 = np.abs(np.dot(x_test, self.w2) + self.b2)
        y_pred = np.zeros_like(y_test)
        for d in range(y_pred.shape[0]):
            if (distance1[d] < distance2[d]):
                y_pred[d][0] =  1;
            else:
                y_pred[d][0] = -1;
        self.preds = y_pred
    
    def calc_Y_or_Z(self, M):
        MMT = np.dot(M, M.T)
        I = np.eye(MMT.shape[0], MMT.shape[1])
        tmp = np.dot(np.dot(M.T, np.linalg.inv(self.eps*I + MMT)), M)
        I = np.eye(tmp.shape[0], tmp.shape[1])
        return (1/self.eps)*(I-tmp)
    
    def get_params(self):
        return self.w1, self.b1, self.w2, self.b2
    

    def get_preds(self):
        return self.preds
    
    def score(self, y_test):
        accuracy = np.sum(self.preds == y_test)/y_test.shape[0]
        return accuracy