"""Hyper-distributions."""

from libqif.core.secrets import Secrets
from libqif.core.channel import Channel
from numpy import array, arange, zeros
from numpy import delete as npdelete

class Hyper:

    def __init__(self, channel):
        """Hyper-distribution. To create an instance of this class it is
        class it is necessary to have an instance of :py:class:`.Channel`
        class. Once created an instance of :py:class:`.Hyper`, the constructor
        generates the joint, outer and inner distributions.

        Attributes
        ----------
        channel : core.Channel
            Channel object. 
        
        joint : numpy.ndarray
            Matrix of joint distribution.

        outer : numpy.ndarray
            Outer distribution.

        inners : numpy.ndarray
            Matrix of inner distributions. 

        num_posteriors : int
            Number of posterior distributions resulted by reducing the
            hyper-distribution, i.e., remove columns that contains only
            zeros and merge columns which one of them a linear combination
            of the other.

        Parameters
        ----------
        channel : core.Channel
            Channel object.
        """
        self._check_types(channel)
        self.channel = channel
        self.joint = self._generate_joint_distribution()
        self.outer, self.inners = self._generate_posteriors()
        self._reduce_hyper()
        self.num_posteriors = len(self.outer)

    def update_prior(self, prior):
        """Update the prior distribution on set of secrets.
        The number of secrets must match the current number of rows of the channel.

        Parameters
        ----------
        prior : list, numpy.ndarray
            Prior distribution on the set of secrets. prior[i] is the
            probability of secret named labels[i] beeing the real secret.
        """

        self.channel.update_prior(prior)
        self.joint = self._generate_joint_distribution()
        self.outer, self.inners = self._generate_posteriors()
        self._reduce_hyper()
        self.num_posteriors = len(self.outer)
    
    def _check_types(self, channel):
        if type(channel) != type(Channel(Secrets(['x1','x2'], [1,0]), ['y1'], array([[1],[1]]))):
            raise TypeError('The parameter \'channel\' must be a core.channel.Channel object')

    def _generate_joint_distribution(self):
        joint = []
        channel_t = self.channel.matrix.T
        for i in arange(self.channel.num_outputs):
            joint.append(self.channel.secrets.prior * channel_t[i])

        return array(joint).T            

    def _generate_posteriors(self):
        joint_t = self.joint.T.copy()
        outer = []
        for i in arange(self.channel.num_outputs):
            outer.append(joint_t[i].sum())
            if outer[i] > 0:
                joint_t[i] = joint_t[i]/outer[i]
        
        return array(outer), joint_t.T

    def _reduce_hyper(self):
        """Given the hyper-distribution generated by _generate_posteriors
        remove columns with zeros and merge columns that are a linear
        combination of others. Thus algorithm has time complexity of O(n*m^2)
        where n is the number of secrets and m is the number of outputs in
        the.
        """
        epsilon = 10**(-6)

        # Delete inners that have 0 probability of occuring
        zero_prob = self.outer < epsilon
        self.outer = npdelete(self.outer, zero_prob, 0)
        self.inners = npdelete(self.inners, zero_prob, 1)

        delete_inner = [False] * len(self.outer)
        for i in arange(self.inners.shape[1]):
            for j in arange(i+1, self.inners.shape[1]):
                # Check if inner i is equal to inner j
                if (abs(self.inners[:,i] - self.inners[:,j]) < epsilon).sum() == self.channel.secrets.num_secrets:
                    delete_inner[j] = True # Delete inner j
                    self.outer[i] += self.outer[j] # Merge inner j into inner i

        self.outer = npdelete(self.outer, delete_inner, 0)
        self.inners = npdelete(self.inners, delete_inner, 1)        

[((72, 17, 72, 49), 'numpy.arange', 'arange', ({(72, 24, 72, 48): 'self.channel.num_outputs'}, {}), '(self.channel.num_outputs)', False, 'from numpy import array, arange, zeros\n'), ((80, 17, 80, 49), 'numpy.arange', 'arange', ({(80, 24, 80, 48): 'self.channel.num_outputs'}, {}), '(self.channel.num_outputs)', False, 'from numpy import array, arange, zeros\n'), ((98, 21, 98, 55), 'numpy.delete', 'npdelete', ({(98, 30, 98, 40): 'self.outer', (98, 42, 98, 51): 'zero_prob', (98, 53, 98, 54): '0'}, {}), '(self.outer, zero_prob, 0)', True, 'from numpy import delete as npdelete\n'), ((99, 22, 99, 57), 'numpy.delete', 'npdelete', ({(99, 31, 99, 42): 'self.inners', (99, 44, 99, 53): 'zero_prob', (99, 55, 99, 56): '1'}, {}), '(self.inners, zero_prob, 1)', True, 'from numpy import delete as npdelete\n'), ((102, 17, 102, 45), 'numpy.arange', 'arange', ({(102, 24, 102, 44): 'self.inners.shape[1]'}, {}), '(self.inners.shape[1])', False, 'from numpy import array, arange, zeros\n'), ((109, 21, 109, 58), 'numpy.delete', 'npdelete', ({(109, 30, 109, 40): 'self.outer', (109, 42, 109, 54): 'delete_inner', (109, 56, 109, 57): '0'}, {}), '(self.outer, delete_inner, 0)', True, 'from numpy import delete as npdelete\n'), ((110, 22, 110, 60), 'numpy.delete', 'npdelete', ({(110, 31, 110, 42): 'self.inners', (110, 44, 110, 56): 'delete_inner', (110, 58, 110, 59): '1'}, {}), '(self.inners, delete_inner, 1)', True, 'from numpy import delete as npdelete\n'), ((75, 15, 75, 27), 'numpy.array', 'array', ({(75, 21, 75, 26): 'joint'}, {}), '(joint)', False, 'from numpy import array, arange, zeros\n'), ((85, 15, 85, 27), 'numpy.array', 'array', ({(85, 21, 85, 26): 'outer'}, {}), '(outer)', False, 'from numpy import array, arange, zeros\n'), ((103, 21, 103, 54), 'numpy.arange', 'arange', ({(103, 28, 103, 31): '(i + 1)', (103, 33, 103, 53): 'self.inners.shape[1]'}, {}), '(i + 1, self.inners.shape[1])', False, 'from numpy import array, arange, zeros\n'), ((66, 41, 66, 68), 'libqif.core.secrets.Secrets', 'Secrets', ({(66, 49, 66, 60): "['x1', 'x2']", (66, 62, 66, 67): '[1, 0]'}, {}), "(['x1', 'x2'], [1, 0])", False, 'from libqif.core.secrets import Secrets\n'), ((66, 78, 66, 94), 'numpy.array', 'array', ({(66, 84, 66, 93): '[[1], [1]]'}, {}), '([[1], [1]])', False, 'from numpy import array, arange, zeros\n')]