# Path: morb/base.py
# class Parameters(object):
#     def __init__(self, rbm, units_list, name=None):
#         self.rbm = rbm
#         self.units_list = units_list
#         self.terms = {} # terms is a dict of FUNCTIONS that take a vmap.
#         self.energy_gradients = {} # a dict of FUNCTIONS that take a vmap.
#         self.energy_gradient_sums = {} # a dict of FUNCTIONS that take a vmap.
#         self.name = name
#         self.rbm.add_parameters(self)
#         
#     def activation_term_for(self, units, vmap):
#         return self.terms[units](vmap)
#         
#     def energy_gradient_for(self, variable, vmap):
#         """
#         Returns the energy gradient for each example in the batch.
#         """
#         return self.energy_gradients[variable](vmap)
#         
#     def energy_gradient_sum_for(self, variable, vmap):
#         """
#         Returns the energy gradient, summed across the minibatch dimension.
#         If a fast implementation for this is available in the energy_gradient_sums
#         dictionary, this will be used. Else the energy gradient will be computed
#         for each example in the batch (using the implementation from the
#         energy_gradients dictionary) and then summed.
#         
#         Take a look at the ProdParameters implementation for an example of where
#         this is useful: the gradient summed over the batch can be computed more
#         efficiently with a dot product.
#         """
#         if variable in self.energy_gradient_sums:
#             return self.energy_gradient_sums[variable](vmap)
#         else:
#             return T.sum(self.energy_gradients[variable](vmap), axis=0)
#         
#     def energy_term(self, vmap):
#         raise NotImplementedError("Parameters base class")
#         
#     def affects(self, units):
#         return (units in self.units_list)
#         
#     def __repr__(self):
#         units_names = ", ".join(("'%s'" % u.name) for u in self.units_list)
#         return "<morb:Parameters '%s' affecting %s>" % (self.name, units_names)
# 
# Path: morb/misc.py
# def tensordot(a, b, axes=2):
#     """
#     implementation of tensordot that reduces to a regular matrix product. This allows tensordot to be GPU accelerated,
#     which isn't possible with the default Theano implementation (which is just a wrapper around numpy.tensordot).
#     based on code from Tijmen Tieleman's gnumpy http://www.cs.toronto.edu/~tijmen/gnumpy.html
#     """
#     if numpy.isscalar(axes):
#         # if 'axes' is a number of axes to multiply and sum over (trailing axes
#         # of a, leading axes of b), we can just reshape and use dot.         
#         outshape = tensor.concatenate([a.shape[:a.ndim - axes], b.shape[axes:]])
#         outndim = a.ndim + b.ndim - 2*axes
#         a_reshaped = a.reshape((tensor.prod(a.shape[:a.ndim - axes]), tensor.prod(a.shape[a.ndim - axes:])))
#         b_reshaped = b.reshape((tensor.prod(b.shape[:axes]), tensor.prod(b.shape[axes:])))
#         return tensor.dot(a_reshaped, b_reshaped).reshape(outshape, ndim=outndim)
#     elif len(axes) == 2:
#         # if 'axes' is a pair of axis lists, we first shuffle the axes of a and
#         # b to reduce this to the first case (note the recursion).
#         a_other, b_other = tuple(axes[0]), tuple(axes[1])
#         num_axes = len(a_other)
#         a_order = tuple(x for x in tuple(xrange(a.ndim)) if x not in a_other) + a_other
#         b_order = b_other + tuple(x for x in tuple(xrange(b.ndim)) if x not in b_other)
#         a_shuffled = a.dimshuffle(a_order)
#         b_shuffled = b.dimshuffle(b_order)
#         return tensordot(a_shuffled, b_shuffled, num_axes)
#     else:
#         raise ValueError("Axes should be scalar valued or a list/tuple of len 2.")


from morb.base import Parameters
from theano.tensor.nnet import conv
from morb.misc import tensordot # better tensordot implementation that can be GPU accelerated
import theano
import theano.tensor as T
        super(BiasParameters, self).__init__(rbm, [units], name=name)
        self.var = b
        self.variables = [self.var]
        self.u = units
        
        self.terms[self.u] = lambda vmap: self.var
        
        self.energy_gradients[self.var] = lambda vmap: vmap[self.u]
        
    def energy_term(self, vmap):
        return - T.dot(vmap[self.u], self.var)
        # bias is NOT TRANSPOSED because it's a vector, and apparently vectors are COLUMN vectors by default.


class AdvancedProdParameters(Parameters):
    def __init__(self, rbm, units_list, dimensions_list, W, name=None):
        super(AdvancedProdParameters, self).__init__(rbm, units_list, name=name)
        assert len(units_list) == 2
        self.var = W
        self.variables = [self.var]
        self.vu = units_list[0]
        self.hu = units_list[1]
        self.vd = dimensions_list[0]
        self.hd = dimensions_list[1]
        self.vard = self.vd + self.hd
        
        # there are vd visible dimensions and hd hidden dimensions, meaning that the weight matrix has
        # vd + hd = Wd dimensions.
        # the hiddens and visibles have hd+1 and vd+1 dimensions respectively, because the first dimension
        # is reserved for minibatches!

# Path: morb/base.py
# class Parameters(object):
#     def __init__(self, rbm, units_list, name=None):
#         self.rbm = rbm
#         self.units_list = units_list
#         self.terms = {} # terms is a dict of FUNCTIONS that take a vmap.
#         self.energy_gradients = {} # a dict of FUNCTIONS that take a vmap.
#         self.energy_gradient_sums = {} # a dict of FUNCTIONS that take a vmap.
#         self.name = name
#         self.rbm.add_parameters(self)
#         
#     def activation_term_for(self, units, vmap):
#         return self.terms[units](vmap)
#         
#     def energy_gradient_for(self, variable, vmap):
#         """
#         Returns the energy gradient for each example in the batch.
#         """
#         return self.energy_gradients[variable](vmap)
#         
#     def energy_gradient_sum_for(self, variable, vmap):
#         """
#         Returns the energy gradient, summed across the minibatch dimension.
#         If a fast implementation for this is available in the energy_gradient_sums
#         dictionary, this will be used. Else the energy gradient will be computed
#         for each example in the batch (using the implementation from the
#         energy_gradients dictionary) and then summed.
#         
#         Take a look at the ProdParameters implementation for an example of where
#         this is useful: the gradient summed over the batch can be computed more
#         efficiently with a dot product.
#         """
#         if variable in self.energy_gradient_sums:
#             return self.energy_gradient_sums[variable](vmap)
#         else:
#             return T.sum(self.energy_gradients[variable](vmap), axis=0)
#         
#     def energy_term(self, vmap):
#         raise NotImplementedError("Parameters base class")
#         
#     def affects(self, units):
#         return (units in self.units_list)
#         
#     def __repr__(self):
#         units_names = ", ".join(("'%s'" % u.name) for u in self.units_list)
#         return "<morb:Parameters '%s' affecting %s>" % (self.name, units_names)
# 
# Path: morb/misc.py
# def tensordot(a, b, axes=2):
#     """
#     implementation of tensordot that reduces to a regular matrix product. This allows tensordot to be GPU accelerated,
#     which isn't possible with the default Theano implementation (which is just a wrapper around numpy.tensordot).
#     based on code from Tijmen Tieleman's gnumpy http://www.cs.toronto.edu/~tijmen/gnumpy.html
#     """
#     if numpy.isscalar(axes):
#         # if 'axes' is a number of axes to multiply and sum over (trailing axes
#         # of a, leading axes of b), we can just reshape and use dot.         
#         outshape = tensor.concatenate([a.shape[:a.ndim - axes], b.shape[axes:]])
#         outndim = a.ndim + b.ndim - 2*axes
#         a_reshaped = a.reshape((tensor.prod(a.shape[:a.ndim - axes]), tensor.prod(a.shape[a.ndim - axes:])))
#         b_reshaped = b.reshape((tensor.prod(b.shape[:axes]), tensor.prod(b.shape[axes:])))
#         return tensor.dot(a_reshaped, b_reshaped).reshape(outshape, ndim=outndim)
#     elif len(axes) == 2:
#         # if 'axes' is a pair of axis lists, we first shuffle the axes of a and
#         # b to reduce this to the first case (note the recursion).
#         a_other, b_other = tuple(axes[0]), tuple(axes[1])
#         num_axes = len(a_other)
#         a_order = tuple(x for x in tuple(xrange(a.ndim)) if x not in a_other) + a_other
#         b_order = b_other + tuple(x for x in tuple(xrange(b.ndim)) if x not in b_other)
#         a_shuffled = a.dimshuffle(a_order)
#         b_shuffled = b.dimshuffle(b_order)
#         return tensordot(a_shuffled, b_shuffled, num_axes)
#     else:
#         raise ValueError("Axes should be scalar valued or a list/tuple of len 2.")


# Path: morb/parameters.py
from morb.base import Parameters
from theano.tensor.nnet import conv
from morb.misc import tensordot # better tensordot implementation that can be GPU accelerated
import theano
import theano.tensor as T

        super(BiasParameters, self).__init__(rbm, [units], name=name)
        self.var = b
        self.variables = [self.var]
        self.u = units
        
        self.terms[self.u] = lambda vmap: self.var
        
        self.energy_gradients[self.var] = lambda vmap: vmap[self.u]
        
    def energy_term(self, vmap):
        return - T.dot(vmap[self.u], self.var)
        # bias is NOT TRANSPOSED because it's a vector, and apparently vectors are COLUMN vectors by default.


class AdvancedProdParameters(Parameters):
    def __init__(self, rbm, units_list, dimensions_list, W, name=None):
        super(AdvancedProdParameters, self).__init__(rbm, units_list, name=name)
        assert len(units_list) == 2
        self.var = W
        self.variables = [self.var]
        self.vu = units_list[0]
        self.hu = units_list[1]
        self.vd = dimensions_list[0]
        self.hd = dimensions_list[1]
        self.vard = self.vd + self.hd
        
        # there are vd visible dimensions and hd hidden dimensions, meaning that the weight matrix has
        # vd + hd = Wd dimensions.
        # the hiddens and visibles have hd+1 and vd+1 dimensions respectively, because the first dimension
        # is reserved for minibatches!

        self.terms[self.vu] = lambda vmap: tensordot(vmap[self.hu], W, axes=(range(1,self.hd+1),range(self.vd, self.vard)))