"""
.. codeauthor:: Ivano Lauriola <ivanolauriola@gmail.com>

==========
Evaluation
==========

.. currentmodule:: MKLpy.metrics.evaluate

This module contains functions that given a kernel returns a value calculated
with a metric.
These functions can be used also in heuristic algorithms, for example we can
assign a value to each kernel in list using the radius of MEB, or the margin.

"""

from cvxopt import matrix,solvers,spdiag
from ..utils import validation

def radius(K):
    """evaluate the radius of the MEB (Minimum Enclosing Ball) of examples in
    feature space.

    Parameters
    ----------
    K : (n,n) ndarray,
        the kernel that represents the data.

    Returns
    -------
    r : np.float64,
        the radius of the minimum enclosing ball of examples in feature space.
    """
    K = validation.check_K(K).numpy()
    n = K.shape[0]
    P = 2 * matrix(K)
    p = -matrix(K.diagonal())
    G = -spdiag([1.0] * n)
    h = matrix([0.0] * n)
    A = matrix([1.0] * n).T
    b = matrix([1.0])
    solvers.options['show_progress']=False
    sol = solvers.qp(P,p,G,h,A,b)
    return abs(sol['primal objective'])**.5

def margin(K,Y):
    """evaluate the margin in a classification problem of examples in feature space.
    If the classes are not linearly separable in feature space, then the
    margin obtained is 0.

    Note that it works only for binary tasks.

    Parameters
    ----------
    K : (n,n) ndarray,
        the kernel that represents the data.
    Y : (n) array_like,
        the labels vector.
    """
    K, Y = validation.check_K_Y(K, Y, binary=True)
    n = Y.size()[0]
    Y = [1 if y==Y[0] else -1 for y in Y]
    YY = spdiag(Y)
    P = 2*(YY*matrix(K.numpy())*YY)
    p = matrix([0.0]*n)
    G = -spdiag([1.0]*n)
    h = matrix([0.0]*n)
    A = matrix([[1.0 if Y[i]==+1 else 0 for i in range(n)],
                [1.0 if Y[j]==-1 else 0 for j in range(n)]]).T
    b = matrix([[1.0],[1.0]],(2,1))
    solvers.options['show_progress']=False
    sol = solvers.qp(P,p,G,h,A,b)
    return sol['primal objective']**.5

def ratio(K,Y):
    """evaluate the ratio between the radius of MEB and the margin in feature space.
    this ratio is defined as
    .. math:: \frac{R^2}{n\cdot\rho^2}

    Parameters
    ----------
    K : (n,n) ndarray,
        the kernel that represents the data.
    Y : (n) array_like,
        the labels vector.

    Returns
    -------
    v : np.float64,
        the value of the ratio
    """
    K, Y = validation.check_K_Y(K, Y, binary=True)
    r2 = radius(K)**2
    m2 = margin(K,Y)**2
    return (r2/m2)/len(Y)
    #return ((radius(K)**2)/(margin(K,Y)**2))/n


def trace(K):
    """return the trace of the kernel as input.

    Parameters
    ----------
    K : (n,n) ndarray,
        the kernel that represents the data.

    Returns
    -------
    t : np.float64,
        the trace of *K*
    """
    K = validation.check_K(K)
    return K.diag().sum().item()

def frobenius(K):
    """return the frobenius-norm of the kernel as input.

    Parameters
    ----------
    K : (n,n) ndarray,
        the kernel that represents the data.

    Returns
    -------
    t : np.float64,
        the frobenius-norm of *K*
    """
    K = validation.check_K(K)
    return ( (K**2).sum()**.5 ).item()

def spectral_ratio(K,norm=True):
    """return the spectral ratio of the kernel as input.

    Parameters
    ----------
    K : (n,n) ndarray,
        the kernel that represents the data.
    norm : bool=True,
           True if we want the normalized spectral ratio.
    
    Returns
    -------
    t : np.float64,
        the spectral ratio of *K*, normalized iif *norm=True*
    """
    K = validation.check_K(K)
    n = K.size()[0]
    c = trace(K)/frobenius(K)
    return (c-1)/(n**.5 - 1) if norm else c