# Authors: Christian Thurau
# License: BSD 3 Clause
"""  
PyMF Semi Non-negative Matrix Factorization.

    SNMF(NMF) : Class for semi non-negative matrix factorization
    
[1] Ding, C., Li, T. and Jordan, M.. Convex and Semi-Nonnegative Matrix Factorizations.
IEEE Trans. on Pattern Analysis and Machine Intelligence 32(1), 45-55. 
"""
import numpy as np
from .base import PyMFBase

__all__ = ["SNMF"]

class SNMF(PyMFBase):
    """      
    SNMF(data, num_bases=4)
    
    Semi Non-negative Matrix Factorization. Factorize a data matrix into two 
    matrices s.t. F = | data - W*H | is minimal. For Semi-NMF only H is 
    constrained to non-negativity.
    
    Parameters
    ----------
    data : array_like, shape (_data_dimension, _num_samples)
        the input data
    num_bases: int, optional
        Number of bases to compute (column rank of W and row rank of H).
        4 (default)       
    
    Attributes
    ----------
    W : "data_dimension x num_bases" matrix of basis vectors
    H : "num bases x num_samples" matrix of coefficients
    ferr : frobenius norm (after calling .factorize())
        
    Example
    -------
    Applying Semi-NMF to some rather stupid data set:
    
    >>> import numpy as np
    >>> data = np.array([[1.0, 0.0, 2.0], [0.0, 1.0, 1.0]])
    >>> snmf_mdl = SNMF(data, num_bases=2)
    >>> snmf_mdl.factorize(niter=10)
    
    The basis vectors are now stored in snmf_mdl.W, the coefficients in snmf_mdl.H. 
    To compute coefficients for an existing set of basis vectors simply    copy W 
    to snmf_mdl.W, and set compute_w to False:
    
    >>> data = np.array([[1.5], [1.2]])
    >>> W = np.array([[1.0, 0.0], [0.0, 1.0]])
    >>> snmf_mdl = SNMF(data, num_bases=2)
    >>> snmf_mdl.W = W
    >>> snmf_mdl.factorize(niter=1, compute_w=False)
    
    The result is a set of coefficients snmf_mdl.H, s.t. data = W * snmf_mdl.H. 
    """


    def _update_w(self):
        W1 = np.dot(self.data[:,:], self.H.T)
        W2 = np.dot(self.H, self.H.T)    
        self.W = np.dot(W1, np.linalg.inv(W2))
        
    def _update_h(self):
        def separate_positive(m):
            return (np.abs(m) + m)/2.0 
        
        def separate_negative(m):
            return (np.abs(m) - m)/2.0
            
        XW = np.dot(self.data[:,:].T, self.W)                

        WW = np.dot(self.W.T, self.W)
        WW_pos = separate_positive(WW)
        WW_neg = separate_negative(WW)
        
        XW_pos = separate_positive(XW)
        H1 = (XW_pos + np.dot(self.H.T, WW_neg)).T
        
        XW_neg = separate_negative(XW)
        H2 = (XW_neg + np.dot(self.H.T, WW_pos)).T + 10**-9
        
        self.H *= np.sqrt(H1/H2)    

def _test():
    import doctest
    doctest.testmod()
 
if __name__ == "__main__":
    _test()
