## Penalized Discriminant Analysis.

## This code is written by Roberto Visintainer, <visintainer@fbk.eu> and
## Davide Albanese, <albanese@fbk.eu>.
## (C) 2008 Fondazione Bruno Kessler - Via Santa Croce 77, 38100 Trento, ITALY.

## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.

## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.

## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.


__all__ = ['Pda']

from numpy import *
from numpy.linalg import inv, LinAlgError


class Pda:
    """
    Penalized Discriminant Analysis (PDA).
    
    Example:
    
    >>> import numpy as np
    >>> import mlpy
    >>> xtr = np.array([[1.0, 2.0, 3.1, 1.0],  # first sample
    ...                 [1.0, 2.0, 3.0, 2.0],  # second sample
    ...                 [1.0, 2.0, 3.1, 1.0]]) # third sample
    >>> ytr = np.array([1, -1, 1])             # classes
    >>> mypda = mlpy.Pda()                   # initialize pda class
    >>> mypda.compute(xtr, ytr)              # compute pda
    1
    >>> mypda.predict(xtr)                   # predict pda model on training data
    array([ 1, -1,  1])
    >>> xts = np.array([4.0, 5.0, 6.0, 7.0]) # test point
    >>> mypda.predict(xts)                   # predict pda model on test point
    -1
    >>> mypda.realpred                       # real-valued prediction
    -7.6106885609535624
    >>> mypda.weights(xtr, ytr)              # compute weights on training data
    array([  4.0468174 ,   8.0936348 ,  18.79228266,  58.42466988])

    """

    def __init__ (self, Nreg = 3):
        """
        Initialize Pda class.

        :Parameters:
          Nreg : int
            number of regressions
        """
    

        if Nreg < 1:
            raise ValueError("Nreg must be >= 1")

        self.__Nreg = Nreg
        
        self.__x = None
        self.__y = None

        self.__onep = None
        self.__onen = None

        self.__OptF = None
        self.__computed = False
        self.__SingularMatrix = False
        self.realpred = None

    def __PenRegrModel(self, Th0):
        """
        Penalized Regression Model
        Perform a Partial Least Squares Regression
        on Matrix of training data x as the predictor
        and the vector Th0.

        :Returns:
          optimal scores.
        """

        a = dot(Th0 , self.__x)

        if  self.__Nreg == 1:
            A = a
        else:
            A = empty((self.__x.shape[1], self.__Nreg))
            A[:, 0] = a

        T = empty((self.__x.shape[0], self.__Nreg))
        T[:, 0] = dot(self.__x , a)

        T0  = T[:, 0]
        T0T = T0.transpose()

        TT   = dot(T0T, T0)
        TTi  = 1.0 / TT
        TTh0 = dot(T0T, Th0)

        r = Th0 - (T0 * TTi * inner(T0, Th0))
                       
        for l in range(1, self.__Nreg):
            
            A[:, l] = dot(r, self.__x)
            T[:, l] = dot(self.__x, A[:, l])

            Tl  = T[:,:l+1]
            TlT = Tl.transpose()

            TT  = dot(TlT, Tl)
            TTi = inv(TT)                          
            TTh0 = dot(TlT, Th0)
            r    = Th0 - dot(Tl, dot(TTi, TTh0))
        
        q = dot(TTi, TTh0)
        B = dot(A, q)

        return B


    def compute (self, x, y):
        """
        Compute Pda model.

        :Parameters:  
          x : 2d ndarray float (samples x feats)
            training data
          y : 1d ndarray integer (-1 or 1)
            classes
            
        :Returns:
          1

        :Raises:
          LinAlgError
            if x is singular matrix in __PenRegrModel
        """
        
        self.__lp = y[y ==  1].shape[0]
        self.__ln = y[y == -1].shape[0]
        
        onep = zeros_like(y)
        onen = zeros_like(y)
        onep[y == 1 ] = 1
        onen[y == -1] = 1

        self.__x = x
        self.__y = y
                   
        Tha = self.__x.shape[0] / float(self.__ln)
        Thb = self.__x.shape[0] / float(self.__lp)
        Th  = array([Tha , -Thb])
        
        Z = empty((self.__x.shape[0] , 2))
        Z[:,0] = onen
        Z[:,1] = onep
        
        Th0 = dot(Z, Th)

        try:
            Be = self.__PenRegrModel(Th0)
        except LinAlgError:
            self.__SingularMatrix = True
            return 0
        else:
            Ths = dot(self.__x, Be)
            Ph  = dot(Ths, Th0)
            self.__OptF = Ph * Be
            self.__computed = True
            return 1
    
        
    def weights (self, x, y):
        """
        Compute feature weights.
        
        :Parameters:
          x : 2d ndarray float (samples x feats)
            training data
          y : 1d ndarray integer (-1 or 1)
            classes
        
        :Returns:
          fw :  1d ndarray float
            feature weights
        """

        self.compute(x, y)

        if self.__SingularMatrix == True:
            return zeros(x.shape[1], dtype = float)
        
        return abs(self.__OptF)
               

    def predict (self, p):
        """
        Predict Pda model on test point(s).

        :Parameters:
          p : 1d or 2d ndarray float (sample(s) x feats)
            test sample(s)

        :Returns:
          cl : integer or 1d numpy array integer
            class(es) predicted

        :Attributes:
          self.realpred : float or 1d numpy array float
            real valued prediction
        """

        if self.__SingularMatrix == True:
            if p.ndim == 2:
                self.realpred = zeros(p.shape[0], dtype = float)
                return zeros(p.shape[0], dtype = int)
            elif p.ndim == 1:
                self.realpred = 0.0
                return 0

        niNEGn = 0
        niPOSn = 0
        
        NI = dot(self.__x, self.__OptF) 
        niNEGn = sum(NI[where(self.__y == -1)])
        niPOSn = sum(NI[where(self.__y == 1)])
        
        niNEG = niNEGn / self.__ln
        niPOS = niPOSn / self.__lp
        
        niMEAN = (niNEG + niPOS) / 2.0
        niDEN  = niPOS - niMEAN
               
        if p.ndim == 2:
            pred = zeros((p.shape[0]), int)

            d = dot(p, self.__OptF)
            delta1 = (d - niNEG)**2
            delta2 = (d - niPOS)**2
            
            pred[where(delta1 < delta2)] = -1
            pred[where(delta1 > delta2)] = 1

            # Real prediction
            self.realpred = (d - niMEAN) / niDEN             
          
        elif p.ndim == 1:
            pred = 0

            d = inner(p, self.__OptF)
            delta1 = (d - niNEG)**2
            delta2 = (d - niPOS)**2         
                
            if delta1 < delta2:
                pred = -1
            elif delta2 < delta1:
                pred = 1

            # Real prediction
            self.realpred = (d - niMEAN) / niDEN
          
        return pred


