# Copyright (c) 2013, GPy authors (see AUTHORS.txt).
# Licensed under the BSD 3-clause license (see LICENSE.txt)

from kernpart import Kernpart
import numpy as np
from ...util.linalg import tdot
from ...core.mapping import Mapping
import GPy

class Hetero(Kernpart):
    """
    TODO: Need to constrain the function outputs
    positive (still thinking of best way of doing this!!! Yes, intend to use
    transformations, but what's the *best* way). Currently just squaring output.

    Heteroschedastic noise which depends on input location. See, for example,
    this paper by Goldberg et al.

    .. math::

       k(x_i, x_j) = \delta_{i,j} \sigma^2(x_i)

       where :math:`\sigma^2(x)` is a function giving the variance  as a function of input space and :math:`\delta_{i,j}` is the Kronecker delta function.

    The parameters are the parameters of \sigma^2(x) which is a
    function that can be specified by the user, by default an
    multi-layer peceptron is used.

    :param input_dim: the number of input dimensions
    :type input_dim: int
    :param mapping: the mapping that gives the lengthscale across the input space (by default GPy.mappings.MLP is used with 20 hidden nodes).
    :type mapping: GPy.core.Mapping
    :rtype: Kernpart object

    See this paper:

    Goldberg, P. W.  Williams, C. K. I. and Bishop,
    C. M. (1998) Regression with Input-dependent Noise: a Gaussian
    Process Treatment In Advances in Neural Information Processing
    Systems, Volume 10, pp.  493-499. MIT Press

    for a Gaussian process treatment of this problem.

    """

    def __init__(self, input_dim, mapping=None, transform=None):
        self.input_dim = input_dim
        if not mapping:
            mapping = GPy.mappings.MLP(output_dim=1, hidden_dim=20, input_dim=input_dim)
        if not transform:
            transform = GPy.core.transformations.logexp()

        self.transform = transform
        self.mapping = mapping
        self.name='hetero'
        self.num_params=self.mapping.num_params
        self._set_params(self.mapping._get_params())

    def _get_params(self):
        return self.mapping._get_params()

    def _set_params(self, x):
        assert x.size == (self.num_params)
        self.mapping._set_params(x)

    def _get_param_names(self):
        return self.mapping._get_param_names()

    def K(self, X, X2, target):
        """Return covariance between X and X2."""
        if (X2 is None) or (X2 is X):
            target[np.diag_indices_from(target)] += self._Kdiag(X)

    def Kdiag(self, X, target):
        """Compute the diagonal of the covariance matrix for X."""
        target+=self._Kdiag(X)

    def _Kdiag(self, X):
        """Helper function for computing the diagonal elements of the covariance."""
        return self.mapping.f(X).flatten()**2

    def dK_dtheta(self, dL_dK, X, X2, target):
        """Derivative of the covariance with respect to the parameters."""
        if (X2 is None) or (X2 is X):
            dL_dKdiag = dL_dK.flat[::dL_dK.shape[0]+1]
            self.dKdiag_dtheta(dL_dKdiag, X, target)

    def dKdiag_dtheta(self, dL_dKdiag, X, target):
        """Gradient of diagonal of covariance with respect to parameters."""
        target += 2.*self.mapping.df_dtheta(dL_dKdiag[:, None]*self.mapping.f(X), X)

    def dK_dX(self, dL_dK, X, X2, target):
        """Derivative of the covariance matrix with respect to X."""
        if X2==None or X2 is X:
            dL_dKdiag = dL_dK.flat[::dL_dK.shape[0]+1]
            self.dKdiag_dX(dL_dKdiag, X, target)

    def dKdiag_dX(self, dL_dKdiag, X, target):
        """Gradient of diagonal of covariance with respect to X."""
        target += 2.*self.mapping.df_dX(dL_dKdiag[:, None], X)*self.mapping.f(X)



